using System.Security.Claims;
using Microsoft.Extensions.Caching.Memory;
using MongoDB.Driver;
using Nalu.Web.Data;
using Nalu.Web.Data.Models;

namespace Nalu.Web.Services;

public record CreditConsumeResult
{
    public bool Success { get; init; }
    public int CreditsConsumed { get; init; }
    public int CreditsUsed { get; init; }
    public int CreditsLimit { get; init; }
    public DateTime ResetAt { get; init; }
    public object? ErrorPayload { get; init; }

    public int CreditsRemaining => Math.Max(0, CreditsLimit - CreditsUsed);
}

public class CreditService(MongoDbContext db, IConfiguration config, IMemoryCache cache)
{
    public async Task<CreditConsumeResult> TryConsumeAsync(
        ClaimsPrincipal user, string validatorId, HttpContext ctx, CancellationToken ct = default)
    {
        var apiKey = user.FindFirst("api_key")?.Value ?? "";
        var plan   = user.FindFirst("plan")?.Value ?? "free";
        var cost   = CreditCosts.Get(validatorId);

        var monthlyLimit = GetPlanLimit(plan, "credits_per_month");
        var dailyLimit   = GetPlanLimit(plan, "credits_per_day");
        var now          = DateTime.UtcNow;
        var yearMonth    = now.ToString("yyyy-MM");
        var today        = now.ToString("yyyy-MM-dd");
        var resetAt      = new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc).AddMonths(1);
        var dailyResetAt = now.Date.AddDays(1);
        var ip           = ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown";

        // ── Per-IP rate limit (all plans) ────────────────────────────────────
        var ipRateResult = CheckIpRateLimit(ip, cost);
        if (!ipRateResult.allowed)
        {
            return new CreditConsumeResult
            {
                Success = false, CreditsConsumed = 0,
                CreditsUsed = 0, CreditsLimit = monthlyLimit, ResetAt = resetAt,
                ErrorPayload = new
                {
                    error   = "rate_limit_exceeded",
                    message = ipRateResult.isHour
                        ? "Muitas requisições por hora deste IP. Aguarde antes de tentar novamente."
                        : "Muitas requisições por minuto deste IP. Aguarde alguns segundos.",
                    retry_after_seconds = ipRateResult.isHour ? 3600 : 60,
                }
            };
        }

        // ── Shared-IP detection (free plan only) ─────────────────────────────
        if (plan == "free" && !string.IsNullOrEmpty(apiKey))
            TrackIpKeyUsage(ip, apiKey, today);

        // ── Daily cap (free plan) ─────────────────────────────────────────────
        if (dailyLimit > 0)
        {
            var dailyKey  = $"daily:{apiKey}:{today}";
            var dailyUsed = cache.GetOrCreate(dailyKey, e =>
            {
                e.AbsoluteExpiration = dailyResetAt;
                return 0;
            });

            if (dailyUsed + cost > dailyLimit)
            {
                return new CreditConsumeResult
                {
                    Success = false, CreditsConsumed = 0,
                    CreditsUsed = dailyUsed, CreditsLimit = dailyLimit, ResetAt = dailyResetAt,
                    ErrorPayload = Build429Body(plan, dailyUsed, dailyLimit, dailyResetAt,
                        isDailyLimit: true)
                };
            }

            cache.Set(dailyKey, dailyUsed + cost, new MemoryCacheEntryOptions
            {
                AbsoluteExpiration = dailyResetAt
            });
        }

        if (!db.IsConnected)
        {
            return new CreditConsumeResult
            {
                Success = true, CreditsConsumed = cost, CreditsUsed = cost,
                CreditsLimit = monthlyLimit, ResetAt = resetAt
            };
        }

        // ── Monthly limit ─────────────────────────────────────────────────────
        var current = await db.UsageMonthly
            .Find(u => u.ApiKey == apiKey && u.YearMonth == yearMonth)
            .FirstOrDefaultAsync(ct);

        var currentTotal = current?.TotalCreditsUsed ?? 0;

        if (monthlyLimit > 0 && currentTotal + cost > monthlyLimit)
        {
            return new CreditConsumeResult
            {
                Success = false, CreditsConsumed = 0,
                CreditsUsed = currentTotal, CreditsLimit = monthlyLimit, ResetAt = resetAt,
                ErrorPayload = Build429Body(plan, currentTotal, monthlyLimit, resetAt)
            };
        }

        // ── Atomic increment ──────────────────────────────────────────────────
        var filter = Builders<UsageMonthly>.Filter.And(
            Builders<UsageMonthly>.Filter.Eq(u => u.ApiKey, apiKey),
            Builders<UsageMonthly>.Filter.Eq(u => u.YearMonth, yearMonth));

        var update = Builders<UsageMonthly>.Update
            .Inc(u => u.TotalCreditsUsed, cost)
            .Inc(u => u.TotalRequests, 1)
            .Inc($"credits_by_validator.{validatorId}", cost)
            .Inc($"requests_by_validator.{validatorId}", 1)
            .Set(u => u.UpdatedAt, now)
            .SetOnInsert(u => u.Plan, plan)
            .SetOnInsert(u => u.ApiKey, apiKey)
            .SetOnInsert(u => u.YearMonth, yearMonth);

        var opts = new FindOneAndUpdateOptions<UsageMonthly>
        {
            IsUpsert = true,
            ReturnDocument = ReturnDocument.After
        };

        var after = await db.UsageMonthly.FindOneAndUpdateAsync(filter, update, opts, ct);

        return new CreditConsumeResult
        {
            Success = true, CreditsConsumed = cost,
            CreditsUsed = after.TotalCreditsUsed,
            CreditsLimit = monthlyLimit, ResetAt = resetAt
        };
    }

    public void ApplyHeaders(HttpContext ctx, CreditConsumeResult result)
    {
        ctx.Response.Headers["X-Credits-Used"]      = result.CreditsConsumed.ToString();
        ctx.Response.Headers["X-Credits-Remaining"] = result.CreditsRemaining.ToString();
        ctx.Response.Headers["X-Credits-Limit"]     = result.CreditsLimit.ToString();
        ctx.Response.Headers["X-Credits-Reset"]     = result.ResetAt.ToString("O");
    }

    // ── Per-IP rate limiting ──────────────────────────────────────────────────

    private (bool allowed, bool isHour) CheckIpRateLimit(string ip, int cost)
    {
        var perMin  = config.GetValue<int>("RateLimit:PerIpPerMinute", 60);
        var perHour = config.GetValue<int>("RateLimit:PerIpPerHour", 500);
        var now     = DateTime.UtcNow;

        var minKey  = $"rl:min:{ip}:{now:yyyyMMddHHmm}";
        var hourKey = $"rl:hr:{ip}:{now:yyyyMMddHH}";

        var minUsed  = cache.GetOrCreate(minKey,  e => { e.AbsoluteExpiration = now.AddMinutes(1);  return 0; });
        var hourUsed = cache.GetOrCreate(hourKey, e => { e.AbsoluteExpiration = now.AddHours(1);    return 0; });

        if (perHour > 0 && hourUsed + cost > perHour)
            return (false, isHour: true);

        if (perMin > 0 && minUsed + cost > perMin)
            return (false, isHour: false);

        cache.Set(minKey,  minUsed  + cost, new MemoryCacheEntryOptions { AbsoluteExpiration = now.AddMinutes(1) });
        cache.Set(hourKey, hourUsed + cost, new MemoryCacheEntryOptions { AbsoluteExpiration = now.AddHours(1) });

        return (true, isHour: false);
    }

    // ── Shared-IP detection (free plan abuse) ─────────────────────────────────
    // Tracks distinct free keys seen from each IP per day.
    // >3 distinct keys from same IP = likely key pooling.

    private void TrackIpKeyUsage(string ip, string apiKey, string today)
    {
        var trackKey = $"ipkeys:{ip}:{today}";
        var keys = cache.GetOrCreate(trackKey, e =>
        {
            e.AbsoluteExpiration = DateTime.UtcNow.Date.AddDays(1);
            return new HashSet<string>();
        })!;

        lock (keys)
        {
            keys.Add(apiKey);

            if (keys.Count >= 4)
            {
                // Throttle ALL keys from this IP for the rest of the day
                var throttleKey = $"ipthrottle:{ip}:{today}";
                cache.Set(throttleKey, true, new MemoryCacheEntryOptions
                {
                    AbsoluteExpiration = DateTime.UtcNow.Date.AddDays(1)
                });
            }
        }
    }

    public bool IsIpThrottled(string ip)
    {
        var today = DateTime.UtcNow.ToString("yyyy-MM-dd");
        return cache.TryGetValue($"ipthrottle:{ip}:{today}", out _);
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    private int GetPlanLimit(string plan, string key)
    {
        var v = config.GetValue<int?>($"Plans:{plan}:{key}");
        return v ?? 0; // 0 = unlimited
    }

    private static object Build429Body(string plan, int used, int limit, DateTime resetAt,
        bool isDailyLimit = false) => new
    {
        error   = isDailyLimit ? "daily_limit_exceeded" : "credits_exhausted",
        message = isDailyLimit
            ? $"Limite diário de {limit} créditos atingido no plano gratuito. Renova amanhã ou faça upgrade."
            : $"Seus créditos do mês acabaram. Seu plano ({Capitalize(plan)}) permite {limit:N0} créditos/mês.",
        credits_used  = used,
        credits_limit = limit,
        reset_at      = resetAt.ToString("O"),
        upgrade_url   = "https://naluai.dev/precos",
        hint          = isDailyLimit
            ? "Plano Starter: 15.000 créditos/mês sem limite diário. R$ 29/mês."
            : "Upgrade para Starter por apenas R$ 0,0019 por validação.",
    };

    private static string Capitalize(string s) =>
        s.Length == 0 ? s : char.ToUpperInvariant(s[0]) + s[1..];
}
