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

namespace Meezi.API.Services;

public class AuthService : IAuthService
{
    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<AuthService> _logger;
    private readonly IAbuseProtectionService _abuse;
    private readonly IHttpContextAccessor _http;

    public AuthService(
        AppDbContext db,
        IConnectionMultiplexer redis,
        ISmsService smsService,
        IJwtTokenService jwtTokenService,
        IRefreshTokenStore refreshTokenStore,
        IConfiguration configuration,
        IAbuseProtectionService abuse,
        IHttpContextAccessor http,
        ILogger<AuthService> 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 employeeExists = await _db.Employees
            .AnyAsync(e => e.Phone == phone && e.DeletedAt == null, cancellationToken);

        if (!employeeExists)
            return (false, null, "NOT_FOUND", "No account found for this phone number.");

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

        if (string.IsNullOrWhiteSpace(_configuration["Kavenegar:ApiKey"]))
            _logger.LogWarning("DEV OTP for {Phone}: {Otp} (configure Kavenegar:ApiKey to send SMS)", phone, otp);

        try
        {
            await _smsService.SendOtpAsync(phone, otp, cancellationToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to send 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));
        }

        _logger.LogInformation("OTP sent for phone ending {Suffix}", phone[^4..]);

        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:{phone}");
        if (storedOtp.IsNullOrEmpty || storedOtp.ToString() != code)
            return (false, null, "INVALID_OTP", "Invalid or expired verification code.");

        var query = _db.Employees
            .Include(e => e.Cafe)
            .Where(e => e.Phone == phone && e.DeletedAt == null);

        if (!string.IsNullOrWhiteSpace(request.CafeId))
            query = query.Where(e => e.CafeId == request.CafeId);

        var matches = await query.ToListAsync(cancellationToken);
        if (matches.Count == 0)
            return (false, null, "NOT_FOUND", "No account found for this phone number.");
        if (matches.Count > 1)
            return (false, null, "MULTIPLE_ACCOUNTS", "Multiple accounts use this phone. Contact your cafe owner.");

        var employee = matches[0];
        if (employee.Cafe is null)
            return (false, null, "NOT_FOUND", "No account found for this phone number.");

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

        var tokens = await IssueTokensAsync(employee, employee.Cafe, 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 == Meezi.Core.Constants.MeeziActorKinds.SystemAdmin)
            return (false, null, "INVALID_TOKEN", "Refresh token is invalid or expired.");

        var employee = await _db.Employees
            .Include(e => e.Cafe)
            .FirstOrDefaultAsync(e => e.Id == payload.UserId && e.CafeId == payload.CafeId && e.DeletedAt == null, cancellationToken);

        if (employee?.Cafe is null)
            return (false, null, "NOT_FOUND", "User no longer exists.");

        await _refreshTokenStore.RevokeAsync(request.RefreshToken, cancellationToken);

        var tokens = await IssueTokensAsync(employee, employee.Cafe, cancellationToken);
        return (true, tokens, null, null);
    }

    private async Task<AuthTokenResponse> IssueTokensAsync(
        Core.Entities.Employee employee,
        Core.Entities.Cafe cafe,
        CancellationToken cancellationToken)
    {
        var accessToken = _jwtTokenService.CreateAccessToken(employee, cafe);
        var refreshToken = _jwtTokenService.CreateRefreshToken();
        var refreshDays = _configuration.GetValue("Jwt:RefreshTokenExpiryDays", 30);

        await _refreshTokenStore.StoreAsync(
            refreshToken,
            new RefreshTokenPayload(
                employee.Id,
                cafe.Id,
                employee.Role.ToString(),
                cafe.PlanTier.ToString(),
                cafe.PreferredLanguage,
                Meezi.Core.Constants.MeeziActorKinds.Merchant),
            TimeSpan.FromDays(refreshDays),
            cancellationToken);

        return new AuthTokenResponse(
            accessToken,
            refreshToken,
            _jwtTokenService.GetAccessTokenExpiry(),
            employee.Id,
            cafe.Id,
            employee.Role.ToString(),
            cafe.PlanTier.ToString(),
            cafe.PreferredLanguage,
            Meezi.Core.Constants.MeeziActorKinds.Merchant,
            employee.BranchId);
    }
}
