using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using OTSSignsOrchestrator.Core.Services;
using OTSSignsOrchestrator.Server.Clients;
using OTSSignsOrchestrator.Server.Data;
using OTSSignsOrchestrator.Server.Data.Entities;
using OTSSignsOrchestrator.Server.Hubs;

namespace OTSSignsOrchestrator.Server.Workers;

/// <summary>
/// OAuth2 credential rotation pipeline — deletes the old Xibo OAuth app, creates a new one,
/// stores the new credentials, and verifies access. Handles <c>JobType = "rotate-oauth2"</c>.
///
/// CRITICAL: OAuth2 clientId CHANGES on rotation — there is no in-place secret refresh.
/// The secret is returned ONLY in the <c>POST /api/application</c> response.
///
/// Steps:
///   1. delete-old-app    — Delete current OAuth2 application via Xibo API
///   2. create-new-app    — POST /api/application — IMMEDIATELY capture secret
///   3. store-credentials — Update Bitwarden + OauthAppRegistry with new clientId
///   4. verify-access     — Test new credentials via client_credentials flow
///   5. audit-log         — Write AuditLog (or CRITICAL error with emergency creds if steps 3-4 fail)
/// </summary>
public sealed class RotateCredentialsPipeline : IProvisioningPipeline
{
    public string HandlesJobType => "rotate-oauth2";

    private const int TotalSteps = 5;

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

    public RotateCredentialsPipeline(
        IServiceProvider services,
        ILogger<RotateCredentialsPipeline> 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 xiboFactory = scope.ServiceProvider.GetRequiredService<XiboClientFactory>();
        var bws = scope.ServiceProvider.GetRequiredService<IBitwardenSecretService>();
        var settings = scope.ServiceProvider.GetRequiredService<SettingsService>();

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

        var abbrev = ctx.Abbreviation;

        // Load current OAuth2 credentials
        var currentReg = await db.OauthAppRegistries
            .Where(r => r.InstanceId == ctx.InstanceId)
            .OrderByDescending(r => r.CreatedAt)
            .FirstOrDefaultAsync(ct)
            ?? throw new InvalidOperationException(
                $"No OauthAppRegistry found for instance {ctx.InstanceId}. Cannot rotate.");

        var currentSecret = await settings.GetAsync(SettingsService.InstanceOAuthSecretId(abbrev))
            ?? throw new InvalidOperationException(
                $"OAuth secret not found in settings for instance '{abbrev}'. Cannot rotate.");

        var currentClientId = currentReg.ClientId;

        // Get a Xibo API client using the current (about-to-be-deleted) credentials
        var xiboClient = await xiboFactory.CreateAsync(ctx.InstanceUrl, currentClientId, currentSecret);

        // Mutable state — captured across steps
        string newClientId = string.Empty;
        string newSecret = string.Empty;

        // ── Step 1: delete-old-app ──────────────────────────────────────────
        await runner.RunAsync("delete-old-app", async () =>
        {
            await xiboClient.DeleteApplicationAsync(currentClientId);
            return $"Old OAuth2 application deleted. ClientId={currentClientId}.";
        }, ct);

        // ── Step 2: create-new-app ──────────────────────────────────────────
        // CRITICAL: After this step, the old credentials are gone. If subsequent steps fail,
        // the new clientId + secret MUST be logged for manual recovery.
        await runner.RunAsync("create-new-app", async () =>
        {
            // Need a client with some auth to create the new app. Use the instance's bootstrap
            // admin credentials (ots-admin user) via direct login if available, or we can
            // re-authenticate using the base URL. Since the old app is deleted, we need a
            // different auth mechanism. The Xibo CMS allows creating apps as any authenticated
            // super-admin. Use the ots-admin password from Bitwarden.
            //
            // However, the simplest path: the DELETE above invalidated the old token, but the
            // Xibo CMS still has the ots-admin and ots-svc users. We stored the admin password
            // in Bitwarden. Retrieve it and create a new factory client.
            var adminPassBwsId = await settings.GetAsync(SettingsService.InstanceAdminPasswordSecretId(abbrev));
            if (string.IsNullOrEmpty(adminPassBwsId))
                throw new InvalidOperationException(
                    $"Admin password Bitwarden secret ID not found for '{abbrev}'. Cannot create new OAuth app.");

            var adminPassSecret = await bws.GetSecretAsync(adminPassBwsId);
            var bootstrapClientId = await settings.GetAsync(SettingsService.XiboBootstrapClientId)
                ?? throw new InvalidOperationException("Bootstrap OAuth client ID not configured.");
            var bootstrapClientSecret = await settings.GetAsync(SettingsService.XiboBootstrapClientSecret)
                ?? throw new InvalidOperationException("Bootstrap OAuth client secret not configured.");

            // Re-create client using bootstrap credentials
            var bootstrapClient = await xiboFactory.CreateAsync(ctx.InstanceUrl, bootstrapClientId, bootstrapClientSecret);

            var appName = $"OTS Orchestrator — {abbrev.ToUpperInvariant()}";
            var response = await bootstrapClient.CreateApplicationAsync(new CreateApplicationRequest(appName));

            EnsureSuccess(response);
            var data = response.Content
                ?? throw new InvalidOperationException("CreateApplication returned empty response.");

            // CRITICAL: Capture secret IMMEDIATELY — it is returned ONLY in this response.
            newClientId = data["key"]?.ToString()
                ?? throw new InvalidOperationException("OAuth application response missing 'key'.");
            newSecret = data["secret"]?.ToString()
                ?? throw new InvalidOperationException("OAuth application response missing 'secret'.");

            return $"New OAuth2 application created. ClientId={newClientId}. Secret captured (stored in next step).";
        }, ct);

        // Steps 3-5: if ANY of these fail after step 2, we must log the credentials for recovery.
        // The old app is already deleted — there is no rollback path.
        try
        {
            // ── Step 3: store-credentials ───────────────────────────────────
            await runner.RunAsync("store-credentials", async () =>
            {
                // Update Bitwarden with new secret
                var existingBwsId = await settings.GetAsync(SettingsService.InstanceOAuthSecretId(abbrev));
                if (!string.IsNullOrEmpty(existingBwsId))
                {
                    await bws.UpdateSecretAsync(
                        existingBwsId,
                        $"{abbrev}/xibo-oauth-secret",
                        newSecret,
                        $"Xibo CMS OAuth2 client secret (rotated). ClientId: {newClientId}");
                }
                else
                {
                    await bws.CreateInstanceSecretAsync(
                        key: $"{abbrev}/xibo-oauth-secret",
                        value: newSecret,
                        note: $"Xibo CMS OAuth2 client secret (rotated). ClientId: {newClientId}");
                }

                // Update OauthAppRegistry with new clientId
                db.OauthAppRegistries.Add(new OauthAppRegistry
                {
                    Id = Guid.NewGuid(),
                    InstanceId = ctx.InstanceId,
                    ClientId = newClientId,
                    CreatedAt = DateTime.UtcNow,
                });

                // Also update the settings cache with the new client ID
                await settings.SetAsync(
                    SettingsService.InstanceOAuthClientId(abbrev),
                    newClientId,
                    SettingsService.CatInstance,
                    isSensitive: false);

                await db.SaveChangesAsync(ct);
                return $"Credentials stored. ClientId={newClientId} in OauthAppRegistry + Bitwarden.";
            }, ct);

            // ── Step 4: verify-access ───────────────────────────────────────
            await runner.RunAsync("verify-access", async () =>
            {
                var verifyClient = await xiboFactory.CreateAsync(ctx.InstanceUrl, newClientId, newSecret);
                var aboutResp = await verifyClient.GetAboutAsync();
                EnsureSuccess(aboutResp);
                return $"New credentials verified. GET /about succeeded on {ctx.InstanceUrl}.";
            }, ct);

            // ── Step 5: audit-log ───────────────────────────────────────────
            await runner.RunAsync("audit-log", async () =>
            {
                db.AuditLogs.Add(new AuditLog
                {
                    Id = Guid.NewGuid(),
                    InstanceId = ctx.InstanceId,
                    Actor = "system/credential-rotation",
                    Action = "rotate-oauth2",
                    Target = $"xibo-{abbrev}",
                    Outcome = "success",
                    Detail = $"OAuth2 credentials rotated. Old clientId={currentClientId} → new clientId={newClientId}. Job {job.Id}.",
                    OccurredAt = DateTime.UtcNow,
                });
                await db.SaveChangesAsync(ct);

                return $"AuditLog entry written. OAuth2 rotation complete.";
            }, ct);
        }
        catch (Exception ex)
        {
            // CRITICAL: Steps 3-5 failed AFTER step 2 created the new app.
            // The old credentials are already deleted. Log new credentials for manual recovery.
            _logger.LogCritical(ex,
                "CRITICAL — OAuth2 rotation for '{Abbrev}' failed after new app creation. " +
                "Old clientId={OldClientId} (DELETED). " +
                "New clientId={NewClientId}, New secret={NewSecret}. " +
                "EMERGENCY RECOVERY DATA — store these credentials manually.",
                abbrev, currentClientId, newClientId, newSecret);

            // Also persist to a JobStep for operator visibility
            var emergencyStep = new JobStep
            {
                Id = Guid.NewGuid(),
                JobId = job.Id,
                StepName = "emergency-credential-log",
                Status = JobStepStatus.Failed,
                StartedAt = DateTime.UtcNow,
                CompletedAt = DateTime.UtcNow,
                LogOutput = $"CRITICAL EMERGENCY RECOVERY DATA — OAuth2 rotation partial failure. " +
                    $"Old clientId={currentClientId} (DELETED). " +
                    $"New clientId={newClientId}. New secret={newSecret}. " +
                    $"These credentials must be stored manually. Error: {ex.Message}",
            };
            db.JobSteps.Add(emergencyStep);
            await db.SaveChangesAsync(CancellationToken.None);

            throw; // Re-throw to fail the job
        }

        _logger.LogInformation(
            "RotateCredentialsPipeline completed for job {JobId} (abbrev={Abbrev}, newClientId={ClientId})",
            job.Id, abbrev, newClientId);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Helpers
    // ─────────────────────────────────────────────────────────────────────────

    private static async Task<PipelineContext> BuildContextAsync(Job job, OrchestratorDbContext db, CancellationToken ct)
    {
        var customer = await db.Customers
            .Include(c => c.Instances)
            .FirstOrDefaultAsync(c => c.Id == job.CustomerId, ct)
            ?? throw new InvalidOperationException($"Customer {job.CustomerId} not found for job {job.Id}.");

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

        var abbrev = customer.Abbreviation.ToLowerInvariant();

        return new PipelineContext
        {
            JobId = job.Id,
            CustomerId = customer.Id,
            InstanceId = instance.Id,
            Abbreviation = abbrev,
            CompanyName = customer.CompanyName,
            AdminEmail = customer.AdminEmail,
            AdminFirstName = customer.AdminFirstName,
            InstanceUrl = instance.XiboUrl,
            DockerStackName = instance.DockerStackName,
            ParametersJson = job.Parameters,
        };
    }

    private static void EnsureSuccess<T>(Refit.IApiResponse<T> response)
    {
        if (!response.IsSuccessStatusCode)
            throw new InvalidOperationException(
                $"API call failed with status {response.StatusCode}: {response.Error?.Content}");
    }
}
