using Meezi.API.Models.Auth;
using Meezi.API.Models.Consumer;
using Meezi.API.Security;
using Meezi.Core.Constants;
using Meezi.Core.Entities;
using Meezi.Core.Interfaces;
using Meezi.Core.Utilities;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;

namespace Meezi.API.Services;

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

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

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

public class ConsumerAuthService : IConsumerAuthService
{
    private const int OtpTtlSeconds = 300;
    private const int DefaultMaxOtpAttemptsPerHour = 5;

    private readonly AppDbContext _db;
    private readonly IConnectionMultiplexer _redis;
    private readonly ISmsService _smsService;
    private readonly IJwtTokenService _jwtTokenService;
    private readonly IRefreshTokenStore _refreshTokenStore;
    private readonly IConfiguration _configuration;
    private readonly ILogger<ConsumerAuthService> _logger;
    private readonly IAbuseProtectionService _abuse;
    private readonly IHttpContextAccessor _http;

    public ConsumerAuthService(
        AppDbContext db,
        IConnectionMultiplexer redis,
        ISmsService smsService,
        IJwtTokenService jwtTokenService,
        IRefreshTokenStore refreshTokenStore,
        IConfiguration configuration,
        IAbuseProtectionService abuse,
        IHttpContextAccessor http,
        ILogger<ConsumerAuthService> logger)
    {
        _db = db;
        _redis = redis;
        _smsService = smsService;
        _jwtTokenService = jwtTokenService;
        _refreshTokenStore = refreshTokenStore;
        _configuration = configuration;
        _abuse = abuse;
        _http = http;
        _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 redis = _redis.GetDatabase();
        var maxAttempts = _configuration.GetValue("Auth:MaxOtpAttemptsPerHour", DefaultMaxOtpAttemptsPerHour);

        if (_http.HttpContext is not null)
        {
            var ip = ClientIpResolver.GetClientIp(_http.HttpContext);
            var ipCheck = await _abuse.CheckAuthOtpByIpAsync(ip, cancellationToken);
            if (!ipCheck.Allowed)
                return (false, null, ipCheck.ErrorCode, ipCheck.Message);
        }

        var attemptsKey = $"consumer-otp:attempts:{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($"consumer-otp:{phone}", otp, TimeSpan.FromSeconds(OtpTtlSeconds));

        try
        {
            await _smsService.SendOtpAsync(phone, otp, cancellationToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to send consumer 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, ConsumerAuthTokenResponse? 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($"consumer-otp:{phone}");
        if (storedOtp.IsNullOrEmpty || storedOtp.ToString() != code)
            return (false, null, "INVALID_OTP", "Invalid or expired verification code.");

        var account = await _db.ConsumerAccounts
            .FirstOrDefaultAsync(a => a.Phone == phone, cancellationToken);

        if (account is null)
        {
            account = new ConsumerAccount { Phone = phone };
            _db.ConsumerAccounts.Add(account);
            await _db.SaveChangesAsync(cancellationToken);
        }

        await redis.KeyDeleteAsync($"consumer-otp:{phone}");

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

    public async Task<(bool Success, ConsumerAuthTokenResponse? 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.Consumer)
            return (false, null, "INVALID_TOKEN", "Refresh token is invalid or expired.");

        var account = await _db.ConsumerAccounts
            .FirstOrDefaultAsync(a => a.Id == payload.UserId, cancellationToken);
        if (account is null)
            return (false, null, "NOT_FOUND", "Account no longer exists.");

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

    private async Task<ConsumerAuthTokenResponse> IssueTokensAsync(
        ConsumerAccount account,
        CancellationToken cancellationToken)
    {
        var lang = "fa";
        var accessToken = _jwtTokenService.CreateConsumerAccessToken(account, lang);
        var refreshToken = _jwtTokenService.CreateRefreshToken();
        var refreshDays = _configuration.GetValue("Jwt:RefreshTokenExpiryDays", 30);

        await _refreshTokenStore.StoreAsync(
            refreshToken,
            new RefreshTokenPayload(
                account.Id,
                string.Empty,
                MeeziRoles.Customer,
                string.Empty,
                lang,
                MeeziActorKinds.Consumer),
            TimeSpan.FromDays(refreshDays),
            cancellationToken);

        return new ConsumerAuthTokenResponse(
            accessToken,
            refreshToken,
            _jwtTokenService.GetAccessTokenExpiry(),
            account.Id,
            account.Phone,
            lang);
    }
}
