using Meezi.Admin.API.Models;
using Meezi.Core.Constants;
using Meezi.Core.Enums;
using Meezi.Core.Interfaces;
using Meezi.Core.Utilities;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;

namespace Meezi.Admin.API.Services;

public interface IAdminAuthService
{
    Task<(bool Success, SendOtpResponse? Data, string? ErrorCode, string? ErrorMessage)> SendOtpAsync(
        SendOtpRequest request,
        CancellationToken cancellationToken = default);

    Task<(bool Success, AuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> VerifyOtpAsync(
        VerifyOtpRequest request,
        CancellationToken cancellationToken = default);

    Task<(bool Success, AuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> LoginWithPasswordAsync(
        LoginWithPasswordRequest request,
        CancellationToken cancellationToken = default);

    Task<(bool Success, string? ErrorCode, string? ErrorMessage)> ChangePasswordAsync(
        string adminId,
        ChangePasswordRequest request,
        CancellationToken cancellationToken = default);

    Task<(bool Success, AuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> RefreshAsync(
        RefreshTokenRequest request,
        CancellationToken cancellationToken = default);
}

public class AdminAuthService : IAdminAuthService
{
    private const int OtpTtlSeconds = 300;
    private const int DefaultMaxOtpAttemptsPerHour = 5;

    private readonly AppDbContext _db;
    private readonly IConnectionMultiplexer _redis;
    private readonly ISmsService _smsService;
    private readonly IAdminJwtTokenService _jwtTokenService;
    private readonly IRefreshTokenStore _refreshTokenStore;
    private readonly IConfiguration _configuration;
    private readonly ILogger<AdminAuthService> _logger;

    public AdminAuthService(
        AppDbContext db,
        IConnectionMultiplexer redis,
        ISmsService smsService,
        IAdminJwtTokenService jwtTokenService,
        IRefreshTokenStore refreshTokenStore,
        IConfiguration configuration,
        ILogger<AdminAuthService> logger)
    {
        _db = db;
        _redis = redis;
        _smsService = smsService;
        _jwtTokenService = jwtTokenService;
        _refreshTokenStore = refreshTokenStore;
        _configuration = configuration;
        _logger = logger;
    }

    public async Task<(bool Success, SendOtpResponse? Data, string? ErrorCode, string? ErrorMessage)> SendOtpAsync(
        SendOtpRequest request,
        CancellationToken cancellationToken = default)
    {
        var phone = PhoneNormalizer.Normalize(request.Phone);
        var admin = await _db.SystemAdmins
            .FirstOrDefaultAsync(a => a.Phone == phone && a.IsActive && a.DeletedAt == null, cancellationToken);

        if (admin is null)
            return (false, null, "NOT_FOUND", "No system admin account for this phone.");

        var redis = _redis.GetDatabase();
        var maxAttempts = _configuration.GetValue("Auth:MaxOtpAttemptsPerHour", DefaultMaxOtpAttemptsPerHour);
        var attemptsKey = $"otp:admin:{phone}";
        if (maxAttempts > 0)
        {
            var attempts = await redis.StringGetAsync(attemptsKey);
            if (attempts.HasValue && (int)attempts >= maxAttempts)
                return (false, null, "RATE_LIMITED", "Too many OTP requests. Try again later.");
        }

        var otp = Random.Shared.Next(100000, 999999).ToString();
        await redis.StringSetAsync($"otp:admin:{phone}", otp, TimeSpan.FromSeconds(OtpTtlSeconds));

        try
        {
            await _smsService.SendOtpAsync(phone, otp, cancellationToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to send admin OTP SMS");
            return (false, null, "SMS_FAILED", "Could not send verification code.");
        }

        if (maxAttempts > 0)
        {
            var newAttempts = await redis.StringIncrementAsync(attemptsKey);
            if (newAttempts == 1)
                await redis.KeyExpireAsync(attemptsKey, TimeSpan.FromHours(1));
        }

        return (true, new SendOtpResponse(true, OtpTtlSeconds), null, null);
    }

    public async Task<(bool Success, AuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> VerifyOtpAsync(
        VerifyOtpRequest request,
        CancellationToken cancellationToken = default)
    {
        var phone = PhoneNormalizer.Normalize(request.Phone);
        var code = OtpNormalizer.Normalize(request.Code);
        if (!OtpNormalizer.IsValidSixDigitCode(code))
            return (false, null, "INVALID_OTP", "Invalid or expired verification code.");

        var redis = _redis.GetDatabase();
        var storedOtp = await redis.StringGetAsync($"otp:admin:{phone}");
        if (storedOtp.IsNullOrEmpty || storedOtp.ToString() != code)
            return (false, null, "INVALID_OTP", "Invalid or expired verification code.");

        var admin = await _db.SystemAdmins
            .FirstOrDefaultAsync(a => a.Phone == phone && a.IsActive && a.DeletedAt == null, cancellationToken);
        if (admin is null)
            return (false, null, "NOT_FOUND", "No system admin account for this phone.");

        await redis.KeyDeleteAsync($"otp:admin:{phone}");
        var tokens = await IssueTokensAsync(admin, cancellationToken);
        return (true, tokens, null, null);
    }

    public async Task<(bool Success, AuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> RefreshAsync(
        RefreshTokenRequest request,
        CancellationToken cancellationToken = default)
    {
        var payload = await _refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken);
        if (payload is null || payload.Actor != MeeziActorKinds.SystemAdmin)
            return (false, null, "INVALID_TOKEN", "Refresh token is invalid or expired.");

        var admin = await _db.SystemAdmins
            .FirstOrDefaultAsync(a => a.Id == payload.UserId && a.IsActive && a.DeletedAt == null, cancellationToken);
        if (admin is null)
            return (false, null, "NOT_FOUND", "Admin no longer exists.");

        await _refreshTokenStore.RevokeAsync(request.RefreshToken, cancellationToken);
        var tokens = await IssueTokensAsync(admin, cancellationToken);
        return (true, tokens, null, null);
    }

    public async Task<(bool Success, AuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> LoginWithPasswordAsync(
        LoginWithPasswordRequest request,
        CancellationToken cancellationToken = default)
    {
        var username = request.Username.Trim();
        var admin = await _db.SystemAdmins
            .FirstOrDefaultAsync(a => a.Username == username && a.IsActive && a.DeletedAt == null, cancellationToken);

        if (admin is null || string.IsNullOrWhiteSpace(admin.PasswordHash))
            return (false, null, "INVALID_CREDENTIALS", "Invalid username or password.");

        if (!PasswordHasher.Verify(request.Password, admin.PasswordHash))
            return (false, null, "INVALID_CREDENTIALS", "Invalid username or password.");

        var tokens = await IssueTokensAsync(admin, cancellationToken);
        return (true, tokens, null, null);
    }

    public async Task<(bool Success, string? ErrorCode, string? ErrorMessage)> ChangePasswordAsync(
        string adminId,
        ChangePasswordRequest request,
        CancellationToken cancellationToken = default)
    {
        var admin = await _db.SystemAdmins
            .FirstOrDefaultAsync(a => a.Id == adminId && a.IsActive && a.DeletedAt == null, cancellationToken);
        if (admin is null)
            return (false, "NOT_FOUND", "Admin not found.");

        // If a password is already set, require the current one
        if (!string.IsNullOrWhiteSpace(admin.PasswordHash))
        {
            if (!PasswordHasher.Verify(request.CurrentPassword, admin.PasswordHash))
                return (false, "INVALID_CREDENTIALS", "Current password is incorrect.");
        }

        if (string.IsNullOrWhiteSpace(request.NewPassword) || request.NewPassword.Length < 8)
            return (false, "VALIDATION_ERROR", "New password must be at least 8 characters.");

        admin.PasswordHash = PasswordHasher.Hash(request.NewPassword);
        await _db.SaveChangesAsync(cancellationToken);
        return (true, null, null);
    }

    private async Task<AuthTokenResponse> IssueTokensAsync(
        Core.Entities.SystemAdmin admin,
        CancellationToken cancellationToken)
    {
        var accessToken = _jwtTokenService.CreateAdminAccessToken(admin);
        var refreshToken = _jwtTokenService.CreateRefreshToken();
        var refreshDays = _configuration.GetValue("Jwt:RefreshTokenExpiryDays", 30);

        await _refreshTokenStore.StoreAsync(
            refreshToken,
            new RefreshTokenPayload(
                admin.Id,
                string.Empty,
                "SystemAdmin",
                PlanTier.Enterprise.ToString(),
                "fa",
                MeeziActorKinds.SystemAdmin),
            TimeSpan.FromDays(refreshDays),
            cancellationToken);

        return new AuthTokenResponse(
            accessToken,
            refreshToken,
            _jwtTokenService.GetAccessTokenExpiry(),
            admin.Id,
            string.Empty,
            "SystemAdmin",
            PlanTier.Enterprise.ToString(),
            "fa",
            MeeziActorKinds.SystemAdmin);
    }
}
