using Application.Abstractions.Data;
using Domain.Entities.Common;
using Domain.Entities.Common.ValueObject;
using Domain.Entities.Members;
using Domain.Entities.Members.Logs;
using Domain.Entities.Members.ValueObject;
using Microsoft.EntityFrameworkCore;

namespace Application.Helpers;

/// <summary>
/// 보상 엔진 — 흩어진 보상 지급(글/댓글/추천)을 단일 지급 관문으로 통합한다.
/// 액션별 일일 캡(횟수/경험치/토큰)을 MemberDailyRewardCounter 로 강제하고,
/// XP 지급 후 다음 등급 RequiredExp 를 넘으면 인라인으로 자동 승급(래칫, 강등 없음)한다.
///
/// 계약: <b>호출자가 SaveChangesAsync</b> 를 수행한다 (작성/전송이 주된 결과, 지급은 부수효과 — 단일 커밋 유지).
/// 멱등: refID 로 이미 지급된 액션이면 재지급하지 않는다 (토큰=WalletTransaction, XP전용=MemberExpLog Reason 마커).
/// 설정은 db.Config.Reward 를 직접 읽는다 (캐시 Response 경유 없음 — Config.Get/Response 는 편집 대상 아님).
/// </summary>
public static class RewardService
{
    private static readonly TimeZoneInfo KstZone = ResolveKst();

    // ---- refID 헬퍼 (멱등 키) ----
    public static string PostRefIDFor(int postID) => $"reward-post:{postID}";
    public static string CommentRefIDFor(int commentID) => $"reward-comment:{commentID}";
    public static string LikeGivenRefIDFor(int postID, int giverID) => $"reward-like-given:{postID}:{giverID}";
    public static string LikeReceivedRefIDFor(int postID, int giverID) => $"reward-like-received:{postID}:{giverID}";

    /// <summary>게시글 작성 보상 (PostExp + PostPoint). MinPostLength·NewMemberHoldDays·일일 캡 적용.</summary>
    public static async Task GrantForPostAsync(IAppDbContext db, int memberID, int postID, int contentLength, CancellationToken ct)
    {
        var config = await LoadConfigAsync(db, ct);
        if (config is null || (config.PostExp <= 0 && config.PostPoint <= 0))
        {
            return;
        }

        if (config.MinPostLength > 0 && contentLength < config.MinPostLength)
        {
            return;
        }

        await GrantAsync(db, memberID, RewardActionType.Post, config.PostExp, config.PostPoint, PostRefIDFor(postID), "게시글 작성 보상", config, ct);
    }

    /// <summary>댓글 작성 보상 (CommentExp + CommentPoint). MinCommentLength·NewMemberHoldDays·일일 캡 적용.</summary>
    public static async Task GrantForCommentAsync(IAppDbContext db, int memberID, int commentID, int contentLength, CancellationToken ct)
    {
        var config = await LoadConfigAsync(db, ct);
        if (config is null || (config.CommentExp <= 0 && config.CommentPoint <= 0))
        {
            return;
        }

        if (config.MinCommentLength > 0 && contentLength < config.MinCommentLength)
        {
            return;
        }

        await GrantAsync(db, memberID, RewardActionType.Comment, config.CommentExp, config.CommentPoint, CommentRefIDFor(commentID), "댓글 작성 보상", config, ct);
    }

    /// <summary>추천(좋아요) 보상 — 누른 회원(LikeGivenExp) + 받은 작성자(LikeReceivedPoint). 본인 글 추천은 지급 없음.</summary>
    public static async Task GrantForLikeAsync(IAppDbContext db, int giverID, int? authorID, int postID, CancellationToken ct)
    {
        var config = await LoadConfigAsync(db, ct);
        if (config is null)
        {
            return;
        }

        // 누른 쪽 XP
        if (config.LikeGivenExp > 0 && giverID > 0)
        {
            await GrantAsync(db, giverID, RewardActionType.LikeGiven, config.LikeGivenExp, 0, LikeGivenRefIDFor(postID, giverID), "추천 보상", config, ct);
        }

        // 받은 쪽 토큰 — 본인 글 자추천 제외
        if (config.LikeReceivedPoint > 0 && authorID is int author && author > 0 && author != giverID)
        {
            await GrantAsync(db, author, RewardActionType.LikeReceived, 0, config.LikeReceivedPoint, LikeReceivedRefIDFor(postID, giverID), "추천 받음 보상", config, ct);
        }
    }

    /// <summary>
    /// 단일 지급 관문. 일일 캡으로 exp/point 를 clamp 하고, 멱등(refID) 판정 후 지급한다.
    /// XP → MemberStats.Exp(+MemberExpLog, 자동 승급), Point → Wallet.CreditReward.
    /// 캡/멱등으로 실제 지급분이 0 이면 카운터도 원장도 남기지 않는다.
    /// </summary>
    public static async Task GrantAsync(
        IAppDbContext db,
        int memberID,
        RewardActionType action,
        int exp,
        int point,
        string refID,
        string reason,
        RewardConfig config,
        CancellationToken ct)
    {
        if (memberID <= 0 || (exp <= 0 && point <= 0))
        {
            return;
        }

        // 신규 회원 보상 유예
        if (config.NewMemberHoldDays > 0)
        {
            var createdAt = await db.Member.AsNoTracking().Where(c => c.ID == memberID).Select(c => (DateTime?)c.CreatedAt).FirstOrDefaultAsync(ct);
            if (createdAt is null)
            {
                return;
            }

            if (createdAt.Value.AddDays(config.NewMemberHoldDays) > DateTime.UtcNow)
            {
                return;
            }
        }

        // 멱등: 동일 refID 로 이미 지급됐는지 확인
        if (await AlreadyGrantedAsync(db, memberID, refID, ct))
        {
            return;
        }

        var today = TodayKst();

        // 오늘 이 액션의 누적치 로드 (캡 clamp 기준)
        var counter = await db.MemberDailyRewardCounter
            .FirstOrDefaultAsync(c => c.MemberID == memberID && c.RewardDate == today && c.ActionType == action, ct);

        var caps = ResolveCaps(config, action);

        var grantExp = ClampByCap(exp, counter?.ExpTotal ?? 0, caps.ExpCap);
        var grantPoint = ClampByCap(point, counter?.PointTotal ?? 0, caps.PointCap);

        // 횟수 캡 도달 시 이번 지급은 스킵
        if (caps.CountCap > 0 && (counter?.Count ?? 0) >= caps.CountCap)
        {
            return;
        }

        if (grantExp <= 0 && grantPoint <= 0)
        {
            return;
        }

        // 카운터 UPSERT (신규면 추가, 기존이면 누적)
        if (counter is null)
        {
            counter = MemberDailyRewardCounter.Create(memberID, today, action);
            counter.Add(grantExp, grantPoint);
            db.MemberDailyRewardCounter.Add(counter);
        }
        else
        {
            counter.Add(grantExp, grantPoint);
        }

        // XP 지급 (+ 자동 승급)
        if (grantExp > 0)
        {
            var stats = await db.MemberStats.FirstOrDefaultAsync(c => c.MemberID == memberID, ct);
            if (stats is not null)
            {
                stats.Exp += grantExp;
                // XP 전용(토큰 미지급) 지급의 멱등 판정을 위해 Reason 에 refID 마커를 붙인다.
                db.MemberExpLog.Add(MemberExpLog.Create(memberID, BuildExpReason(reason, refID), grantExp, stats.Exp));

                await TryPromoteGradeAsync(db, memberID, stats.Exp, ct);
            }
        }

        // 토큰 지급 (Wallet Reward 파티션)
        if (grantPoint > 0)
        {
            var wallet = await db.Wallet.Include(c => c.Balances).FirstOrDefaultAsync(c => c.MemberID == memberID, ct);
            wallet?.CreditReward(Money.KRW(grantPoint), reason, refID);
        }
    }

    /// <summary>
    /// 자동 승급(인라인). 현재 등급보다 상위이면서 RequiredExp 를 충족하는 활성 등급 중 최고 등급으로 승급한다.
    /// 래칫 — 강등 없음. 캐시 무효화(멤버 표시정보)는 상위에서 관리.
    /// </summary>
    private static async Task TryPromoteGradeAsync(IAppDbContext db, int memberID, long currentExp, CancellationToken ct)
    {
        var member = await db.Member.FirstOrDefaultAsync(c => c.ID == memberID, ct);
        if (member is null)
        {
            return;
        }

        // 활성 등급을 RequiredExp 오름차순으로 로드 (소량 — 캐시 불필요)
        var grades = await db.MemberGrade.AsNoTracking()
            .Where(c => c.IsActive)
            .OrderBy(c => c.RequiredExp)
            .Select(c => new { c.ID, c.KorName, c.RequiredExp, c.Order })
            .ToListAsync(ct);

        if (grades.Count == 0)
        {
            return;
        }

        // 충족하는 최고 등급 (RequiredExp <= currentExp 중 마지막)
        var eligible = grades.LastOrDefault(c => c.RequiredExp <= currentExp);
        if (eligible is null)
        {
            return;
        }

        // 현재 등급 Order 확인 — 래칫(하향 금지)
        var currentOrder = member.MemberGradeID is int gid
            ? grades.FirstOrDefault(c => c.ID == gid)?.Order
            : null;

        var eligibleGrade = grades.First(c => c.ID == eligible.ID);

        // 이미 해당 등급이면 승급 없음
        if (member.MemberGradeID == eligible.ID)
        {
            return;
        }

        if (currentOrder is short order && order >= eligibleGrade.Order)
        {
            return;
        }

        member.SetMemberGrade(eligible.ID);
    }

    private static async Task<bool> AlreadyGrantedAsync(IAppDbContext db, int memberID, string refID, CancellationToken ct)
    {
        // 토큰을 지급하는 액션은 원장(WalletTransaction.RefID)이 존재하면 이미 지급된 것.
        var byLedger = await db.WalletTransaction.AsNoTracking()
            .AnyAsync(c => c.RefID == refID && c.TxType == Domain.Entities.Wallets.ValueObject.WalletTransactionType.RewardEarned, ct);

        if (byLedger)
        {
            return true;
        }

        // XP 전용(토큰 미지급) 액션은 MemberExpLog Reason 마커로 판정.
        var marker = RefMarker(refID);
        return await db.MemberExpLog.AsNoTracking().AnyAsync(c => c.MemberID == memberID && c.Reason.EndsWith(marker), ct);
    }

    private static (int CountCap, int ExpCap, int PointCap) ResolveCaps(RewardConfig c, RewardActionType action) => action switch
    {
        RewardActionType.Post => (c.PostDailyCount, c.PostDailyExp, c.PostDailyPoint),
        RewardActionType.Comment => (c.CommentDailyCount, c.CommentDailyExp, c.CommentDailyPoint),
        RewardActionType.LikeGiven => (c.LikeGivenDailyCount, c.LikeGivenDailyExp, 0),
        RewardActionType.LikeReceived => (c.LikeReceivedDailyCount, 0, c.LikeReceivedDailyPoint),
        _ => (0, 0, 0)
    };

    /// <summary>남은 한도로 clamp. cap 0 = 무제한. 이미 소진됐으면 0.</summary>
    private static int ClampByCap(int requested, int alreadyToday, int cap)
    {
        if (requested <= 0)
        {
            return 0;
        }

        if (cap <= 0)
        {
            return requested;
        }

        var remaining = cap - alreadyToday;
        if (remaining <= 0)
        {
            return 0;
        }

        return Math.Min(requested, remaining);
    }

    private static string BuildExpReason(string reason, string refID) => $"{reason} {RefMarker(refID)}";

    private static string RefMarker(string refID) => $"[{refID}]";

    private static DateOnly TodayKst()
    {
        var nowKst = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, KstZone);
        return DateOnly.FromDateTime(nowKst);
    }

    private static async Task<RewardConfig?> LoadConfigAsync(IAppDbContext db, CancellationToken ct)
    {
        var entity = await db.Config.AsNoTracking().OrderByDescending(c => c.ID).FirstOrDefaultAsync(ct);
        return entity?.Reward;
    }

    private static TimeZoneInfo ResolveKst()
    {
        if (TimeZoneInfo.TryFindSystemTimeZoneById("Asia/Seoul", out var tz))
        {
            return tz;
        }

        if (TimeZoneInfo.TryFindSystemTimeZoneById("Korea Standard Time", out tz))
        {
            return tz;
        }

        return TimeZoneInfo.CreateCustomTimeZone("KST", TimeSpan.FromHours(9), "KST", "KST");
    }
}
