using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Backoffice;
using Tiku.Application.PlatformBilling;
using Tiku.Application.Security;
using Tiku.Domain.Platform;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.PlatformBilling;

internal sealed class SaasCatalogAdminService(
    TikuDbContext dbContext,
    IOperationAuditService auditService) : ISaasCatalogAdminService
{
    public async Task<SaasCatalogSnapshot> GetCatalogAsync(
        SaasCatalogActor actor,
        CancellationToken cancellationToken = default)
    {
        var features = await dbContext.SaasFeatures.AsNoTracking()
            .OrderBy(value => value.SortOrder).ThenBy(value => value.Code)
            .ToArrayAsync(cancellationToken);
        var limits = await dbContext.SaasFeatureLimitDefinitions.AsNoTracking()
            .OrderBy(value => value.FeatureCode).ThenBy(value => value.MetricCode)
            .ToArrayAsync(cancellationToken);
        var offerings = await dbContext.SaasOfferings.AsNoTracking()
            .OrderBy(value => value.SortOrder).ThenBy(value => value.Code)
            .ToArrayAsync(cancellationToken);
        var versions = await LoadVersionsAsync(null, cancellationToken);
        return new SaasCatalogSnapshot(features, limits, offerings, versions);
    }

    public async Task<SaasFeatureLimitDefinition> UpsertLimitDefinitionAsync(
        SaasCatalogActor actor,
        UpsertSaasFeatureLimitCommand command,
        CancellationToken cancellationToken = default)
    {
        var metricCode = Normalize(command.MetricCode);
        var featureCode = Normalize(command.FeatureCode);
        if (command.WarningPercent != 80 || !command.IsHardLimit)
            throw Error("Phase nine supports hard limits with an 80 percent warning threshold.",
                "saas_feature_limit_policy_invalid");
        if (!await dbContext.SaasFeatures.AnyAsync(value =>
                value.Code == featureCode && value.Status == SaasFeatureStatus.Active, cancellationToken))
            throw Error("SaaS feature was not found or active.", "saas_feature_not_found");

        var item = command.Id.HasValue
            ? await dbContext.SaasFeatureLimitDefinitions.SingleOrDefaultAsync(value => value.Id == command.Id,
                cancellationToken)
            : await dbContext.SaasFeatureLimitDefinitions.SingleOrDefaultAsync(value => value.MetricCode == metricCode,
                cancellationToken);
        if (item is null)
        {
            item = new SaasFeatureLimitDefinition { MetricCode = metricCode };
            dbContext.SaasFeatureLimitDefinitions.Add(item);
        }
        else if (await dbContext.SaasOfferingVersionLimits.AnyAsync(value => value.MetricCode == item.MetricCode,
                     cancellationToken) &&
                 (!string.Equals(item.MetricCode, metricCode, StringComparison.Ordinal) ||
                  !string.Equals(item.FeatureCode, featureCode, StringComparison.Ordinal) ||
                  item.Kind != command.Kind))
        {
            throw Error("A limit definition referenced by offering versions cannot change identity or kind.",
                "saas_feature_limit_locked");
        }

        item.MetricCode = metricCode;
        item.FeatureCode = featureCode;
        item.Name = Required(command.Name, "name");
        item.Unit = Required(command.Unit, "unit");
        item.Kind = command.Kind;
        item.WarningPercent = command.WarningPercent;
        item.IsHardLimit = command.IsHardLimit;
        await dbContext.SaveChangesAsync(cancellationToken);
        await AuditAsync(actor, "platform.saas.feature_limit.upserted", "saas_feature_limit_definitions", item.Id,
            new { item.MetricCode, item.FeatureCode, item.Kind }, cancellationToken);
        return item;
    }

    public async Task<SaasFeature> UpsertFeatureAsync(
        SaasCatalogActor actor,
        UpsertSaasFeatureCommand command,
        CancellationToken cancellationToken = default)
    {
        var code = Normalize(command.Code);
        if (!SaasFeatureCatalog.All.Contains(code))
            throw Error("Feature code is not part of the application catalog.", "saas_feature_code_unknown");
        if (command.ReferencePriceCents < 0)
            throw Error("Reference price cannot be negative.", "saas_feature_price_invalid");

        var item = command.Id.HasValue
            ? await dbContext.SaasFeatures.SingleOrDefaultAsync(value => value.Id == command.Id, cancellationToken)
            : await dbContext.SaasFeatures.SingleOrDefaultAsync(value => value.Code == code, cancellationToken);
        if (item is null)
        {
            item = new SaasFeature { Code = code };
            dbContext.SaasFeatures.Add(item);
        }

        if (item.IsCore)
            throw Error("Core features cannot be changed through the product catalog.", "saas_core_feature_locked");

        item.Code = code;
        item.Name = Required(command.Name, "name");
        item.Category = Required(command.Category, "category");
        item.Description = Clean(command.Description);
        item.ReferencePriceCents = command.ReferencePriceCents;
        item.Currency = NormalizeCurrency(command.Currency);
        item.Status = command.Status;
        item.SortOrder = command.SortOrder;
        await dbContext.SaveChangesAsync(cancellationToken);
        await AuditAsync(actor, "platform.saas.feature.upserted", "saas_features", item.Id,
            new { item.Code, item.Status }, cancellationToken);
        return item;
    }

    public async Task<SaasOffering> UpsertOfferingAsync(
        SaasCatalogActor actor,
        UpsertSaasOfferingCommand command,
        CancellationToken cancellationToken = default)
    {
        var code = Normalize(command.Code);
        var item = command.Id.HasValue
            ? await dbContext.SaasOfferings.SingleOrDefaultAsync(value => value.Id == command.Id, cancellationToken)
            : await dbContext.SaasOfferings.SingleOrDefaultAsync(value => value.Code == code, cancellationToken);
        if (item is null)
        {
            item = new SaasOffering { Code = code };
            dbContext.SaasOfferings.Add(item);
        }
        else if (item.Type != command.Type &&
                 await dbContext.SaasOfferingVersions.AnyAsync(value => value.OfferingId == item.Id, cancellationToken))
        {
            throw Error("Offering type cannot change after versions exist.", "saas_offering_type_locked");
        }

        item.Code = code;
        item.Name = Required(command.Name, "name");
        item.Type = command.Type;
        item.Status = command.Status;
        item.Description = Clean(command.Description);
        item.SortOrder = command.SortOrder;
        await dbContext.SaveChangesAsync(cancellationToken);
        await AuditAsync(actor, "platform.saas.offering.upserted", "saas_offerings", item.Id,
            new { item.Code, item.Type, item.Status }, cancellationToken);
        return item;
    }

    public async Task<SaasOfferingVersionItem> UpsertDraftVersionAsync(
        SaasCatalogActor actor,
        UpsertSaasOfferingVersionCommand command,
        CancellationToken cancellationToken = default)
    {
        var offering =
            await dbContext.SaasOfferings.SingleOrDefaultAsync(value => value.Id == command.OfferingId,
                cancellationToken)
            ?? throw Error("SaaS offering was not found.", "saas_offering_not_found");
        if (command.AmountCents < 0 || command.OriginalAmountCents < command.AmountCents)
            throw Error("Offering version price is invalid.", "saas_offering_price_invalid");

        var featureCodes = command.FeatureCodes.Select(Normalize).Distinct(StringComparer.Ordinal)
            .Order(StringComparer.Ordinal).ToArray();
        if (offering.Type == SaasOfferingType.BasePlan && featureCodes.Length == 0)
            throw Error("A base plan must contain at least one sellable feature.", "saas_base_plan_features_required");
        var validFeatures = await dbContext.SaasFeatures.CountAsync(value =>
                featureCodes.Contains(value.Code) && value.Status == SaasFeatureStatus.Active && !value.IsCore,
            cancellationToken);
        if (validFeatures != featureCodes.Length)
            throw Error("One or more features are unavailable.", "saas_feature_unavailable");

        var metricCodes = command.Limits.Keys.Select(Normalize).Distinct(StringComparer.Ordinal).ToArray();
        var limitDefinitions = await dbContext.SaasFeatureLimitDefinitions.AsNoTracking()
            .Where(value => metricCodes.Contains(value.MetricCode))
            .ToArrayAsync(cancellationToken);
        if (command.Limits.Any(value => value.Value < 0) ||
            limitDefinitions.Length != metricCodes.Length ||
            limitDefinitions.Any(value => !featureCodes.Contains(value.FeatureCode)))
            throw Error("One or more feature limits are invalid.", "saas_feature_limit_invalid");

        SaasOfferingVersion version;
        if (command.Id.HasValue)
        {
            version = await dbContext.SaasOfferingVersions.SingleOrDefaultAsync(value => value.Id == command.Id,
                          cancellationToken)
                      ?? throw Error("Offering version was not found.", "saas_offering_version_not_found");
            if (version.Status != SaasOfferingVersionStatus.Draft)
                throw Error("Published offering versions are immutable.", "saas_offering_version_immutable");
            if (version.OfferingId != command.OfferingId)
                throw Error("Offering version cannot move to another offering.",
                    "saas_offering_version_offering_locked");
        }
        else
        {
            var nextVersion = (await dbContext.SaasOfferingVersions
                .Where(value => value.OfferingId == command.OfferingId)
                .MaxAsync(value => (int?)value.Version, cancellationToken) ?? 0) + 1;
            version = new SaasOfferingVersion { OfferingId = command.OfferingId, Version = nextVersion };
            dbContext.SaasOfferingVersions.Add(version);
        }

        version.BillingCycle = command.BillingCycle;
        version.OriginalAmountCents = command.OriginalAmountCents;
        version.AmountCents = command.AmountCents;
        version.Currency = NormalizeCurrency(command.Currency);
        version.EffectiveAt = command.EffectiveAt;
        version.Metadata = ObjectOrEmpty(command.Metadata);

        await dbContext.SaasOfferingVersionFeatures.Where(value => value.OfferingVersionId == version.Id)
            .ExecuteDeleteAsync(cancellationToken);
        await dbContext.SaasOfferingVersionLimits.Where(value => value.OfferingVersionId == version.Id)
            .ExecuteDeleteAsync(cancellationToken);
        dbContext.SaasOfferingVersionFeatures.AddRange(featureCodes.Select(code => new SaasOfferingVersionFeature
        {
            OfferingVersionId = version.Id,
            FeatureCode = code
        }));
        dbContext.SaasOfferingVersionLimits.AddRange(command.Limits.Select(value => new SaasOfferingVersionLimit
        {
            OfferingVersionId = version.Id,
            MetricCode = Normalize(value.Key),
            LimitValue = value.Value
        }));
        await dbContext.SaveChangesAsync(cancellationToken);
        await AuditAsync(actor, "platform.saas.offering_version.saved", "saas_offering_versions", version.Id,
            new { offering.Code, version.Version }, cancellationToken);
        return (await LoadVersionsAsync(version.Id, cancellationToken)).Single();
    }

    public async Task<SaasOfferingVersionItem> PublishVersionAsync(
        SaasCatalogActor actor,
        Guid versionId,
        CancellationToken cancellationToken = default)
    {
        var version = await RequireDraftAsync(versionId, cancellationToken);
        var hasFeatures =
            await dbContext.SaasOfferingVersionFeatures.AnyAsync(value => value.OfferingVersionId == versionId,
                cancellationToken);
        if (!hasFeatures) throw Error("Offering version has no features.", "saas_offering_version_empty");

        version.Status = SaasOfferingVersionStatus.Published;
        version.PublishedAt = DateTimeOffset.UtcNow;
        version.EffectiveAt ??= version.PublishedAt;
        var offering =
            await dbContext.SaasOfferings.SingleAsync(value => value.Id == version.OfferingId, cancellationToken);
        offering.Status = SaasOfferingStatus.Active;
        await dbContext.SaveChangesAsync(cancellationToken);
        await AuditAsync(actor, "platform.saas.offering_version.published", "saas_offering_versions", version.Id,
            new { version.Version }, cancellationToken);
        return (await LoadVersionsAsync(version.Id, cancellationToken)).Single();
    }

    public async Task<SaasOfferingVersionItem> CloneVersionAsync(
        SaasCatalogActor actor,
        Guid versionId,
        CancellationToken cancellationToken = default)
    {
        var source = await dbContext.SaasOfferingVersions.AsNoTracking()
                         .SingleOrDefaultAsync(value => value.Id == versionId, cancellationToken)
                     ?? throw Error("Offering version was not found.", "saas_offering_version_not_found");
        var features = await dbContext.SaasOfferingVersionFeatures.AsNoTracking()
            .Where(value => value.OfferingVersionId == versionId).Select(value => value.FeatureCode)
            .ToArrayAsync(cancellationToken);
        var limits = await dbContext.SaasOfferingVersionLimits.AsNoTracking()
            .Where(value => value.OfferingVersionId == versionId).ToDictionaryAsync(value => value.MetricCode,
                value => value.LimitValue, cancellationToken);
        return await UpsertDraftVersionAsync(actor, new UpsertSaasOfferingVersionCommand(
            null,
            source.OfferingId,
            source.BillingCycle,
            source.OriginalAmountCents,
            source.AmountCents,
            source.Currency,
            null,
            features,
            limits,
            source.Metadata), cancellationToken);
    }

    public async Task<SaasOfferingVersionItem> RetireVersionAsync(
        SaasCatalogActor actor,
        Guid versionId,
        CancellationToken cancellationToken = default)
    {
        var version =
            await dbContext.SaasOfferingVersions.SingleOrDefaultAsync(value => value.Id == versionId, cancellationToken)
            ?? throw Error("Offering version was not found.", "saas_offering_version_not_found");
        if (version.Status != SaasOfferingVersionStatus.Published)
            throw Error("Only a published offering version can be retired.", "saas_offering_version_status_invalid");
        version.Status = SaasOfferingVersionStatus.Retired;
        version.RetiredAt = DateTimeOffset.UtcNow;
        await dbContext.SaveChangesAsync(cancellationToken);
        await AuditAsync(actor, "platform.saas.offering_version.retired", "saas_offering_versions", version.Id,
            new { version.Version }, cancellationToken);
        return (await LoadVersionsAsync(version.Id, cancellationToken)).Single();
    }

    private async Task<SaasOfferingVersionItem[]> LoadVersionsAsync(Guid? versionId,
        CancellationToken cancellationToken)
    {
        var query = from version in dbContext.SaasOfferingVersions.AsNoTracking()
            join offering in dbContext.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
            select new { Version = version, Offering = offering };
        if (versionId.HasValue) query = query.Where(value => value.Version.Id == versionId);
        var rows = await query.OrderBy(value => value.Offering.SortOrder).ThenBy(value => value.Offering.Code)
            .ThenByDescending(value => value.Version.Version).ToArrayAsync(cancellationToken);
        var ids = rows.Select(value => value.Version.Id).ToArray();
        var features = await dbContext.SaasOfferingVersionFeatures.AsNoTracking()
            .Where(value => ids.Contains(value.OfferingVersionId)).ToArrayAsync(cancellationToken);
        var limits = await dbContext.SaasOfferingVersionLimits.AsNoTracking()
            .Where(value => ids.Contains(value.OfferingVersionId)).ToArrayAsync(cancellationToken);
        return rows.Select(row => new SaasOfferingVersionItem(
                row.Version.Id,
                row.Offering.Id,
                row.Offering.Code,
                row.Offering.Name,
                row.Offering.Type,
                row.Version.Version,
                row.Version.Status,
                row.Version.BillingCycle,
                row.Version.OriginalAmountCents,
                row.Version.AmountCents,
                row.Version.Currency,
                row.Version.EffectiveAt,
                row.Version.PublishedAt,
                features.Where(value => value.OfferingVersionId == row.Version.Id).Select(value => value.FeatureCode)
                    .Order(StringComparer.Ordinal).ToArray(),
                limits.Where(value => value.OfferingVersionId == row.Version.Id).ToDictionary(value => value.MetricCode,
                    value => value.LimitValue, StringComparer.Ordinal)))
            .ToArray();
    }

    private async Task<SaasOfferingVersion> RequireDraftAsync(Guid versionId, CancellationToken cancellationToken)
    {
        var version =
            await dbContext.SaasOfferingVersions.SingleOrDefaultAsync(value => value.Id == versionId, cancellationToken)
            ?? throw Error("Offering version was not found.", "saas_offering_version_not_found");
        return version.Status == SaasOfferingVersionStatus.Draft
            ? version
            : throw Error("Published offering versions are immutable.", "saas_offering_version_immutable");
    }

    private Task AuditAsync(SaasCatalogActor actor, string action, string targetType, Guid targetId, object details,
        CancellationToken cancellationToken)
    {
        return auditService.WriteAsync(new BackofficeOperationAuditCommand(
            null,
            actor.UserId,
            action,
            targetType,
            targetId.ToString(),
            JsonSerializer.SerializeToElement(details)), cancellationToken);
    }

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

    private static string Normalize(string value)
    {
        return Required(value, "code").ToLowerInvariant();
    }

    private static string NormalizeCurrency(string value)
    {
        return Required(value, "currency").ToUpperInvariant();
    }

    private static string Required(string value, string field)
    {
        return string.IsNullOrWhiteSpace(value) ? throw Error($"{field} is required.", "required_field") : value.Trim();
    }

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

    private static JsonElement ObjectOrEmpty(JsonElement value)
    {
        return value.ValueKind == JsonValueKind.Object ? value.Clone() : JsonDocument.Parse("{}").RootElement.Clone();
    }
}