using ZB.MOM.WW.MxGateway.Server.Security.Authorization;

namespace ZB.MOM.WW.MxGateway.Server.Security.Authentication;

public static class ApiKeyAdminCommandLineParser
{
    /// <summary>Parses command-line arguments for the API key admin subcommand.</summary>
    /// <param name="args">Command-line arguments to parse.</param>
    /// <returns>Parse result containing the command kind and options, or a failure message.</returns>
    public static ApiKeyAdminParseResult Parse(IReadOnlyList<string> args)
    {
        if (args.Count == 0 || !string.Equals(args[0], "apikey", StringComparison.OrdinalIgnoreCase))
        {
            return ApiKeyAdminParseResult.NotApiKeyCommand();
        }

        if (args.Count < 2)
        {
            return ApiKeyAdminParseResult.Fail("Missing apikey subcommand.");
        }

        if (!TryParseKind(args[1], out ApiKeyAdminCommandKind kind))
        {
            return ApiKeyAdminParseResult.Fail($"Unknown apikey subcommand '{args[1]}'.");
        }

        Dictionary<string, List<string?>> options = new(StringComparer.OrdinalIgnoreCase);
        bool json = false;

        for (int index = 2; index < args.Count; index++)
        {
            string arg = args[index];
            if (string.Equals(arg, "--json", StringComparison.OrdinalIgnoreCase))
            {
                json = true;
                continue;
            }

            if (!arg.StartsWith("--", StringComparison.Ordinal))
            {
                return ApiKeyAdminParseResult.Fail($"Unexpected argument '{arg}'.");
            }

            string name = arg[2..];
            string? value;

            int equalsIndex = name.IndexOf('=', StringComparison.Ordinal);
            if (equalsIndex >= 0)
            {
                value = name[(equalsIndex + 1)..];
                name = name[..equalsIndex];
            }
            else
            {
                if (index + 1 >= args.Count || args[index + 1].StartsWith("--", StringComparison.Ordinal))
                {
                    if (IsBooleanConstraintFlag(name))
                    {
                        value = "true";
                    }
                    else
                    {
                        return ApiKeyAdminParseResult.Fail($"Option '--{name}' requires a value.");
                    }
                }
                else
                {
                    value = args[++index];
                }
            }

            if (!options.TryGetValue(name, out List<string?>? values))
            {
                values = [];
                options[name] = values;
            }

            values.Add(value);
        }

        string? keyId = GetOption(options, "key-id");
        string? displayName = GetOption(options, "display-name");
        IReadOnlySet<string> scopes = ParseScopes(GetOption(options, "scopes"));
        ApiKeyConstraints constraints;
        DateTimeOffset? expiresUtc;
        try
        {
            constraints = ParseConstraints(options);
            expiresUtc = ParseExpiry(GetOption(options, "expires"));
        }
        catch (FormatException exception)
        {
            return ApiKeyAdminParseResult.Fail(exception.Message);
        }

        string? validationError = Validate(kind, keyId, displayName);
        if (validationError is not null)
        {
            return ApiKeyAdminParseResult.Fail(validationError);
        }

        string? scopeError = ValidateScopes(kind, scopes);
        if (scopeError is not null)
        {
            return ApiKeyAdminParseResult.Fail(scopeError);
        }

        return ApiKeyAdminParseResult.Success(new ApiKeyAdminCommand(
            Kind: kind,
            Json: json,
            SqlitePath: GetOption(options, "sqlite-path"),
            Pepper: GetOption(options, "pepper"),
            KeyId: keyId,
            DisplayName: displayName,
            Scopes: scopes,
            Constraints: constraints,
            ExpiresUtc: expiresUtc));
    }

    private static bool TryParseKind(string value, out ApiKeyAdminCommandKind kind)
    {
        switch (value.ToLowerInvariant())
        {
            case "init-db":
                kind = ApiKeyAdminCommandKind.InitDb;
                return true;
            case "create-key":
                kind = ApiKeyAdminCommandKind.CreateKey;
                return true;
            case "list-keys":
                kind = ApiKeyAdminCommandKind.ListKeys;
                return true;
            case "revoke-key":
                kind = ApiKeyAdminCommandKind.RevokeKey;
                return true;
            case "rotate-key":
                kind = ApiKeyAdminCommandKind.RotateKey;
                return true;
            default:
                kind = default;
                return false;
        }
    }

    private static string? Validate(ApiKeyAdminCommandKind kind, string? keyId, string? displayName)
    {
        if (kind is ApiKeyAdminCommandKind.CreateKey or ApiKeyAdminCommandKind.RevokeKey or ApiKeyAdminCommandKind.RotateKey
            && string.IsNullOrWhiteSpace(keyId))
        {
            return $"Subcommand '{KindName(kind)}' requires --key-id.";
        }

        if (!string.IsNullOrWhiteSpace(keyId) && !IsValidKeyId(keyId))
        {
            return "API key id may contain only letters, numbers, periods, and hyphens.";
        }

        if (kind == ApiKeyAdminCommandKind.CreateKey && string.IsNullOrWhiteSpace(displayName))
        {
            return "Subcommand 'create-key' requires --display-name.";
        }

        return null;
    }

    private static string? ValidateScopes(ApiKeyAdminCommandKind kind, IReadOnlySet<string> scopes)
    {
        if (kind != ApiKeyAdminCommandKind.CreateKey)
        {
            return null;
        }

        string[] unknown = scopes.Where(scope => !GatewayScopes.IsKnown(scope)).ToArray();
        if (unknown.Length == 0)
        {
            return null;
        }

        return $"Unknown scope(s): {string.Join(", ", unknown)}. "
            + $"Valid scopes are: {string.Join(", ", GatewayScopes.All)}.";
    }

    private static string KindName(ApiKeyAdminCommandKind kind)
    {
        return kind switch
        {
            ApiKeyAdminCommandKind.InitDb => "init-db",
            ApiKeyAdminCommandKind.CreateKey => "create-key",
            ApiKeyAdminCommandKind.ListKeys => "list-keys",
            ApiKeyAdminCommandKind.RevokeKey => "revoke-key",
            ApiKeyAdminCommandKind.RotateKey => "rotate-key",
            _ => kind.ToString()
        };
    }

    private static bool IsValidKeyId(string keyId)
    {
        return keyId.All(character =>
            char.IsAsciiLetterOrDigit(character)
            || character is '.' or '-');
    }

    private static string? GetOption(Dictionary<string, List<string?>> options, string name)
    {
        return options.TryGetValue(name, out List<string?>? values) && values.Count > 0 ? values[^1] : null;
    }

    private static IReadOnlyList<string> GetOptions(Dictionary<string, List<string?>> options, string name)
    {
        return options.TryGetValue(name, out List<string?>? values)
            ? values.Where(value => !string.IsNullOrWhiteSpace(value)).Select(value => value!).ToArray()
            : Array.Empty<string>();
    }

    private static bool HasFlag(Dictionary<string, List<string?>> options, string name)
    {
        return options.ContainsKey(name);
    }

    private static bool IsBooleanConstraintFlag(string name)
    {
        return string.Equals(name, "read-alarm-only", StringComparison.OrdinalIgnoreCase)
            || string.Equals(name, "read-historized-only", StringComparison.OrdinalIgnoreCase);
    }

    private static ApiKeyConstraints ParseConstraints(Dictionary<string, List<string?>> options)
    {
        return new ApiKeyConstraints(
            ReadSubtrees: GetOptions(options, "read-subtree"),
            WriteSubtrees: GetOptions(options, "write-subtree"),
            ReadTagGlobs: GetOptions(options, "read-tag-glob"),
            WriteTagGlobs: GetOptions(options, "write-tag-glob"),
            MaxWriteClassification: ParseNullableInt(GetOption(options, "max-write-classification")),
            BrowseSubtrees: GetOptions(options, "browse-subtree"),
            ReadAlarmOnly: HasFlag(options, "read-alarm-only"),
            ReadHistorizedOnly: HasFlag(options, "read-historized-only"))
        {
            DashboardTags = ParseDashboardTags(options),
        };
    }

    // --dashboard-tags takes a comma-separated list ("team-a,team-b"); repeating the flag unions
    // its values. Segments are trimmed and de-duplicated ordinal-ignore-case, matching how the
    // enforcement site compares them. An empty segment is rejected rather than dropped: a stray
    // comma otherwise silently persists a grant the operator did not mean to write.
    private static IReadOnlyList<string> ParseDashboardTags(Dictionary<string, List<string?>> options)
    {
        if (!options.TryGetValue("dashboard-tags", out List<string?>? values))
        {
            return Array.Empty<string>();
        }

        List<string> tags = [];
        HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);

        foreach (string? raw in values)
        {
            foreach (string segment in (raw ?? string.Empty).Split(','))
            {
                string tag = segment.Trim();
                if (tag.Length == 0)
                {
                    throw new FormatException(
                        "--dashboard-tags must be a comma-separated list of non-empty tags.");
                }

                if (seen.Add(tag))
                {
                    tags.Add(tag);
                }
            }
        }

        return tags.Count == 0 ? Array.Empty<string>() : tags;
    }

    // Parses the optional --expires value into an absolute UTC expiry. Accepts a relative
    // "<N>d"/"<N>h" duration from now (operator-friendly) or an absolute ISO-8601 instant/date
    // (assumed UTC). Null/blank means no expiry — expiry stays opt-in, preserving prior behaviour.
    private static DateTimeOffset? ParseExpiry(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        string trimmed = value.Trim();
        char unit = char.ToLowerInvariant(trimmed[^1]);
        if (unit is 'd' or 'h'
            && int.TryParse(
                trimmed[..^1],
                System.Globalization.NumberStyles.None,
                System.Globalization.CultureInfo.InvariantCulture,
                out int amount))
        {
            TimeSpan offset = unit == 'd' ? TimeSpan.FromDays(amount) : TimeSpan.FromHours(amount);
            return DateTimeOffset.UtcNow + offset;
        }

        if (DateTimeOffset.TryParse(
                trimmed,
                System.Globalization.CultureInfo.InvariantCulture,
                System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal,
                out DateTimeOffset parsed))
        {
            return parsed;
        }

        throw new FormatException(
            "--expires must be a relative duration ('<N>d' or '<N>h') or an absolute ISO-8601 UTC timestamp.");
    }

    private static int? ParseNullableInt(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        return int.TryParse(
            value,
            System.Globalization.NumberStyles.Integer,
            System.Globalization.CultureInfo.InvariantCulture,
            out int parsed)
            ? parsed
            : throw new FormatException("--max-write-classification must be an integer.");
    }

    private static IReadOnlySet<string> ParseScopes(string? scopes)
    {
        return new HashSet<string>(
            (scopes ?? string.Empty)
                .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
            StringComparer.Ordinal);
    }
}
