using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Quartz;
using OTSSignsOrchestrator.Server.Clients;
using OTSSignsOrchestrator.Server.Data;
using OTSSignsOrchestrator.Server.Data.Entities;
using OTSSignsOrchestrator.Server.Hubs;
using OTSSignsOrchestrator.Server.Jobs;

namespace OTSSignsOrchestrator.Server.Workers;

/// <summary>
/// BYOI SAML provisioning pipeline — configures an upstream SAML source in Authentik
/// so a Pro-tier customer can federate their own Identity Provider.
///
/// Handles <c>JobType = "provision-byoi"</c>.
///
/// Steps:
///   1. import-cert          — Import the customer's public cert PEM into Authentik
///   2. create-saml-source   — Create the upstream SAML source in Authentik
///   3. store-metadata       — Persist slug, entity_id, sso_url, cert info to ByoiConfig
///   4. schedule-cert-monitor — Register a Quartz ByoiCertExpiryJob for daily cert expiry checks
///
/// IMPORTANT: Xibo's settings-custom.php is IDENTICAL for Pro BYOI and Essentials.
/// Xibo always authenticates through Authentik. BYOI is entirely handled within
/// Authentik's upstream SAML Source config — no Xibo changes are needed.
/// </summary>
public sealed class ByoiSamlPipeline : IProvisioningPipeline
{
    public string HandlesJobType => "provision-byoi";

    private const int TotalSteps = 4;

    private readonly IServiceProvider _services;
    private readonly ILogger<ByoiSamlPipeline> _logger;

    public ByoiSamlPipeline(
        IServiceProvider services,
        ILogger<ByoiSamlPipeline> logger)
    {
        _services = services;
        _logger = logger;
    }

    public async Task ExecuteAsync(Job job, CancellationToken ct)
    {
        await using var scope = _services.CreateAsyncScope();
        var db = scope.ServiceProvider.GetRequiredService<OrchestratorDbContext>();
        var hub = scope.ServiceProvider.GetRequiredService<IHubContext<FleetHub, IFleetClient>>();
        var authentikClient = scope.ServiceProvider.GetRequiredService<IAuthentikClient>();
        var authentikOpts = scope.ServiceProvider.GetRequiredService<IOptions<AuthentikOptions>>().Value;
        var schedulerFactory = scope.ServiceProvider.GetRequiredService<ISchedulerFactory>();

        // Load customer + instance
        var customer = await db.Customers
            .Include(c => c.Instances)
            .FirstOrDefaultAsync(c => c.Id == job.CustomerId, ct)
            ?? throw new InvalidOperationException($"Customer {job.CustomerId} not found.");

        if (customer.Plan != CustomerPlan.Pro)
            throw new InvalidOperationException("BYOI SAML is only available for Pro tier customers.");

        var instance = customer.Instances.FirstOrDefault()
            ?? throw new InvalidOperationException($"No instance found for customer {job.CustomerId}.");

        var abbrev = customer.Abbreviation;
        var companyName = customer.CompanyName;

        // Parse BYOI parameters from Job.Parameters JSON
        var parms = JsonSerializer.Deserialize<ByoiParameters>(
            job.Parameters ?? throw new InvalidOperationException("Job.Parameters is null."),
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
            ?? throw new InvalidOperationException("Failed to deserialize BYOI parameters.");

        var runner = new StepRunner(db, hub, _logger, job.Id, TotalSteps);

        // Mutable state accumulated across steps
        string? verificationKpId = null;
        string slug = $"byoi-{abbrev}";
        DateTime certExpiry;

        // Parse the cert up front to get expiry and validate
        using var cert = X509CertificateLoader.LoadCertificate(
            Convert.FromBase64String(ExtractBase64FromPem(parms.CustomerCertPem)));
        certExpiry = cert.NotAfter.ToUniversalTime();

        // ── Step 1: import-cert ─────────────────────────────────────────────
        await runner.RunAsync("import-cert", async () =>
        {
            var response = await authentikClient.ImportCertificateAsync(new ImportCertRequest(
                Name: $"byoi-{abbrev}-cert",
                CertificateData: parms.CustomerCertPem,
                KeyData: null)); // PUBLIC cert only — no private key

            if (!response.IsSuccessStatusCode || response.Content is null)
                throw new InvalidOperationException(
                    $"Failed to import certificate: {response.Error?.Content ?? response.ReasonPhrase}");

            verificationKpId = response.Content["pk"]?.ToString()
                ?? throw new InvalidOperationException("Authentik cert import returned no pk.");

            return $"Imported customer cert as keypair '{verificationKpId}'.";
        }, ct);

        // ── Step 2: create-saml-source ──────────────────────────────────────
        await runner.RunAsync("create-saml-source", async () =>
        {
            var response = await authentikClient.CreateSamlSourceAsync(new CreateSamlSourceRequest(
                Name: $"{companyName} IdP",
                Slug: slug,
                SsoUrl: parms.CustomerSsoUrl,
                SloUrl: parms.CustomerSloUrl,
                Issuer: parms.CustomerIdpEntityId,
                SigningKp: string.IsNullOrWhiteSpace(authentikOpts.OtsSigningKpId) ? null : authentikOpts.OtsSigningKpId,
                VerificationKp: verificationKpId,
                BindingType: "redirect",
                NameIdPolicy: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
                PreAuthenticationFlow: authentikOpts.SourcePreAuthFlowSlug,
                AuthenticationFlow: authentikOpts.SourceAuthFlowSlug,
                AllowIdpInitiated: false)); // REQUIRED: IdP-initiated SSO is a CSRF risk

            if (!response.IsSuccessStatusCode || response.Content is null)
                throw new InvalidOperationException(
                    $"Failed to create SAML source: {response.Error?.Content ?? response.ReasonPhrase}");

            return $"SAML source '{slug}' created with AllowIdpInitiated=false.";
        }, ct);

        // ── Step 3: store-metadata ──────────────────────────────────────────
        await runner.RunAsync("store-metadata", async () =>
        {
            var byoiConfig = new ByoiConfig
            {
                Id = Guid.NewGuid(),
                InstanceId = instance.Id,
                Slug = slug,
                EntityId = parms.CustomerIdpEntityId,
                SsoUrl = parms.CustomerSsoUrl,
                CertPem = parms.CustomerCertPem,
                CertExpiry = certExpiry,
                Enabled = true,
                CreatedAt = DateTime.UtcNow,
            };

            db.ByoiConfigs.Add(byoiConfig);
            await db.SaveChangesAsync(ct);

            return $"ByoiConfig stored: slug={slug}, certExpiry={certExpiry:O}.";
        }, ct);

        // ── Step 4: schedule-cert-monitor ───────────────────────────────────
        await runner.RunAsync("schedule-cert-monitor", async () =>
        {
            var scheduler = await schedulerFactory.GetScheduler(ct);
            var jobKey = new JobKey($"byoi-cert-expiry-{abbrev}", "byoi-cert-expiry");

            // Only schedule if not already present (idempotent)
            if (!await scheduler.CheckExists(jobKey, ct))
            {
                var quartzJob = JobBuilder.Create<ByoiCertExpiryJob>()
                    .WithIdentity(jobKey)
                    .UsingJobData("instanceId", instance.Id.ToString())
                    .Build();

                var trigger = TriggerBuilder.Create()
                    .WithIdentity($"byoi-cert-expiry-{abbrev}-trigger", "byoi-cert-expiry")
                    .WithDailyTimeIntervalSchedule(s => s
                        .OnEveryDay()
                        .StartingDailyAt(TimeOfDay.HourAndMinuteOfDay(6, 0))
                        .WithRepeatCount(1))
                    .StartNow()
                    .Build();

                await scheduler.ScheduleJob(quartzJob, trigger, ct);
            }

            return $"Quartz ByoiCertExpiryJob scheduled daily for instance {instance.Id}.";
        }, ct);
    }

    /// <summary>
    /// Extracts the Base64 payload from a PEM string, stripping headers/footers.
    /// </summary>
    private static string ExtractBase64FromPem(string pem)
    {
        return pem
            .Replace("-----BEGIN CERTIFICATE-----", "")
            .Replace("-----END CERTIFICATE-----", "")
            .Replace("\r", "")
            .Replace("\n", "")
            .Trim();
    }
}

/// <summary>
/// Deserialized from Job.Parameters JSON for provision-byoi jobs.
/// </summary>
public sealed record ByoiParameters
{
    public string CustomerCertPem { get; init; } = string.Empty;
    public string CustomerSsoUrl { get; init; } = string.Empty;
    public string CustomerIdpEntityId { get; init; } = string.Empty;
    public string? CustomerSloUrl { get; init; }
}
