using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Tiku.Application.Tenancy;
using Tiku.Application.Security;
using Tiku.Domain.Common;
using Tiku.Domain.Platform;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Tenancy;

public sealed class TenantFrontendConfigService(
    TikuDbContext dbContext,
    IMemoryCache cache,
    IFeatureAccessService featureAccessService,
    ITenantPublicCacheInvalidator publicCacheInvalidator) : ITenantFrontendConfigService
{
    private static readonly TimeSpan RuntimeCacheDuration = TimeSpan.FromMinutes(2);

    public async Task<TenantFrontendConfigItem> GetAsync(
        Guid tenantId,
        CancellationToken cancellationToken = default)
    {
        var config = await dbContext.TenantFrontendConfigs.AsNoTracking()
            .SingleOrDefaultAsync(item => item.TenantId == tenantId, cancellationToken);
        return ToItem(config ?? CreateDefault(tenantId));
    }

    public async Task<TenantFrontendConfigItem> SaveDraftAsync(
        Guid tenantId,
        TenantFrontendConfigDraft draft,
        CancellationToken cancellationToken = default)
    {
        Validate(draft);
        var config = await dbContext.TenantFrontendConfigs
            .SingleOrDefaultAsync(item => item.TenantId == tenantId, cancellationToken);
        if (config is null)
        {
            config = CreateDefault(tenantId);
            dbContext.TenantFrontendConfigs.Add(config);
        }

        config.DraftBranding = draft.Branding.Clone();
        config.DraftTheme = draft.Theme.Clone();
        config.DraftFeatures = draft.Features.Clone();
        config.DraftNavigation = draft.Navigation.Clone();
        config.DraftHomeModules = draft.HomeModules.Clone();
        await dbContext.SaveChangesAsync(cancellationToken);
        return ToItem(config);
    }

    public async Task<TenantFrontendConfigItem> PublishAsync(
        Guid tenantId,
        int expectedVersion,
        CancellationToken cancellationToken = default)
    {
        var config = await dbContext.TenantFrontendConfigs
            .SingleOrDefaultAsync(item => item.TenantId == tenantId, cancellationToken)
            ?? throw new TenantFrontendConfigException(
                "frontend_config_not_found",
                "Save a frontend configuration draft before publishing.");
        if (config.ConfigVersion != expectedVersion)
        {
            throw new TenantFrontendConfigException(
                "frontend_config_version_conflict",
                "Frontend configuration has changed; reload it before publishing.");
        }

        var draft = Draft(config);
        Validate(draft);
        config.PublishedBranding = config.DraftBranding.Clone();
        config.PublishedTheme = config.DraftTheme.Clone();
        config.PublishedFeatures = config.DraftFeatures.Clone();
        config.PublishedNavigation = config.DraftNavigation.Clone();
        config.PublishedHomeModules = config.DraftHomeModules.Clone();
        config.ConfigVersion++;
        config.PublishedAt = DateTimeOffset.UtcNow;
        await dbContext.SaveChangesAsync(cancellationToken);
        cache.Remove(CacheKey(tenantId));
        await publicCacheInvalidator.InvalidateAsync(tenantId, cancellationToken);
        return ToItem(config);
    }

    public async Task<TenantRuntimeBootstrap> GetRuntimeAsync(
        Guid tenantId,
        CancellationToken cancellationToken = default)
    {
        if (cache.TryGetValue<TenantRuntimeBootstrap>(CacheKey(tenantId), out var cached) && cached is not null)
        {
            return cached;
        }

        var tenant = await dbContext.Tenants.AsNoTracking().SingleOrDefaultAsync(
            item => item.Id == tenantId && item.Status == TenantStatus.Active,
            cancellationToken)
            ?? throw new TenantFrontendConfigException("tenant_not_found", "Active tenant was not found.");
        var now = DateTimeOffset.UtcNow;
        var hasActiveSubscription = await dbContext.TenantSaasSubscriptions.AsNoTracking().AnyAsync(item =>
            item.TenantId == tenantId &&
            (item.Status == TenantSaasSubscriptionStatus.Trial || item.Status == TenantSaasSubscriptionStatus.Active) &&
            item.StartsAt <= now && item.CurrentPeriodEnd > now,
            cancellationToken);
        if (!hasActiveSubscription)
        {
            throw new TenantFrontendConfigException("subscription_inactive", "An active tenant subscription is required.");
        }
        var config = await dbContext.TenantFrontendConfigs.AsNoTracking()
            .SingleOrDefaultAsync(item => item.TenantId == tenantId, cancellationToken)
            ?? CreateDefault(tenantId);
        var result = new TenantRuntimeBootstrap(
            config.SchemaVersion,
            config.ConfigVersion,
            tenant.Slug,
            tenant.Name,
            config.PublishedBranding.Clone(),
            config.PublishedTheme.Clone(),
            config.PublishedFeatures.Clone(),
            config.PublishedNavigation.Clone(),
            config.PublishedHomeModules.Clone(),
            (await featureAccessService.GetEnabledFeaturesAsync(tenantId, FeatureAccessOperation.Read, cancellationToken))
                .Where(code => code != SaasFeatureCatalog.CoreBackoffice)
                .Order(StringComparer.Ordinal)
                .ToArray(),
            (await dbContext.TenantAuthPolicies.AsNoTracking()
                .Where(item => item.TenantId == tenantId)
                .Select(item => item.AllowedStudentLoginMethods)
                .SingleOrDefaultAsync(cancellationToken) ?? ["password"])
                .Select(value => value.Trim().ToLowerInvariant())
                .Where(value => value.Length > 0)
                .Distinct(StringComparer.Ordinal)
                .Order(StringComparer.Ordinal)
                .ToArray());
        cache.Set(CacheKey(tenantId), result, RuntimeCacheDuration);
        return result;
    }

    private static TenantFrontendConfig CreateDefault(Guid tenantId)
    {
        return new TenantFrontendConfig
        {
            TenantId = tenantId,
            SchemaVersion = 1,
            ConfigVersion = 1
        };
    }

    private static TenantFrontendConfigItem ToItem(TenantFrontendConfig config)
    {
        return new TenantFrontendConfigItem(
            config.SchemaVersion,
            config.ConfigVersion,
            new TenantFrontendConfigDraft(
                config.PublishedBranding.Clone(),
                config.PublishedTheme.Clone(),
                config.PublishedFeatures.Clone(),
                config.PublishedNavigation.Clone(),
                config.PublishedHomeModules.Clone()),
            Draft(config),
            config.PublishedAt);
    }

    private static TenantFrontendConfigDraft Draft(TenantFrontendConfig config)
    {
        return new TenantFrontendConfigDraft(
            config.DraftBranding.Clone(),
            config.DraftTheme.Clone(),
            config.DraftFeatures.Clone(),
            config.DraftNavigation.Clone(),
            config.DraftHomeModules.Clone());
    }

    private static void Validate(TenantFrontendConfigDraft draft)
    {
        RequireKind(draft.Branding, JsonValueKind.Object, "branding");
        RequireKind(draft.Theme, JsonValueKind.Object, "theme");
        RequireKind(draft.Features, JsonValueKind.Object, "features");
        RequireKind(draft.Navigation, JsonValueKind.Array, "navigation");
        RequireKind(draft.HomeModules, JsonValueKind.Array, "homeModules");

        foreach (var root in new[] { draft.Branding, draft.Theme, draft.Features, draft.Navigation, draft.HomeModules })
        {
            RejectUnsafeContent(root);
        }
    }

    private static void RequireKind(JsonElement value, JsonValueKind expected, string field)
    {
        if (value.ValueKind != expected)
        {
            throw new TenantFrontendConfigException(
                "frontend_config_invalid",
                $"Frontend configuration field '{field}' must be a JSON {expected.ToString().ToLowerInvariant()}.");
        }
    }

    private static void RejectUnsafeContent(JsonElement value)
    {
        if (value.ValueKind == JsonValueKind.Object)
        {
            foreach (var property in value.EnumerateObject())
            {
                if (property.Name.Contains("script", StringComparison.OrdinalIgnoreCase) ||
                    property.Name.Contains("html", StringComparison.OrdinalIgnoreCase))
                {
                    throw UnsafeConfig();
                }

                RejectUnsafeContent(property.Value);
            }
        }
        else if (value.ValueKind == JsonValueKind.Array)
        {
            foreach (var item in value.EnumerateArray())
            {
                RejectUnsafeContent(item);
            }
        }
        else if (value.ValueKind == JsonValueKind.String)
        {
            var text = value.GetString() ?? string.Empty;
            if (text.Contains("<script", StringComparison.OrdinalIgnoreCase) ||
                text.Contains("javascript:", StringComparison.OrdinalIgnoreCase))
            {
                throw UnsafeConfig();
            }
        }
    }

    private static TenantFrontendConfigException UnsafeConfig() => new(
        "frontend_config_unsafe",
        "Frontend configuration cannot contain HTML or executable scripts.");

    private static string CacheKey(Guid tenantId) => $"tenant-runtime:{tenantId:N}";
}
