using System.Globalization;
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Growth;
using Tiku.Application.Security;
using Tiku.Domain.Commerce;
using Tiku.Domain.Common;
using Tiku.Domain.Growth;
using Tiku.Domain.Identity;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Growth;

public sealed class ReferralService(
    TikuDbContext dbContext,
    IReferralQrcodeGenerator qrcodeGenerator,
    ICurrentAccessContext currentAccessContext) : IReferralService
{
    private static readonly HashSet<string> AllowedEventTypes = new(StringComparer.OrdinalIgnoreCase)
    {
        "enter",
        "register",
        "purchase",
        "share",
        "scan",
        "manual_bind"
    };

    private static readonly HashSet<string> AllowedSources = new(StringComparer.OrdinalIgnoreCase)
    {
        "share",
        "qrcode",
        "timeline",
        "miniapp",
        "h5",
        "manual",
        "unknown"
    };

    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);
    }

    public async Task<ReferralStatsItem> GetStatsAsync(
        ReferralAdminActor actor,
        ReferralStatsQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var referrerUserId = query.ReferrerUserId ?? actor.UserId;
        await AssertActiveMemberAsync(actor.TenantId, referrerUserId, cancellationToken);
        return await BuildStatsAsync(actor.TenantId, referrerUserId, cancellationToken);
    }

    public async Task<ReferralList<ReferralStatsItem>> GetSalesStatsAsync(
        ReferralAdminActor actor,
        ReferralStatsQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var limit = Math.Clamp(query.Limit ?? 100, 1, 500);
        var referrerIdsQuery = dbContext.ReferralLeads
            .AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId && item.ReferrerUserId != null);
        if (query.ReferrerUserId.HasValue)
        {
            referrerIdsQuery = referrerIdsQuery.Where(item => item.ReferrerUserId == query.ReferrerUserId.Value);
        }

        var referrerIds = await referrerIdsQuery
            .Select(item => item.ReferrerUserId!.Value)
            .Distinct()
            .Take(limit)
            .ToArrayAsync(cancellationToken);
        var stats = new List<ReferralStatsItem>(referrerIds.Length);
        foreach (var referrerId in referrerIds)
        {
            stats.Add(await BuildStatsAsync(actor.TenantId, referrerId, cancellationToken));
        }

        return new ReferralList<ReferralStatsItem>(
            stats
                .OrderByDescending(item => item.PaidAmountCents)
                .ThenByDescending(item => item.LeadCount)
                .Take(limit)
                .ToArray());
    }

    public async Task<ReferralConversionReport> GetConversionReportAsync(
        ReferralAdminActor actor,
        ReferralConversionQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var today = DateOnly.FromDateTime(DateTime.UtcNow);
        var endDate = query.EndDate ?? today;
        var startDate = query.StartDate ?? endDate.AddDays(-Math.Clamp(query.Days ?? 30, 1, 365) + 1);
        if (endDate < startDate)
        {
            throw new ReferralException("Referral date range was invalid.", "invalid_date_range");
        }

        var start = startDate.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
        var endExclusive = endDate.AddDays(1).ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
        var leadsQuery = dbContext.ReferralLeads
            .AsNoTracking()
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.BoundAt >= start &&
                item.BoundAt < endExclusive);
        if (query.ReferrerUserId.HasValue)
        {
            leadsQuery = leadsQuery.Where(item => item.ReferrerUserId == query.ReferrerUserId.Value);
        }

        var leads = await leadsQuery
            .OrderByDescending(item => item.BoundAt)
            .ToArrayAsync(cancellationToken);
        var paidStudentIds = await dbContext.Orders.AsNoTracking()
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.Status == OrderStatus.Paid &&
                item.UserId != null &&
                item.PaidAt != null &&
                item.PaidAt >= start &&
                item.PaidAt < endExclusive)
            .GroupBy(item => item.UserId!.Value)
            .Select(group => new
            {
                UserId = group.Key,
                AmountCents = group.Sum(item => item.AmountCents)
            })
            .ToDictionaryAsync(item => item.UserId, item => item.AmountCents, cancellationToken);
        var paidLeadCount = leads.Count(item => paidStudentIds.ContainsKey(item.StudentUserId));
        var paidAmountCents = leads.Sum(item => paidStudentIds.GetValueOrDefault(item.StudentUserId));
        var top = await GetSalesStatsAsync(
            actor,
            new ReferralStatsQuery(query.ReferrerUserId, query.Limit ?? 20),
            cancellationToken);

        return new ReferralConversionReport(
            startDate,
            endDate,
            leads.Length,
            paidLeadCount,
            paidAmountCents,
            leads.Length == 0 ? 0m : Math.Round(paidLeadCount * 100m / leads.Length, 2),
            top.Items,
            leads
                .Where(item => !paidStudentIds.ContainsKey(item.StudentUserId))
                .Take(Math.Clamp(query.Limit ?? 20, 1, 100))
                .Select(item => ToLeadItem(item, false))
                .ToArray());
    }

    public async Task<ReferralList<ReferralLeadItem>> GetClientsAsync(
        ReferralAdminActor actor,
        ReferralStatsQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var referrerUserId = query.ReferrerUserId ?? actor.UserId;
        await AssertActiveMemberAsync(actor.TenantId, referrerUserId, cancellationToken);
        var leads = await dbContext.ReferralLeads
            .AsNoTracking()
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.ReferrerUserId == referrerUserId)
            .OrderByDescending(item => item.BoundAt)
            .Take(Math.Clamp(query.Limit ?? 100, 1, 500))
            .ToArrayAsync(cancellationToken);
        return new ReferralList<ReferralLeadItem>(leads.Select(item => ToLeadItem(item, false)).ToArray());
    }

    public async Task<ReferralBindResult> ManualBindAsync(
        ReferralAdminActor actor,
        ManualBindReferralCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        if (command.StudentUserId == command.ReferrerUserId)
        {
            throw new ReferralException("User cannot bind to own referral code.", "self_referral_not_allowed");
        }

        var refCode = await dbContext.ReferralCodes
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == command.ReferrerUserId &&
                item.Status == ReferralCodeStatus.Active)
            .Select(item => item.Code)
            .FirstOrDefaultAsync(cancellationToken);
        if (refCode is null)
        {
            refCode = (await GetOrCreateInviteCodeAsync(
                new ReferralActor(actor.TenantId, command.ReferrerUserId),
                new ReferralInviteCommand("manual"),
                cancellationToken)).InviteCode;
        }

        var before = await dbContext.ReferralLeads
            .AsNoTracking()
            .FirstOrDefaultAsync(item =>
                item.TenantId == actor.TenantId &&
                item.StudentUserId == command.StudentUserId,
                cancellationToken);
        var lead = await BindLeadCoreAsync(
            actor.TenantId,
            command.StudentUserId,
            command.ReferrerUserId,
            refCode,
            NormalizeChoice(command.Source, AllowedSources, "manual", "invalid_referral_source"),
            ReferralLeadBindType.Manual,
            command.Force,
            command.Metadata,
            cancellationToken);
        lead.AssignedBy = actor.UserId;
        lead.AssignedAt = DateTimeOffset.UtcNow;
        var crmQueue = before?.ReferrerUserId == lead.ReferrerUserId
            ? null
            : await EnqueueCrmIfEnabledAsync(actor.TenantId, lead, "referral.manual_bind", cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return new ReferralBindResult(ToLeadItem(lead, before?.ReferrerUserId != lead.ReferrerUserId), ToQueuePreview(crmQueue));
    }

    public async Task<ReferralList<ReferralTeamItem>> GetTeamAsync(
        ReferralAdminActor actor,
        ReferralTeamQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        var edges = dbContext.ReferralTeamEdges
            .AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId);
        if (query.LeaderUserId.HasValue)
        {
            edges = edges.Where(item => item.LeaderUserId == query.LeaderUserId.Value);
        }

        var items = await edges
            .OrderBy(item => item.RelationType)
            .ThenBy(item => item.MemberUserId)
            .ToArrayAsync(cancellationToken);
        return new ReferralList<ReferralTeamItem>(items.Select(ToTeamItem).ToArray());
    }

    public async Task<ReferralTeamItem> UpsertTeamAsync(
        ReferralAdminActor actor,
        UpsertReferralTeamCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertAdminAsync(actor, cancellationToken);
        await AssertActiveMemberAsync(actor.TenantId, command.MemberUserId, cancellationToken);
        if (command.LeaderUserId.HasValue)
        {
            await AssertActiveMemberAsync(actor.TenantId, command.LeaderUserId.Value, cancellationToken);
        }

        var relationType = ParseEnum(command.RelationType, ReferralTeamRelationType.SalesTeam, "invalid_referral_team_relation");
        var status = ParseEnum(command.Status, ReferralTeamEdgeStatus.Active, "invalid_referral_team_status");
        var edge = await dbContext.ReferralTeamEdges
            .FirstOrDefaultAsync(item =>
                item.TenantId == actor.TenantId &&
                item.MemberUserId == command.MemberUserId &&
                item.RelationType == relationType,
                cancellationToken);
        if (edge is null)
        {
            edge = new ReferralTeamEdge
            {
                TenantId = actor.TenantId,
                MemberUserId = command.MemberUserId,
                RelationType = relationType
            };
            dbContext.ReferralTeamEdges.Add(edge);
        }

        edge.LeaderUserId = command.LeaderUserId;
        edge.Status = status;
        edge.Metadata = command.Metadata ?? JsonDefaults.Object();
        await dbContext.SaveChangesAsync(cancellationToken);
        return ToTeamItem(edge);
    }

    private async Task<ReferralLead> BindLeadCoreAsync(
        Guid tenantId,
        Guid studentUserId,
        Guid referrerUserId,
        string refCode,
        string source,
        ReferralLeadBindType bindType,
        bool force,
        JsonElement? metadata,
        CancellationToken cancellationToken)
    {
        await AssertActiveMemberAsync(tenantId, studentUserId, cancellationToken);
        await AssertActiveMemberAsync(tenantId, referrerUserId, cancellationToken);
        var now = DateTimeOffset.UtcNow;
        var lead = await dbContext.ReferralLeads
            .FirstOrDefaultAsync(item =>
                item.TenantId == tenantId &&
                item.StudentUserId == studentUserId,
                cancellationToken);
        if (lead is not null)
        {
            if (lead.ReferrerUserId == referrerUserId)
            {
                return lead;
            }

            if (!force && lead.Status == ReferralLeadStatus.Protected && (lead.ProtectedUntil is null || lead.ProtectedUntil > now))
            {
                throw new ReferralException("Referral lead is protected and cannot be rebound.", "referral_lead_protected");
            }
        }
        else
        {
            lead = new ReferralLead
            {
                TenantId = tenantId,
                StudentUserId = studentUserId
            };
            dbContext.ReferralLeads.Add(lead);
        }

        lead.ReferrerUserId = referrerUserId;
        lead.RefCode = refCode;
        lead.Source = source;
        lead.BindType = bindType;
        lead.Status = ReferralLeadStatus.Protected;
        lead.ProtectedUntil = now.AddDays(30);
        lead.BoundAt = now;
        lead.Metadata = metadata ?? JsonDefaults.Object();
        return lead;
    }

    private async Task<CrmWebhookQueueItem?> EnqueueCrmIfEnabledAsync(
        Guid tenantId,
        ReferralLead lead,
        string source,
        CancellationToken cancellationToken)
    {
        var config = await dbContext.CrmConfigs
            .AsNoTracking()
            .FirstOrDefaultAsync(item => item.TenantId == tenantId && item.Enabled, cancellationToken);
        if (config is null || string.IsNullOrWhiteSpace(config.Url))
        {
            return null;
        }

        var recordId = lead.Id.ToString("N", CultureInfo.InvariantCulture);
        var idempotencyKey = $"{source}:{recordId}";
        var existing = await dbContext.CrmWebhookQueue
            .FirstOrDefaultAsync(item => item.TenantId == tenantId && item.IdempotencyKey == idempotencyKey, cancellationToken);
        if (existing is not null)
        {
            return existing;
        }

        var queue = new CrmWebhookQueueItem
        {
            TenantId = tenantId,
            RecordId = recordId,
            LeadId = recordId,
            Source = source,
            Provider = "webhook",
            Status = CrmWebhookQueueStatus.Pending,
            ScheduledAt = DateTimeOffset.UtcNow.AddSeconds(Math.Max(config.DelaySeconds ?? 0, 0)),
            IdempotencyKey = idempotencyKey,
            TargetUrl = config.Url,
            Payload = JsonSerializer.SerializeToElement(new
            {
                tenantId,
                leadId = lead.Id,
                lead.StudentUserId,
                lead.ReferrerUserId,
                lead.RefCode,
                lead.Source,
                lead.BoundAt,
                config.FormName,
                config.ExamType
            })
        };
        dbContext.CrmWebhookQueue.Add(queue);
        return queue;
    }

    private async Task<ReferralCode?> ResolveCodeCoreAsync(Guid tenantId, string code, CancellationToken cancellationToken)
    {
        return await dbContext.ReferralCodes
            .AsNoTracking()
            .FirstOrDefaultAsync(item =>
                item.TenantId == tenantId &&
                item.Code == code &&
                item.Status == ReferralCodeStatus.Active,
                cancellationToken);
    }

    private async Task AssertActiveMemberAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken)
    {
        var exists = await dbContext.TenantMemberships.AnyAsync(
            item =>
                item.TenantId == tenantId &&
                item.UserId == userId &&
                item.Status == MembershipStatus.Active,
            cancellationToken);
        if (!exists)
        {
            throw new ReferralException("Tenant member was not found.", "tenant_access_denied");
        }
    }

    private async Task AssertAdminAsync(ReferralAdminActor actor, CancellationToken cancellationToken)
    {
        var access = await currentAccessContext.GetAsync(cancellationToken);
        if (!access.IsCurrentTenantMember ||
            access.TenantId != actor.TenantId ||
            access.UserId != actor.UserId ||
            !access.HasTenantPermission(BackendPermissions.TenantCrmManage))
        {
            throw new ReferralException("Referral admin access was denied.", "referral_access_denied");
        }
    }

    private async Task<ReferralStatsItem> BuildStatsAsync(
        Guid tenantId,
        Guid referrerUserId,
        CancellationToken cancellationToken)
    {
        var user = await dbContext.Users.AsNoTracking()
            .Where(item => item.Id == referrerUserId)
            .Select(item => new { item.Name, item.UserName, item.Phone })
            .FirstOrDefaultAsync(cancellationToken);
        var membership = await dbContext.TenantMemberships.AsNoTracking()
            .Where(item => item.TenantId == tenantId && item.UserId == referrerUserId)
            .Select(item => item.Role)
            .FirstOrDefaultAsync(cancellationToken);
        var inviteCode = await dbContext.ReferralCodes.AsNoTracking()
            .Where(item =>
                item.TenantId == tenantId &&
                item.UserId == referrerUserId &&
                item.Status == ReferralCodeStatus.Active)
            .Select(item => item.Code)
            .FirstOrDefaultAsync(cancellationToken);
        var leads = await dbContext.ReferralLeads.AsNoTracking()
            .Where(item => item.TenantId == tenantId && item.ReferrerUserId == referrerUserId)
            .Select(item => item.StudentUserId)
            .ToArrayAsync(cancellationToken);
        var paid = leads.Length == 0
            ? []
            : await dbContext.Orders.AsNoTracking()
                .Where(item =>
                    item.TenantId == tenantId &&
                    item.Status == OrderStatus.Paid &&
                    item.UserId != null &&
                    leads.Contains(item.UserId.Value))
                .GroupBy(item => item.UserId!.Value)
                .Select(group => new
                {
                    UserId = group.Key,
                    AmountCents = group.Sum(item => item.AmountCents)
                })
                .ToArrayAsync(cancellationToken);
        var trackCount = await dbContext.ReferralTracks.AsNoTracking()
            .CountAsync(item => item.TenantId == tenantId && item.ReferrerUserId == referrerUserId, cancellationToken);

        return new ReferralStatsItem(
            referrerUserId,
            FirstNonBlank(user?.Name, user?.UserName, user?.Phone),
            membership.ToString(),
            inviteCode,
            leads.Length,
            paid.Length,
            paid.Sum(item => item.AmountCents),
            trackCount,
            leads.Length == 0 ? 0m : Math.Round(paid.Length * 100m / leads.Length, 2));
    }

    private async Task<string> GenerateUniqueCodeAsync(Guid tenantId, CancellationToken cancellationToken)
    {
        for (var attempt = 0; attempt < 20; attempt++)
        {
            var code = GenerateCode();
            var exists = await dbContext.ReferralCodes.AnyAsync(
                item => item.TenantId == tenantId && item.Code == code,
                cancellationToken);
            if (!exists)
            {
                return code;
            }
        }

        throw new ReferralException("Could not generate referral code.", "referral_code_generation_failed");
    }

    private static string GenerateCode()
    {
        const string alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
        Span<byte> bytes = stackalloc byte[8];
        RandomNumberGenerator.Fill(bytes);
        Span<char> chars = stackalloc char[8];
        for (var index = 0; index < chars.Length; index++)
        {
            chars[index] = alphabet[bytes[index] % alphabet.Length];
        }

        return new string(chars);
    }

    private static Guid RequireUser(ReferralActor actor)
    {
        return actor.UserId ?? throw new ReferralException("Current referral actor was not resolved.", "referral_access_denied");
    }

    private static string? NormalizeCode(string? value)
    {
        return string.IsNullOrWhiteSpace(value)
            ? null
            : value.Trim().ToUpperInvariant();
    }

    private static string NormalizeChoice(
        string? value,
        HashSet<string> allowed,
        string defaultValue,
        string errorCode)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return defaultValue;
        }

        var normalized = value.Trim().ToLowerInvariant();
        return allowed.Contains(normalized)
            ? normalized
            : throw new ReferralException("Referral value was invalid.", errorCode);
    }

    private static TEnum ParseEnum<TEnum>(string? value, TEnum defaultValue, string errorCode)
        where TEnum : struct, Enum
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return defaultValue;
        }

        var normalized = value.Replace("_", string.Empty, StringComparison.Ordinal);
        foreach (var enumValue in Enum.GetValues<TEnum>())
        {
            if (string.Equals(enumValue.ToString(), normalized, StringComparison.OrdinalIgnoreCase))
            {
                return enumValue;
            }
        }

        throw new ReferralException("Referral enum value was invalid.", errorCode);
    }

    private static string? NormalizeOptional(string? value)
    {
        return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
    }

    private static string? Truncate(string? value, int maxLength)
    {
        return value is null || value.Length <= maxLength ? value : value[..maxLength];
    }

    private static string? FirstNonBlank(params string?[] values)
    {
        return values.Select(NormalizeOptional).FirstOrDefault(value => value is not null);
    }

    private static ReferralTrackItem ToTrackItem(ReferralTrack item)
    {
        return new ReferralTrackItem(
            item.Id,
            item.EventType,
            item.RefCode,
            item.ReferrerUserId,
            item.TargetUserId,
            item.Source,
            item.CreatedAt);
    }

    private static ReferralLeadItem ToLeadItem(ReferralLead item, bool changed)
    {
        return new ReferralLeadItem(
            item.Id,
            item.StudentUserId,
            item.ReferrerUserId,
            item.RefCode,
            item.Status.ToString(),
            item.BoundAt,
            changed);
    }

    private static ReferralQrcodeItem ToQrcodeItem(ReferralQrcode item)
    {
        return new ReferralQrcodeItem(
            item.Id,
            item.RefCode,
            item.Scene,
            item.Page,
            item.Provider,
            item.QrcodeUrl,
            item.Status.ToString(),
            item.Metadata);
    }

    private static CrmQueuePreviewItem? ToQueuePreview(CrmWebhookQueueItem? item)
    {
        return item is null ? null : new CrmQueuePreviewItem(item.Id, item.Status.ToString(), item.Source);
    }

    private static ReferralTeamItem ToTeamItem(ReferralTeamEdge item)
    {
        return new ReferralTeamItem(
            item.Id,
            item.MemberUserId,
            item.LeaderUserId,
            item.RelationType.ToString(),
            item.Status.ToString(),
            item.Metadata);
    }
}
