using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Growth;
using Tiku.Domain.Common;
using Tiku.Domain.Growth;
using Tiku.Domain.Tenancy;

namespace Tiku.Infrastructure.Growth;

public sealed partial class ReferralService
{
    public async Task<ReferralInviteItem> GetOrCreateInviteCodeAsync(
        ReferralActor actor,
        ReferralInviteCommand command,
        CancellationToken cancellationToken = default)
    {
        var userId = RequireUser(actor);
        await AssertActiveMemberAsync(actor.TenantId, userId, cancellationToken);

        var existing = await dbContext.ReferralCodes
            .FirstOrDefaultAsync(item =>
                    item.TenantId == actor.TenantId &&
                    item.UserId == userId,
                cancellationToken);
        if (existing is not null)
        {
            if (!string.IsNullOrWhiteSpace(command.Channel)) existing.Channel = command.Channel.Trim();

            if (!string.IsNullOrWhiteSpace(command.LandingPath)) existing.LandingPath = command.LandingPath.Trim();

            existing.Status = ReferralCodeStatus.Active;
            await dbContext.SaveChangesAsync(cancellationToken);
            return new ReferralInviteItem(existing.Code);
        }

        var code = await GenerateUniqueCodeAsync(actor.TenantId, cancellationToken);
        var referralCode = new ReferralCode
        {
            TenantId = actor.TenantId,
            UserId = userId,
            Code = code,
            Channel = NormalizeOptional(command.Channel),
            LandingPath = NormalizeOptional(command.LandingPath),
            Metadata = JsonSerializer.SerializeToElement(new { source = "referral_invite_code" })
        };
        dbContext.ReferralCodes.Add(referralCode);
        await dbContext.SaveChangesAsync(cancellationToken);
        return new ReferralInviteItem(code);
    }

    public async Task<ReferralResolutionItem> ResolveAsync(
        ReferralActor actor,
        ResolveReferralCommand command,
        CancellationToken cancellationToken = default)
    {
        var code = NormalizeCode(command.Code);
        if (code is null) return new ReferralResolutionItem(false, null, null, null, null);

        var row = await (
                from referralCode in dbContext.ReferralCodes.AsNoTracking()
                join membership in dbContext.TenantMemberships.AsNoTracking()
                    on new { referralCode.TenantId, referralCode.UserId } equals new
                        { membership.TenantId, membership.UserId }
                join user in dbContext.Users.AsNoTracking()
                    on referralCode.UserId equals user.Id
                where referralCode.TenantId == actor.TenantId &&
                      referralCode.Code == code &&
                      referralCode.Status == ReferralCodeStatus.Active &&
                      membership.Status == MembershipStatus.Active
                select new
                {
                    referralCode.Code,
                    referralCode.UserId,
                    membership.Role,
                    user.Name,
                    user.UserName,
                    user.Phone
                })
            .FirstOrDefaultAsync(cancellationToken);

        return row is null
            ? new ReferralResolutionItem(false, null, null, null, null)
            : new ReferralResolutionItem(
                true,
                row.UserId,
                row.Code,
                row.Role.ToString(),
                FirstNonBlank(row.Name, row.UserName, row.Phone));
    }

    public async Task<ReferralTrackResult> TrackEventAsync(
        ReferralActor actor,
        TrackReferralEventCommand command,
        string? ipAddress,
        string? userAgent,
        CancellationToken cancellationToken = default)
    {
        var code = NormalizeCode(command.RefCode)
                   ?? throw new ReferralException("Referral code is required.", "referral_code_required");
        var eventType = NormalizeChoice(command.EventType, AllowedEventTypes, "enter", "invalid_referral_event_type");
        var source = NormalizeChoice(command.Source, AllowedSources, "unknown", "invalid_referral_source");
        var resolution = await ResolveCodeCoreAsync(actor.TenantId, code, cancellationToken);
        await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
        var track = new ReferralTrack
        {
            TenantId = actor.TenantId,
            ReferrerUserId = resolution?.UserId,
            TargetUserId = actor.UserId ?? command.TargetUserId,
            EventType = eventType,
            RefCode = code,
            Source = source,
            IpAddress = NormalizeOptional(ipAddress),
            UserAgent = Truncate(NormalizeOptional(userAgent), 1024),
            Metadata = command.Metadata ?? JsonDefaults.Object()
        };

        ReferralLead? lead = null;
        CrmWebhookQueueItem? crmQueue = null;
        if (resolution is not null && track.TargetUserId.HasValue && track.TargetUserId.Value != resolution.UserId)
        {
            lead = await BindLeadCoreAsync(
                actor.TenantId,
                track.TargetUserId.Value,
                resolution.UserId,
                code,
                source,
                ReferralLeadBindType.FirstTouch,
                false,
                command.Metadata,
                cancellationToken);
            var setFirstTrack = lead.FirstTrackId is null;
            if (setFirstTrack) await dbContext.SaveChangesAsync(cancellationToken);

            dbContext.ReferralTracks.Add(track);
            track.LeadId = lead.Id;
            await dbContext.SaveChangesAsync(cancellationToken);
            if (setFirstTrack) lead.FirstTrackId = track.Id;

            crmQueue = await EnqueueCrmIfEnabledAsync(actor.TenantId, lead, "referral.track_event", cancellationToken);
        }
        else
        {
            dbContext.ReferralTracks.Add(track);
        }

        await dbContext.SaveChangesAsync(cancellationToken);
        await transaction.CommitAsync(cancellationToken);
        return new ReferralTrackResult(ToTrackItem(track), lead is null ? null : ToLeadItem(lead, true),
            ToQueuePreview(crmQueue));
    }

    public async Task<ReferralBindResult> BindAsync(
        ReferralActor actor,
        BindReferralCommand command,
        CancellationToken cancellationToken = default)
    {
        var userId = RequireUser(actor);
        await AssertActiveMemberAsync(actor.TenantId, userId, cancellationToken);
        var code = NormalizeCode(command.RefCode)
                   ?? throw new ReferralException("Referral code is required.", "referral_code_required");
        var source = NormalizeChoice(command.Source, AllowedSources, "unknown", "invalid_referral_source");
        var resolution = await ResolveCodeCoreAsync(actor.TenantId, code, cancellationToken)
                         ?? throw new ReferralException("Referral code was not found.", "referral_code_not_found");
        if (resolution.UserId == userId)
            throw new ReferralException("User cannot bind to own referral code.", "self_referral_not_allowed");

        var existing = await dbContext.ReferralLeads
            .FirstOrDefaultAsync(item =>
                    item.TenantId == actor.TenantId &&
                    item.StudentUserId == userId,
                cancellationToken);
        var beforeReferrerId = existing?.ReferrerUserId;
        var lead = await BindLeadCoreAsync(
            actor.TenantId,
            userId,
            resolution.UserId,
            code,
            source,
            ReferralLeadBindType.FirstTouch,
            false,
            command.Metadata,
            cancellationToken);
        var crmQueue = beforeReferrerId == lead.ReferrerUserId
            ? null
            : await EnqueueCrmIfEnabledAsync(actor.TenantId, lead, "referral.bind", cancellationToken);

        await dbContext.SaveChangesAsync(cancellationToken);
        return new ReferralBindResult(ToLeadItem(lead, beforeReferrerId != lead.ReferrerUserId),
            ToQueuePreview(crmQueue));
    }

    public async Task<ReferralQrcodeItem> GetOrCreateQrcodeAsync(
        ReferralActor actor,
        ReferralQrcodeCommand command,
        CancellationToken cancellationToken = default)
    {
        var userId = RequireUser(actor);
        await AssertActiveMemberAsync(actor.TenantId, userId, cancellationToken);
        var refCode = (await GetOrCreateInviteCodeAsync(actor, new ReferralInviteCommand("qrcode"), cancellationToken))
            .InviteCode;
        var page = NormalizeOptional(command.Page) ?? "pages/index/index";
        var provider = NormalizeOptional(command.Provider) ?? "wechat-miniapp";
        var scene = NormalizeOptional(command.Scene) ?? $"ref={refCode}";
        var providedQrcodeUrl = NormalizeOptional(command.QrcodeUrl);
        var generated = providedQrcodeUrl is null
            ? await qrcodeGenerator.GenerateAsync(
                new ReferralQrcodeGenerateRequest(actor.TenantId, userId, refCode, provider, page, scene),
                cancellationToken)
            : new ReferralQrcodeGenerateResult(
                providedQrcodeUrl,
                provider,
                JsonSerializer.SerializeToElement(new { generatedBy = "external_url" }));
        var metadata = command.Metadata ?? generated.Metadata;

        var item = await dbContext.ReferralQrcodes
            .FirstOrDefaultAsync(entry =>
                    entry.TenantId == actor.TenantId &&
                    entry.Provider == generated.Provider &&
                    entry.Scene == scene &&
                    entry.Page == page,
                cancellationToken);
        if (item is null)
        {
            item = new ReferralQrcode
            {
                TenantId = actor.TenantId,
                UserId = userId,
                RefCode = refCode,
                Scene = scene,
                Page = page,
                Provider = generated.Provider
            };
            dbContext.ReferralQrcodes.Add(item);
        }

        item.UserId = userId;
        item.RefCode = refCode;
        item.QrcodeUrl = generated.QrcodeUrl;
        item.Status = ReferralQrcodeStatus.Ready;
        item.ErrorMessage = null;
        item.Metadata = metadata;
        await dbContext.SaveChangesAsync(cancellationToken);
        return ToQrcodeItem(item);
    }
}