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.Tenancy;

namespace Tiku.Infrastructure.Growth;

public sealed partial class ReferralService
{
    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);
    }
}