using System.Globalization;
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Points;
using Tiku.Domain.Commerce;
using Tiku.Domain.Learning;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Points;

public sealed class PointService(TikuDbContext dbContext) : IPointService
{
    public async Task<PointSummaryItem> GetSummaryAsync(
        PointActor actor,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        return await GetSummaryCoreAsync(actor, cancellationToken);
    }

    public async Task<PointList<PointTaskItem>> GetTasksAsync(
        PointActor actor,
        PointLimitQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        var now = DateTimeOffset.UtcNow;
        var limit = Math.Clamp(query.Limit ?? 50, 1, 200);
        var tasks = await dbContext.PointActivityTasks
            .AsNoTracking()
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.Status == PointActivityTaskStatus.Active &&
                (item.StartsAt == null || item.StartsAt <= now) &&
                (item.EndsAt == null || item.EndsAt > now))
            .OrderBy(item => item.SortOrder)
            .ThenBy(item => item.CreatedAt)
            .Take(limit)
            .ToArrayAsync(cancellationToken);
        var taskIds = tasks.Select(item => item.Id).ToArray();
        var claimCounts = await dbContext.PointActivityClaims
            .AsNoTracking()
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId &&
                item.Status == PointActivityClaimStatus.Claimed &&
                taskIds.Contains(item.TaskId))
            .GroupBy(item => item.TaskId)
            .Select(group => new { TaskId = group.Key, Count = group.Count() })
            .ToDictionaryAsync(item => item.TaskId, item => item.Count, cancellationToken);

        return new PointList<PointTaskItem>(
            tasks.Select(task =>
            {
                var claimedCount = claimCounts.GetValueOrDefault(task.Id);
                return ToTaskItem(task, claimedCount);
            }).ToArray());
    }

    public async Task<PointClaimItem> ClaimTaskAsync(
        PointActor actor,
        ClaimPointTaskCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        var taskKey = NormalizeKey(command.TaskKey, "task_key_required");
        var now = DateTimeOffset.UtcNow;
        var task = await dbContext.PointActivityTasks
            .SingleOrDefaultAsync(item =>
                item.TenantId == actor.TenantId &&
                item.TaskKey == taskKey,
                cancellationToken)
            ?? throw new PointException("Point activity task was not found.", "point_task_not_found");
        if (task.Status != PointActivityTaskStatus.Active ||
            task.StartsAt is not null && task.StartsAt > now ||
            task.EndsAt is not null && task.EndsAt <= now)
        {
            throw new PointException("Point activity task is not claimable.", "point_task_inactive");
        }

        if (!string.IsNullOrWhiteSpace(command.SourceType) && command.SourceId.HasValue)
        {
            var sourceType = NormalizeOptional(command.SourceType);
            var existing = await dbContext.PointActivityClaims
                .AsNoTracking()
                .Where(item =>
                    item.TenantId == actor.TenantId &&
                    item.UserId == actor.UserId &&
                    item.TaskId == task.Id &&
                    item.SourceType == sourceType &&
                    item.SourceId == command.SourceId.Value &&
                    item.Status == PointActivityClaimStatus.Claimed)
                .OrderByDescending(item => item.CreatedAt)
                .FirstOrDefaultAsync(cancellationToken);
            if (existing is not null)
            {
                return ToClaimItem(existing);
            }
        }

        var claimedCount = await dbContext.PointActivityClaims.CountAsync(
            item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId &&
                item.TaskId == task.Id &&
                item.Status == PointActivityClaimStatus.Claimed,
            cancellationToken);
        if (claimedCount >= task.MaxClaimsPerUser)
        {
            throw new PointException("Point activity task claim limit has been reached.", "point_task_claim_limit_reached");
        }

        var claim = new PointActivityClaim
        {
            TenantId = actor.TenantId,
            TaskId = task.Id,
            UserId = actor.UserId,
            TaskKey = task.TaskKey,
            Points = task.Points,
            Status = PointActivityClaimStatus.Claimed,
            SourceType = NormalizeOptional(command.SourceType),
            SourceId = command.SourceId,
            ClaimedAt = now,
            Metadata = JsonSerializer.SerializeToElement(new { source = "student_points" })
        };
        dbContext.PointActivityClaims.Add(claim);
        var balanceAfter = (await GetSummaryCoreAsync(actor, cancellationToken)).BalancePoints + task.Points;
        dbContext.UserScoreEvents.Add(new UserScoreEvent
        {
            TenantId = actor.TenantId,
            UserId = actor.UserId,
            EventType = task.TaskType == PointActivityTaskType.DailyLogin
                ? UserScoreEventType.CheckIn
                : UserScoreEventType.ActivityReward,
            Points = task.Points,
            BalanceAfter = balanceAfter,
            SourceType = NormalizeOptional(command.SourceType) ?? "point_task",
            SourceId = command.SourceId ?? claim.Id,
            IdempotencyKey = $"point-claim:{actor.TenantId:N}:{claim.Id:N}",
            Metadata = JsonSerializer.SerializeToElement(new
            {
                task.Id,
                task.TaskKey
            })
        });
        await dbContext.SaveChangesAsync(cancellationToken);
        return ToClaimItem(claim);
    }

    public async Task<PointList<PointExchangeItemDto>> GetExchangeItemsAsync(
        PointActor actor,
        PointLimitQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        var now = DateTimeOffset.UtcNow;
        var balance = (await GetSummaryCoreAsync(actor, cancellationToken)).BalancePoints;
        var items = dbContext.PointExchangeItems
            .AsNoTracking()
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.Status == PointExchangeItemStatus.Active &&
                (item.StartsAt == null || item.StartsAt <= now) &&
                (item.EndsAt == null || item.EndsAt > now));
        if (query.RegionId.HasValue)
        {
            items = items.Where(item => item.RegionId == null || item.RegionId == query.RegionId.Value);
        }

        var result = await items
            .OrderBy(item => item.SortOrder)
            .ThenBy(item => item.PointsCost)
            .Take(Math.Clamp(query.Limit ?? 50, 1, 200))
            .ToArrayAsync(cancellationToken);
        return new PointList<PointExchangeItemDto>(result.Select(item => ToExchangeItem(item, balance)).ToArray());
    }

    public async Task<PointExchangeOrderItem> CreateExchangeOrderAsync(
        PointActor actor,
        CreatePointExchangeOrderCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        await using var transaction = dbContext.Database.IsRelational()
            ? await dbContext.Database.BeginTransactionAsync(cancellationToken)
            : null;
        var now = DateTimeOffset.UtcNow;
        var item = await dbContext.PointExchangeItems
            .SingleOrDefaultAsync(entry =>
                entry.TenantId == actor.TenantId &&
                entry.Id == command.ItemId,
                cancellationToken)
            ?? throw new PointException("Point exchange item was not found.", "point_exchange_item_not_found");
        if (item.Status != PointExchangeItemStatus.Active ||
            item.StartsAt is not null && item.StartsAt > now ||
            item.EndsAt is not null && item.EndsAt <= now)
        {
            throw new PointException("Point exchange item is not available.", "point_exchange_item_inactive");
        }

        if (item.Stock is <= 0)
        {
            throw new PointException("Point exchange item is sold out.", "point_exchange_item_sold_out");
        }

        var balance = (await GetSummaryCoreAsync(actor, cancellationToken)).BalancePoints;
        if (balance < item.PointsCost)
        {
            throw new PointException("Point balance is insufficient.", "insufficient_points");
        }

        if (item.Stock.HasValue)
        {
            item.Stock -= 1;
        }

        var order = new PointExchangeOrder
        {
            TenantId = actor.TenantId,
            ItemId = item.Id,
            UserId = actor.UserId,
            OrderNo = GenerateOrderNo(),
            ItemName = item.Name,
            ItemType = item.ItemType,
            Status = PointExchangeOrderStatus.Completed,
            PointsCost = item.PointsCost,
            OrderedAt = now,
            CompletedAt = now,
            FulfillmentSnapshot = item.FulfillmentPayload,
            Metadata = JsonSerializer.SerializeToElement(new { source = "student_points_exchange" })
        };
        dbContext.PointExchangeOrders.Add(order);
        dbContext.UserScoreEvents.Add(new UserScoreEvent
        {
            TenantId = actor.TenantId,
            UserId = actor.UserId,
            EventType = UserScoreEventType.RedeemCost,
            Points = -item.PointsCost,
            BalanceAfter = balance - item.PointsCost,
            SourceType = "point_exchange_order",
            SourceId = order.Id,
            IdempotencyKey = $"point-exchange:{actor.TenantId:N}:{order.Id:N}",
            Metadata = JsonSerializer.SerializeToElement(new
            {
                item.Id,
                item.ItemKey,
                item.Name
            })
        });
        if (item.ItemType == PointExchangeItemType.Entitlement)
        {
            await GrantEntitlementAsync(actor, item, order, now, cancellationToken);
        }

        await dbContext.SaveChangesAsync(cancellationToken);
        if (transaction is not null)
        {
            await transaction.CommitAsync(cancellationToken);
        }
        return ToExchangeOrderItem(order);
    }

    public async Task<PointList<PointExchangeOrderItem>> GetExchangeOrdersAsync(
        PointActor actor,
        PointLimitQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        var orders = dbContext.PointExchangeOrders
            .AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId);
        if (!string.IsNullOrWhiteSpace(query.Status))
        {
            orders = orders.Where(item => item.Status == ParseExchangeOrderStatus(query.Status));
        }

        var result = await orders
            .OrderByDescending(item => item.CreatedAt)
            .Take(Math.Clamp(query.Limit ?? 50, 1, 200))
            .ToArrayAsync(cancellationToken);
        return new PointList<PointExchangeOrderItem>(result.Select(ToExchangeOrderItem).ToArray());
    }

    private async Task<PointSummaryItem> GetSummaryCoreAsync(
        PointActor actor,
        CancellationToken cancellationToken)
    {
        var earned = await dbContext.PointActivityClaims
            .AsNoTracking()
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId &&
                item.Status == PointActivityClaimStatus.Claimed)
            .SumAsync(item => (int?)item.Points, cancellationToken) ?? 0;
        var spent = await dbContext.PointExchangeOrders
            .AsNoTracking()
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId &&
                item.Status == PointExchangeOrderStatus.Completed)
            .SumAsync(item => (int?)item.PointsCost, cancellationToken) ?? 0;
        return new PointSummaryItem(earned, spent, earned - spent);
    }

    private async Task GrantEntitlementAsync(
        PointActor actor,
        PointExchangeItem item,
        PointExchangeOrder order,
        DateTimeOffset now,
        CancellationToken cancellationToken)
    {
        var days = item.Days ?? ReadInt(item.FulfillmentPayload, "days") ?? 0;
        var current = await dbContext.Entitlements
            .Where(entry =>
                entry.TenantId == actor.TenantId &&
                entry.UserId == actor.UserId &&
                entry.EntitlementType == "svip" &&
                entry.Status == EntitlementStatus.Active)
            .OrderByDescending(entry => entry.ExpiresAt)
            .FirstOrDefaultAsync(cancellationToken);
        if (current is null)
        {
            dbContext.Entitlements.Add(new Entitlement
            {
                TenantId = actor.TenantId,
                UserId = actor.UserId,
                EntitlementType = "svip",
                ScopeType = EntitlementScopeType.Tenant,
                SourceType = "point_exchange_order",
                SourceId = order.Id,
                StartsAt = now,
                ExpiresAt = days > 0 ? now.AddDays(days) : null,
                Status = EntitlementStatus.Active,
                Metadata = item.FulfillmentPayload
            });
        }
        else if (days > 0)
        {
            var baseAt = current.ExpiresAt.HasValue && current.ExpiresAt > now
                ? current.ExpiresAt.Value
                : now;
            current.ExpiresAt = baseAt.AddDays(days);
            current.Metadata = item.FulfillmentPayload;
        }
    }

    private async Task AssertActiveMemberAsync(PointActor actor, CancellationToken cancellationToken)
    {
        var exists = await dbContext.TenantMemberships.AnyAsync(item =>
            item.TenantId == actor.TenantId &&
            item.UserId == actor.UserId &&
            item.Status == MembershipStatus.Active,
            cancellationToken);
        if (!exists)
        {
            throw new PointException("Current user is not a member of the tenant.", "tenant_access_denied");
        }
    }

    private static PointTaskItem ToTaskItem(PointActivityTask task, int claimedCount)
    {
        return new PointTaskItem(
            task.Id,
            task.TaskKey,
            task.Title,
            task.Description,
            task.TaskType.ToString(),
            task.Status.ToString(),
            task.Points,
            task.MaxClaimsPerUser,
            claimedCount,
            claimedCount < task.MaxClaimsPerUser,
            task.StartsAt,
            task.EndsAt,
            task.SortOrder,
            task.Rules,
            task.Metadata);
    }

    private static PointClaimItem ToClaimItem(PointActivityClaim claim)
    {
        return new PointClaimItem(
            claim.Id,
            claim.TaskId,
            claim.TaskKey,
            claim.Points,
            claim.Status.ToString(),
            claim.SourceType,
            claim.SourceId,
            claim.ClaimedAt,
            claim.Metadata);
    }

    private static PointExchangeItemDto ToExchangeItem(PointExchangeItem item, int balance)
    {
        return new PointExchangeItemDto(
            item.Id,
            item.RegionId,
            item.ItemKey,
            item.Name,
            item.Description,
            item.ItemType.ToString(),
            item.Status.ToString(),
            item.PointsCost,
            item.Stock,
            item.Days,
            balance >= item.PointsCost && item.Stock is not <= 0,
            item.SortOrder,
            item.FulfillmentPayload,
            item.Metadata);
    }

    private static PointExchangeOrderItem ToExchangeOrderItem(PointExchangeOrder order)
    {
        return new PointExchangeOrderItem(
            order.Id,
            order.ItemId,
            order.OrderNo,
            order.ItemName,
            order.ItemType.ToString(),
            order.Status.ToString(),
            order.PointsCost,
            order.OrderedAt,
            order.CompletedAt,
            order.FulfillmentSnapshot,
            order.Metadata);
    }

    private static PointExchangeOrderStatus ParseExchangeOrderStatus(string? status)
    {
        return Enum.TryParse<PointExchangeOrderStatus>(
            NormalizeEnum(status),
            true,
            out var parsed)
            ? parsed
            : throw new PointException("Point exchange order status is invalid.", "invalid_point_exchange_order_status");
    }

    private static string NormalizeEnum(string? value) =>
        string.Concat((value ?? string.Empty).Split(
            ['_', '-', ' '],
            StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));

    private static string NormalizeKey(string? value, string code)
    {
        var normalized = NormalizeOptional(value);
        return !string.IsNullOrWhiteSpace(normalized)
            ? normalized
            : throw new PointException("Required point key is missing.", code);
    }

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

    private static int? ReadInt(JsonElement payload, string propertyName)
    {
        if (payload.ValueKind != JsonValueKind.Object ||
            !payload.TryGetProperty(propertyName, out var property))
        {
            return null;
        }

        return property.ValueKind == JsonValueKind.Number && property.TryGetInt32(out var value)
            ? value
            : null;
    }

    private static string GenerateOrderNo()
    {
        Span<byte> bytes = stackalloc byte[6];
        RandomNumberGenerator.Fill(bytes);
        return string.Create(
            CultureInfo.InvariantCulture,
            $"PX{DateTimeOffset.UtcNow:yyyyMMddHHmmss}{Convert.ToHexString(bytes)}");
    }
}
