using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.PlatformAdmin;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
using Tiku.Domain.Common;
using Tiku.Domain.Growth;
using Tiku.Domain.Operations;
using Tiku.Domain.Platform;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.PlatformAdmin;

internal sealed class PlatformSmsAdminService(ITenantExecutionScope tenantExecutionScope) : IPlatformSmsAdminService
{
    public Task<PlatformTenantCapabilityList<PlatformSmsChannelItem>> GetChannelsAsync(PlatformCapabilityActor actor,
        PlatformCapabilityQuery query, CancellationToken cancellationToken = default)
    {
        return ExecuteAsync("platform sms channels list", async (services, token) =>
        {
            var db = services.GetRequiredService<IPlatformTenantCapabilitiesPersistence>();
            var monthStart = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, 1, 0, 0, 0,
                TimeSpan.Zero);
            var sentCounts = await db.SmsSendLogs.AsNoTracking()
                .Where(value => value.CreatedAt >= monthStart && value.Status == SmsSendLogStatus.Sent &&
                                value.ChannelId.HasValue)
                .GroupBy(value => value.ChannelId!.Value)
                .Select(group => new { ChannelId = group.Key, Count = group.Count() })
                .ToDictionaryAsync(value => value.ChannelId, value => value.Count, token);
            var values =
                from channel in db.SmsChannels.AsNoTracking()
                join tenant in db.Tenants.AsNoTracking() on channel.TenantId equals tenant.Id
                select new { channel, tenant.Name };
            if (query.TenantId.HasValue) values = values.Where(value => value.channel.TenantId == query.TenantId);
            if (!string.IsNullOrWhiteSpace(query.Status))
                values = values.Where(value =>
                    value.channel.Status == ParseEnum(query.Status, TenantExternalProviderStatus.Active));
            var rows = await values.OrderBy(value => value.channel.Priority)
                .ThenByDescending(value => value.channel.UpdatedAt).Take(Limit(query.Limit)).ToArrayAsync(token);
            return new PlatformTenantCapabilityList<PlatformSmsChannelItem>(rows.Select(value =>
                ToChannelItem(value.channel, value.Name, sentCounts.GetValueOrDefault(value.channel.Id))).ToArray());
        }, cancellationToken);
    }

    public Task<PlatformSmsChannelItem> UpsertChannelAsync(PlatformCapabilityActor actor,
        UpsertPlatformSmsChannelCommand command, CancellationToken cancellationToken = default)
    {
        return ExecuteAsync("platform sms channel upsert", async (services, token) =>
        {
            var db = services.GetRequiredService<IPlatformTenantCapabilitiesPersistence>();
            var tenantName = await TenantNameAsync(db, command.TenantId, token);
            var provider = NormalizeCode(command.Provider);
            var scene = NormalizeCode(command.Scene);
            var item = command.Id.HasValue
                ? await db.SmsChannels.SingleOrDefaultAsync(
                    value => value.Id == command.Id.Value && value.TenantId == command.TenantId, token)
                : await db.SmsChannels.SingleOrDefaultAsync(
                    value => value.TenantId == command.TenantId && value.Provider == provider && value.Scene == scene,
                    token);
            if (item is null)
            {
                item = new SmsChannel { TenantId = command.TenantId };
                db.SmsChannels.Add(item);
            }

            item.Provider = provider;
            item.Name = command.Name.Trim();
            item.Signature = command.Signature.Trim();
            item.Scene = scene;
            item.Status = command.Status;
            item.SecretRef = Normalize(command.SecretRef);
            item.Priority = command.Priority ?? item.Priority;
            item.MonthlyQuota = Math.Max(0, command.MonthlyQuota ?? item.MonthlyQuota);
            item.ConfigPublic = command.ConfigPublic;
            item.Metadata = command.Metadata;
            item.UpdatedAt = DateTimeOffset.UtcNow;
            AddAudit(db, actor, command.TenantId, "platform.sms.channel.upserted", "sms_channels", item.Id,
                new { item.Provider, item.Scene, item.Status });
            await db.SaveChangesAsync(token);
            return ToChannelItem(item, tenantName, 0);
        }, cancellationToken);
    }

    public Task<PlatformSmsChannelItem> DisableChannelAsync(PlatformCapabilityActor actor, Guid channelId,
        CancellationToken cancellationToken = default)
    {
        return ExecuteAsync("platform sms channel disable", async (services, token) =>
        {
            var db = services.GetRequiredService<IPlatformTenantCapabilitiesPersistence>();
            var item = await db.SmsChannels.SingleOrDefaultAsync(value => value.Id == channelId, token)
                       ?? throw Error("SMS channel was not found.", "sms_channel_not_found");
            item.Status = TenantExternalProviderStatus.Disabled;
            item.UpdatedAt = DateTimeOffset.UtcNow;
            AddAudit(db, actor, item.TenantId, "platform.sms.channel.disabled", "sms_channels", item.Id,
                new { item.Provider, item.Scene });
            await db.SaveChangesAsync(token);
            return ToChannelItem(item, await TenantNameAsync(db, item.TenantId, token), 0);
        }, cancellationToken);
    }

    public Task<PlatformTenantCapabilityList<PlatformSmsTemplateItem>> GetTemplatesAsync(PlatformCapabilityActor actor,
        PlatformCapabilityQuery query, CancellationToken cancellationToken = default)
    {
        return ExecuteAsync("platform sms templates list", async (services, token) =>
        {
            var db = services.GetRequiredService<IPlatformTenantCapabilitiesPersistence>();
            var sentCounts = await db.SmsSendLogs.AsNoTracking()
                .Where(value => value.TemplateId.HasValue)
                .GroupBy(value => value.TemplateId!.Value)
                .Select(group => new
                {
                    TemplateId = group.Key,
                    Sent = group.Count(value => value.Status == SmsSendLogStatus.Sent),
                    Failed = group.Count(value => value.Status == SmsSendLogStatus.Failed)
                })
                .ToDictionaryAsync(value => value.TemplateId, token);
            var values =
                from template in db.SmsTemplates.AsNoTracking()
                join channel in db.SmsChannels.AsNoTracking() on new { template.TenantId, template.ChannelId } equals
                    new { channel.TenantId, ChannelId = channel.Id }
                join tenant in db.Tenants.AsNoTracking() on template.TenantId equals tenant.Id
                select new { template, channel.Name, TenantName = tenant.Name };
            if (query.TenantId.HasValue) values = values.Where(value => value.template.TenantId == query.TenantId);
            if (!string.IsNullOrWhiteSpace(query.Status))
                values = values.Where(value =>
                    value.template.Status == ParseEnum(query.Status, SmsTemplateStatus.Active));
            var rows = await values.OrderByDescending(value => value.template.UpdatedAt).Take(Limit(query.Limit))
                .ToArrayAsync(token);
            return new PlatformTenantCapabilityList<PlatformSmsTemplateItem>(rows.Select(value =>
            {
                var counts = sentCounts.GetValueOrDefault(value.template.Id);
                return ToTemplateItem(value.template, value.TenantName, value.Name, counts?.Sent ?? 0,
                    counts?.Failed ?? 0);
            }).ToArray());
        }, cancellationToken);
    }

    public Task<PlatformSmsTemplateItem> UpsertTemplateAsync(PlatformCapabilityActor actor,
        UpsertPlatformSmsTemplateCommand command, CancellationToken cancellationToken = default)
    {
        return ExecuteAsync("platform sms template upsert", async (services, token) =>
        {
            var db = services.GetRequiredService<IPlatformTenantCapabilitiesPersistence>();
            var channel = await db.SmsChannels.AsNoTracking()
                              .SingleOrDefaultAsync(
                                  value => value.Id == command.ChannelId && value.TenantId == command.TenantId, token)
                          ?? throw Error("SMS channel was not found.", "sms_channel_not_found");
            var item = command.Id.HasValue
                ? await db.SmsTemplates.SingleOrDefaultAsync(
                    value => value.Id == command.Id.Value && value.TenantId == command.TenantId, token)
                : await db.SmsTemplates.SingleOrDefaultAsync(
                    value => value.TenantId == command.TenantId && value.Code == NormalizeCode(command.Code), token);
            if (item is null)
            {
                item = new SmsTemplate { TenantId = command.TenantId };
                db.SmsTemplates.Add(item);
            }

            item.ChannelId = command.ChannelId;
            item.Code = NormalizeCode(command.Code);
            item.Name = command.Name.Trim();
            item.Type = command.Type;
            item.AuditStatus = command.AuditStatus;
            item.Status = command.Status;
            item.ProviderTemplateCode = Normalize(command.ProviderTemplateCode);
            item.Content = command.Content.Trim();
            item.Remark = Normalize(command.Remark);
            item.Metadata = command.Metadata;
            item.UpdatedAt = DateTimeOffset.UtcNow;
            AddAudit(db, actor, command.TenantId, "platform.sms.template.upserted", "sms_templates", item.Id,
                new { item.Code, item.AuditStatus, item.Status });
            await db.SaveChangesAsync(token);
            return ToTemplateItem(item, await TenantNameAsync(db, item.TenantId, token), channel.Name, 0, 0);
        }, cancellationToken);
    }

    public Task<PlatformSmsTemplateItem> SubmitTemplateReviewAsync(PlatformCapabilityActor actor, Guid templateId,
        CancellationToken cancellationToken = default)
    {
        return ChangeTemplateAsync(actor, templateId, SmsTemplateAuditStatus.PendingReview, null,
            "platform.sms.template.review_submitted", cancellationToken);
    }

    public Task<PlatformSmsTemplateItem> DisableTemplateAsync(PlatformCapabilityActor actor, Guid templateId,
        CancellationToken cancellationToken = default)
    {
        return ChangeTemplateAsync(actor, templateId, null, SmsTemplateStatus.Disabled,
            "platform.sms.template.disabled", cancellationToken);
    }

    public Task<PlatformTenantCapabilityList<SmsSendLog>> GetLogsAsync(PlatformCapabilityActor actor,
        PlatformCapabilityQuery query, CancellationToken cancellationToken = default)
    {
        return ExecuteAsync("platform sms logs list", async (services, token) =>
        {
            var db = services.GetRequiredService<IPlatformTenantCapabilitiesPersistence>();
            var values = db.SmsSendLogs.AsNoTracking();
            if (query.TenantId.HasValue) values = values.Where(value => value.TenantId == query.TenantId);
            if (!string.IsNullOrWhiteSpace(query.Status))
                values = values.Where(value => value.Status == ParseEnum(query.Status, SmsSendLogStatus.Sent));
            return new PlatformTenantCapabilityList<SmsSendLog>(await values.OrderByDescending(value => value.CreatedAt)
                .Take(Limit(query.Limit)).ToArrayAsync(token));
        }, cancellationToken);
    }

    private Task<PlatformSmsTemplateItem> ChangeTemplateAsync(PlatformCapabilityActor actor, Guid templateId,
        SmsTemplateAuditStatus? auditStatus, SmsTemplateStatus? status, string action,
        CancellationToken cancellationToken)
    {
        return ExecuteAsync(action, async (services, token) =>
        {
            var db = services.GetRequiredService<IPlatformTenantCapabilitiesPersistence>();
            var item = await db.SmsTemplates.SingleOrDefaultAsync(value => value.Id == templateId, token)
                       ?? throw Error("SMS template was not found.", "sms_template_not_found");
            var channelName = await db.SmsChannels.AsNoTracking()
                .Where(value => value.TenantId == item.TenantId && value.Id == item.ChannelId)
                .Select(value => value.Name).SingleAsync(token);
            if (auditStatus.HasValue)
            {
                item.AuditStatus = auditStatus.Value;
                item.SubmittedAt = DateTimeOffset.UtcNow;
            }

            if (status.HasValue)
            {
                item.Status = status.Value;
                item.DisabledAt = DateTimeOffset.UtcNow;
            }

            item.UpdatedAt = DateTimeOffset.UtcNow;
            AddAudit(db, actor, item.TenantId, action, "sms_templates", item.Id,
                new { item.Code, item.AuditStatus, item.Status });
            await db.SaveChangesAsync(token);
            return ToTemplateItem(item, await TenantNameAsync(db, item.TenantId, token), channelName, 0, 0);
        }, cancellationToken);
    }

    private Task<TResult> ExecuteAsync<TResult>(string reason,
        Func<IServiceProvider, CancellationToken, Task<TResult>> operation, CancellationToken cancellationToken)
    {
        return tenantExecutionScope.ExecuteAsync(
            new SystemScopeRequest(null, SystemScopeCallerType.Platform, nameof(PlatformSmsAdminService), reason,
                Guid.NewGuid().ToString("N"), true), operation, cancellationToken);
    }

    private static PlatformSmsChannelItem ToChannelItem(SmsChannel channel, string tenantName, int sentThisMonth)
    {
        return new PlatformSmsChannelItem(channel.Id, channel.TenantId, tenantName, channel.Provider, channel.Name,
            channel.Signature,
            channel.Scene, channel.Status,
            channel.SecretRef, channel.Priority, channel.MonthlyQuota, sentThisMonth, channel.ConfigPublic,
            channel.Metadata, channel.UpdatedAt);
    }

    private static PlatformSmsTemplateItem ToTemplateItem(SmsTemplate template, string tenantName, string channelName,
        int sent, int failed)
    {
        return new PlatformSmsTemplateItem(template.Id, template.TenantId, tenantName, template.ChannelId, channelName,
            template.Code,
            template.Name, template.Type,
            template.AuditStatus, template.Status, template.ProviderTemplateCode, template.Content, template.Remark,
            sent, failed, template.UpdatedAt);
    }

    private static async Task<string> TenantNameAsync(IPlatformTenantCapabilitiesPersistence db, Guid tenantId, CancellationToken token)
    {
        return await db.Tenants.Where(value => value.Id == tenantId).Select(value => value.Name)
                   .SingleOrDefaultAsync(token)
               ?? throw Error("Tenant was not found.", "tenant_not_found");
    }

    private static PlatformCapabilityException Error(string message, string code)
    {
        return new PlatformCapabilityException(message, code);
    }

    private static int Limit(int value)
    {
        return Math.Clamp(value, 1, 500);
    }

    private static string NormalizeCode(string value)
    {
        return value.Trim().ToLowerInvariant().Replace("-", "_", StringComparison.Ordinal);
    }

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

    private static T ParseEnum<T>(string? value, T fallback) where T : struct, Enum
    {
        return Enum.TryParse<T>(value?.Trim().Replace("-", "_", StringComparison.Ordinal), true, out var parsed)
            ? parsed
            : fallback;
    }

    private static void AddAudit(IPlatformTenantCapabilitiesPersistence db, PlatformCapabilityActor actor, Guid tenantId, string action,
        string targetType, Guid targetId, object details)
    {
        db.AuditLogs.Add(new AuditLog
        {
            TenantId = tenantId,
            ActorUserId = actor.UserId,
            Action = action,
            TargetType = targetType,
            TargetId = targetId.ToString(),
            Details = JsonSerializer.SerializeToElement(details)
        });
    }
}
