using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.Plugin.Standard.Security;
using StellaOps.Cryptography.Audit;

namespace StellaOps.Authority.Plugin.Standard.Storage;

internal sealed class StandardUserCredentialStore : IUserCredentialStore
{
    private readonly IMongoCollection<StandardUserDocument> users;
    private readonly StandardPluginOptions options;
    private readonly IPasswordHasher passwordHasher;
    private readonly ILogger<StandardUserCredentialStore> logger;
    private readonly string pluginName;

    public StandardUserCredentialStore(
        string pluginName,
        IMongoDatabase database,
        StandardPluginOptions options,
        IPasswordHasher passwordHasher,
        ILogger<StandardUserCredentialStore> logger)
    {
        this.pluginName = pluginName ?? throw new ArgumentNullException(nameof(pluginName));
        this.options = options ?? throw new ArgumentNullException(nameof(options));
        this.passwordHasher = passwordHasher ?? throw new ArgumentNullException(nameof(passwordHasher));
        this.logger = logger ?? throw new ArgumentNullException(nameof(logger));

        ArgumentNullException.ThrowIfNull(database);

        var collectionName = $"authority_users_{pluginName.ToLowerInvariant()}";
        users = database.GetCollection<StandardUserDocument>(collectionName);
        EnsureIndexes();
    }

    public async ValueTask<AuthorityCredentialVerificationResult> VerifyPasswordAsync(
        string username,
        string password,
        CancellationToken cancellationToken)
    {
        var auditProperties = new List<AuthEventProperty>();

        if (string.IsNullOrWhiteSpace(username) || string.IsNullOrEmpty(password))
        {
            return AuthorityCredentialVerificationResult.Failure(AuthorityCredentialFailureCode.InvalidCredentials, auditProperties: auditProperties);
        }

        var normalized = NormalizeUsername(username);
        var user = await users.Find(u => u.NormalizedUsername == normalized)
            .FirstOrDefaultAsync(cancellationToken)
            .ConfigureAwait(false);

        if (user is null)
        {
            logger.LogWarning("Plugin {PluginName} failed password verification for unknown user {Username}.", pluginName, normalized);
            return AuthorityCredentialVerificationResult.Failure(AuthorityCredentialFailureCode.InvalidCredentials, auditProperties: auditProperties);
        }

        if (options.Lockout.Enabled && user.Lockout.LockoutEnd is { } lockoutEnd && lockoutEnd > DateTimeOffset.UtcNow)
        {
            var retryAfter = lockoutEnd - DateTimeOffset.UtcNow;
            logger.LogWarning("Plugin {PluginName} denied access for {Username} due to lockout (retry after {RetryAfter}).", pluginName, normalized, retryAfter);
            auditProperties.Add(new AuthEventProperty
            {
                Name = "plugin.lockout_until",
                Value = ClassifiedString.Public(lockoutEnd.ToString("O", CultureInfo.InvariantCulture))
            });

            return AuthorityCredentialVerificationResult.Failure(
                AuthorityCredentialFailureCode.LockedOut,
                "Account is temporarily locked.",
                retryAfter,
                auditProperties);
        }

        var verification = passwordHasher.Verify(password, user.PasswordHash);
        if (verification is PasswordVerificationResult.Success or PasswordVerificationResult.SuccessRehashNeeded)
        {
            if (verification == PasswordVerificationResult.SuccessRehashNeeded)
            {
                user.PasswordHash = passwordHasher.Hash(password);
                auditProperties.Add(new AuthEventProperty
                {
                    Name = "plugin.rehashed",
                    Value = ClassifiedString.Public("argon2id")
                });
            }

            var previousFailures = user.Lockout.FailedAttempts;
            ResetLockout(user);
            user.UpdatedAt = DateTimeOffset.UtcNow;
            await users.ReplaceOneAsync(
                Builders<StandardUserDocument>.Filter.Eq(u => u.Id, user.Id),
                user,
                cancellationToken: cancellationToken).ConfigureAwait(false);

            if (previousFailures > 0)
            {
                auditProperties.Add(new AuthEventProperty
                {
                    Name = "plugin.failed_attempts_cleared",
                    Value = ClassifiedString.Public(previousFailures.ToString(CultureInfo.InvariantCulture))
                });
            }

            var descriptor = ToDescriptor(user);
            return AuthorityCredentialVerificationResult.Success(
                descriptor,
                descriptor.RequiresPasswordReset ? "Password reset required." : null,
                auditProperties);
        }

        await RegisterFailureAsync(user, cancellationToken).ConfigureAwait(false);

        var code = options.Lockout.Enabled && user.Lockout.LockoutEnd is { } lockout
            ? AuthorityCredentialFailureCode.LockedOut
            : AuthorityCredentialFailureCode.InvalidCredentials;

        TimeSpan? retry = user.Lockout.LockoutEnd is { } lockoutTime && lockoutTime > DateTimeOffset.UtcNow
            ? lockoutTime - DateTimeOffset.UtcNow
            : null;

        auditProperties.Add(new AuthEventProperty
        {
            Name = "plugin.failed_attempts",
            Value = ClassifiedString.Public(user.Lockout.FailedAttempts.ToString(CultureInfo.InvariantCulture))
        });

        if (user.Lockout.LockoutEnd is { } pendingLockout)
        {
            auditProperties.Add(new AuthEventProperty
            {
                Name = "plugin.lockout_until",
                Value = ClassifiedString.Public(pendingLockout.ToString("O", CultureInfo.InvariantCulture))
            });
        }

        return AuthorityCredentialVerificationResult.Failure(
            code,
            code == AuthorityCredentialFailureCode.LockedOut ? "Account is temporarily locked." : "Invalid credentials.",
            retry,
            auditProperties);
    }

    public async ValueTask<AuthorityPluginOperationResult<AuthorityUserDescriptor>> UpsertUserAsync(
        AuthorityUserRegistration registration,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(registration);

        var normalized = NormalizeUsername(registration.Username);
        var now = DateTimeOffset.UtcNow;

        if (!string.IsNullOrEmpty(registration.Password))
        {
            var passwordValidation = ValidatePassword(registration.Password);
            if (passwordValidation is not null)
            {
                return AuthorityPluginOperationResult<AuthorityUserDescriptor>.Failure("password_policy_violation", passwordValidation);
            }
        }

        var existing = await users.Find(u => u.NormalizedUsername == normalized)
            .FirstOrDefaultAsync(cancellationToken)
            .ConfigureAwait(false);

        if (existing is null)
        {
            if (string.IsNullOrEmpty(registration.Password))
            {
                return AuthorityPluginOperationResult<AuthorityUserDescriptor>.Failure("password_required", "New users require a password.");
            }

            var document = new StandardUserDocument
            {
                Username = registration.Username,
                NormalizedUsername = normalized,
                DisplayName = registration.DisplayName,
                Email = registration.Email,
                PasswordHash = passwordHasher.Hash(registration.Password!),
                RequirePasswordReset = registration.RequirePasswordReset,
                Roles = registration.Roles.ToList(),
                Attributes = new Dictionary<string, string?>(registration.Attributes, StringComparer.OrdinalIgnoreCase),
                CreatedAt = now,
                UpdatedAt = now
            };

            await users.InsertOneAsync(document, cancellationToken: cancellationToken).ConfigureAwait(false);
            return AuthorityPluginOperationResult<AuthorityUserDescriptor>.Success(ToDescriptor(document));
        }

        existing.Username = registration.Username;
        existing.DisplayName = registration.DisplayName ?? existing.DisplayName;
        existing.Email = registration.Email ?? existing.Email;
        existing.Roles = registration.Roles.Any()
            ? registration.Roles.ToList()
            : existing.Roles;

        if (registration.Attributes.Count > 0)
        {
            foreach (var pair in registration.Attributes)
            {
                existing.Attributes[pair.Key] = pair.Value;
            }
        }

        if (!string.IsNullOrEmpty(registration.Password))
        {
            existing.PasswordHash = passwordHasher.Hash(registration.Password!);
            existing.RequirePasswordReset = registration.RequirePasswordReset;
        }
        else if (registration.RequirePasswordReset)
        {
            existing.RequirePasswordReset = true;
        }

        existing.UpdatedAt = now;

        await users.ReplaceOneAsync(
            Builders<StandardUserDocument>.Filter.Eq(u => u.Id, existing.Id),
            existing,
            cancellationToken: cancellationToken).ConfigureAwait(false);

        return AuthorityPluginOperationResult<AuthorityUserDescriptor>.Success(ToDescriptor(existing));
    }

    public async ValueTask<AuthorityUserDescriptor?> FindBySubjectAsync(string subjectId, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(subjectId))
        {
            return null;
        }

        var user = await users.Find(u => u.SubjectId == subjectId)
            .FirstOrDefaultAsync(cancellationToken)
            .ConfigureAwait(false);

        return user is null ? null : ToDescriptor(user);
    }

    public async Task EnsureBootstrapUserAsync(BootstrapUserOptions bootstrap, CancellationToken cancellationToken)
    {
        if (bootstrap is null || !bootstrap.IsConfigured)
        {
            return;
        }

        var registration = new AuthorityUserRegistration(
            bootstrap.Username!,
            bootstrap.Password,
            displayName: bootstrap.Username,
            email: null,
            requirePasswordReset: bootstrap.RequirePasswordReset,
            roles: Array.Empty<string>(),
            attributes: new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase));

        var result = await UpsertUserAsync(registration, cancellationToken).ConfigureAwait(false);
        if (!result.Succeeded)
        {
            logger.LogWarning(
                "Plugin {PluginName} failed to seed bootstrap user '{Username}': {Reason}",
                pluginName,
                bootstrap.Username,
                result.ErrorCode);
        }
    }

    public async Task<AuthorityPluginHealthResult> CheckHealthAsync(CancellationToken cancellationToken)
    {
        try
        {
            var command = new BsonDocument("ping", 1);
            await users.Database.RunCommandAsync<BsonDocument>(command, cancellationToken: cancellationToken).ConfigureAwait(false);
            return AuthorityPluginHealthResult.Healthy();
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Plugin {PluginName} failed MongoDB health check.", pluginName);
            return AuthorityPluginHealthResult.Unavailable(ex.Message);
        }
    }

    private string? ValidatePassword(string password)
    {
        if (password.Length < options.PasswordPolicy.MinimumLength)
        {
            return $"Password must be at least {options.PasswordPolicy.MinimumLength} characters long.";
        }

        if (options.PasswordPolicy.RequireUppercase && !password.Any(char.IsUpper))
        {
            return "Password must contain an uppercase letter.";
        }

        if (options.PasswordPolicy.RequireLowercase && !password.Any(char.IsLower))
        {
            return "Password must contain a lowercase letter.";
        }

        if (options.PasswordPolicy.RequireDigit && !password.Any(char.IsDigit))
        {
            return "Password must contain a digit.";
        }

        if (options.PasswordPolicy.RequireSymbol && password.All(char.IsLetterOrDigit))
        {
            return "Password must contain a symbol.";
        }

        return null;
    }

    private async Task RegisterFailureAsync(StandardUserDocument user, CancellationToken cancellationToken)
    {
        user.Lockout.LastFailure = DateTimeOffset.UtcNow;
        user.Lockout.FailedAttempts += 1;

        if (options.Lockout.Enabled && user.Lockout.FailedAttempts >= options.Lockout.MaxAttempts)
        {
            user.Lockout.LockoutEnd = DateTimeOffset.UtcNow + options.Lockout.Window;
            user.Lockout.FailedAttempts = 0;
        }

        await users.ReplaceOneAsync(
            Builders<StandardUserDocument>.Filter.Eq(u => u.Id, user.Id),
            user,
            cancellationToken: cancellationToken).ConfigureAwait(false);
    }

    private static void ResetLockout(StandardUserDocument user)
    {
        user.Lockout.FailedAttempts = 0;
        user.Lockout.LockoutEnd = null;
        user.Lockout.LastFailure = null;
    }

    private static string NormalizeUsername(string username)
        => username.Trim().ToLowerInvariant();

    private AuthorityUserDescriptor ToDescriptor(StandardUserDocument document)
        => new(
            document.SubjectId,
            document.Username,
            document.DisplayName,
            document.RequirePasswordReset,
            document.Roles,
            document.Attributes);

    private void EnsureIndexes()
    {
        var indexKeys = Builders<StandardUserDocument>.IndexKeys
            .Ascending(u => u.NormalizedUsername);

        var indexModel = new CreateIndexModel<StandardUserDocument>(
            indexKeys,
            new CreateIndexOptions { Unique = true, Name = "idx_normalized_username" });

        try
        {
            users.Indexes.CreateOne(indexModel);
        }
        catch (MongoCommandException ex) when (ex.CodeName.Equals("IndexOptionsConflict", StringComparison.OrdinalIgnoreCase))
        {
            logger.LogDebug("Plugin {PluginName} skipped index creation due to existing index.", pluginName);
        }
    }
}
