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

namespace OTSSignsOrchestrator.Server.Webhooks;

public static class StripeWebhookHandler
{
    public static void MapStripeWebhook(this WebApplication app)
    {
        app.MapPost("/api/webhooks/stripe", HandleWebhook)
            .DisableRateLimiting();
    }

    private static async Task<IResult> HandleWebhook(
        HttpContext httpContext,
        IConfiguration config,
        OrchestratorDbContext db,
        AbbreviationService abbreviationService,
        EmailService emailService,
        IHubContext<FleetHub, IFleetClient> hub,
        ILogger<StripeEvent> logger)
    {
        // ── Verify signature ────────────────────────────────────────────────
        var json = await new StreamReader(httpContext.Request.Body).ReadToEndAsync();
        var webhookSecret = config["Stripe:WebhookSecret"]
            ?? throw new InvalidOperationException("Stripe:WebhookSecret not configured.");

        Event stripeEvent;
        try
        {
            stripeEvent = EventUtility.ConstructEvent(
                json,
                httpContext.Request.Headers["Stripe-Signature"],
                webhookSecret);
        }
        catch (StripeException ex)
        {
            logger.LogWarning("Stripe signature verification failed: {Error}", ex.Message);
            return Results.BadRequest("Invalid signature.");
        }

        // ── Idempotency check ───────────────────────────────────────────────
        if (await db.StripeEvents.AnyAsync(e => e.StripeEventId == stripeEvent.Id))
        {
            logger.LogInformation("Stripe event {EventId} already processed — skipping", stripeEvent.Id);
            return Results.Ok();
        }

        // ── Record event before processing ──────────────────────────────────
        db.StripeEvents.Add(new Data.Entities.StripeEvent
        {
            StripeEventId = stripeEvent.Id,
            EventType = stripeEvent.Type,
            ProcessedAt = DateTime.UtcNow,
            Payload = json,
        });
        await db.SaveChangesAsync();

        // ── Dispatch ────────────────────────────────────────────────────────
        logger.LogInformation("Processing Stripe event {EventId} type={EventType}", stripeEvent.Id, stripeEvent.Type);

        switch (stripeEvent.Type)
        {
            case EventTypes.CheckoutSessionCompleted:
                await HandleCheckoutSessionCompleted(stripeEvent, db, abbreviationService, hub, logger);
                break;

            case EventTypes.InvoicePaid:
                logger.LogInformation("Invoice paid: {EventId}", stripeEvent.Id);
                break;

            case EventTypes.InvoicePaymentFailed:
                await HandleInvoicePaymentFailed(stripeEvent, db, emailService, hub, logger);
                break;

            case EventTypes.CustomerSubscriptionUpdated:
                await HandleSubscriptionUpdated(stripeEvent, db, hub, logger);
                break;

            case EventTypes.CustomerSubscriptionDeleted:
                await HandleSubscriptionDeleted(stripeEvent, db, hub, logger);
                break;

            case "customer.subscription.trial_will_end":
                logger.LogInformation("Trial ending soon for event {EventId}", stripeEvent.Id);
                break;

            default:
                logger.LogInformation("Unhandled Stripe event type: {EventType}", stripeEvent.Type);
                break;
        }

        return Results.Ok();
    }

    // ── checkout.session.completed ──────────────────────────────────────────
    private static async Task HandleCheckoutSessionCompleted(
        Event stripeEvent,
        OrchestratorDbContext db,
        AbbreviationService abbreviationService,
        IHubContext<FleetHub, IFleetClient> hub,
        ILogger<StripeEvent> logger)
    {
        var session = stripeEvent.Data.Object as Stripe.Checkout.Session;
        if (session is null) return;

        var meta = session.Metadata;
        if (!meta.TryGetValue("ots_customer_id", out var customerIdStr) ||
            !Guid.TryParse(customerIdStr, out var customerId))
        {
            logger.LogWarning("checkout.session.completed missing ots_customer_id metadata");
            return;
        }

        var customer = await db.Customers.FindAsync(customerId);
        if (customer is null)
        {
            logger.LogWarning("Customer {CustomerId} not found for checkout session", customerId);
            return;
        }

        // Update customer from Stripe metadata
        customer.StripeCustomerId = session.CustomerId;
        customer.StripeSubscriptionId = session.SubscriptionId;
        customer.Status = CustomerStatus.Provisioning;

        if (meta.TryGetValue("company_name", out var cn)) customer.CompanyName = cn;
        if (meta.TryGetValue("admin_email", out var ae)) customer.AdminEmail = ae;
        if (meta.TryGetValue("admin_first_name", out var fn)) customer.AdminFirstName = fn;
        if (meta.TryGetValue("admin_last_name", out var ln)) customer.AdminLastName = ln;
        if (meta.TryGetValue("plan", out var planStr) && Enum.TryParse<CustomerPlan>(planStr, true, out var plan))
            customer.Plan = plan;
        if (meta.TryGetValue("screen_count", out var scStr) && int.TryParse(scStr, out var sc))
            customer.ScreenCount = sc;

        // Generate abbreviation
        var abbrev = await abbreviationService.GenerateAsync(customer.CompanyName);
        customer.Abbreviation = abbrev;

        // Create provisioning job
        var job = new Job
        {
            Id = Guid.NewGuid(),
            CustomerId = customer.Id,
            JobType = "provision",
            Status = JobStatus.Queued,
            TriggeredBy = "stripe-webhook",
            CreatedAt = DateTime.UtcNow,
        };
        db.Jobs.Add(job);
        await db.SaveChangesAsync();

        logger.LogInformation(
            "Checkout completed: customer={CustomerId}, abbrev={Abbrev}, job={JobId}",
            customer.Id, abbrev, job.Id);

        await hub.Clients.All.SendJobCreated(job.Id.ToString(), abbrev, "provision");
    }

    // ── invoice.payment_failed ──────────────────────────────────────────────
    private static async Task HandleInvoicePaymentFailed(
        Event stripeEvent,
        OrchestratorDbContext db,
        EmailService emailService,
        IHubContext<FleetHub, IFleetClient> hub,
        ILogger<StripeEvent> logger)
    {
        var invoice = stripeEvent.Data.Object as Stripe.Invoice;
        if (invoice is null) return;

        var customer = await db.Customers.FirstOrDefaultAsync(
            c => c.StripeCustomerId == invoice.CustomerId);
        if (customer is null)
        {
            logger.LogWarning("No customer found for Stripe customer {StripeCustomerId}", invoice.CustomerId);
            return;
        }

        customer.FailedPaymentCount++;
        customer.FirstPaymentFailedAt ??= DateTime.UtcNow;
        await db.SaveChangesAsync();

        logger.LogInformation(
            "Payment failed for customer {CustomerId} ({Company}) — count={Count}",
            customer.Id, customer.CompanyName, customer.FailedPaymentCount);

        await emailService.SendPaymentFailedEmailAsync(
            customer.AdminEmail, customer.CompanyName, customer.FailedPaymentCount);

        // At day 14 with 3+ failures → suspend
        var daysSinceFirst = (DateTime.UtcNow - customer.FirstPaymentFailedAt.Value).TotalDays;
        if (customer.FailedPaymentCount >= 3 && daysSinceFirst >= 14)
        {
            logger.LogWarning(
                "Customer {CustomerId} ({Company}) has {Count} failed payments over {Days} days — creating suspend job",
                customer.Id, customer.CompanyName, customer.FailedPaymentCount, (int)daysSinceFirst);

            var job = new Job
            {
                Id = Guid.NewGuid(),
                CustomerId = customer.Id,
                JobType = "suspend",
                Status = JobStatus.Queued,
                TriggeredBy = "stripe-webhook",
                CreatedAt = DateTime.UtcNow,
            };
            db.Jobs.Add(job);
            await db.SaveChangesAsync();

            await hub.Clients.All.SendJobCreated(job.Id.ToString(), customer.Abbreviation, "suspend");
        }
    }

    // ── customer.subscription.updated ───────────────────────────────────────
    private static async Task HandleSubscriptionUpdated(
        Event stripeEvent,
        OrchestratorDbContext db,
        IHubContext<FleetHub, IFleetClient> hub,
        ILogger<StripeEvent> logger)
    {
        var subscription = stripeEvent.Data.Object as Stripe.Subscription;
        if (subscription is null) return;

        var customer = await db.Customers.FirstOrDefaultAsync(
            c => c.StripeSubscriptionId == subscription.Id);
        if (customer is null)
        {
            logger.LogWarning("No customer found for subscription {SubscriptionId}", subscription.Id);
            return;
        }

        // Sync screen count from subscription quantity
        var quantity = subscription.Items?.Data?.FirstOrDefault()?.Quantity;
        if (quantity.HasValue && (int)quantity.Value != customer.ScreenCount)
        {
            var oldCount = customer.ScreenCount;
            customer.ScreenCount = (int)quantity.Value;

            logger.LogInformation(
                "Screen count changed for customer {CustomerId}: {Old} → {New}",
                customer.Id, oldCount, customer.ScreenCount);

            var job = new Job
            {
                Id = Guid.NewGuid(),
                CustomerId = customer.Id,
                JobType = "update-screen-limit",
                Status = JobStatus.Queued,
                TriggeredBy = "stripe-webhook",
                Parameters = $"{{\"oldCount\":{oldCount},\"newCount\":{customer.ScreenCount}}}",
                CreatedAt = DateTime.UtcNow,
            };
            db.Jobs.Add(job);
            await db.SaveChangesAsync();

            await hub.Clients.All.SendJobCreated(job.Id.ToString(), customer.Abbreviation, "update-screen-limit");
        }

        // Reactivate if subscription is active and customer was suspended
        if (subscription.Status == "active" && customer.Status == CustomerStatus.Suspended)
        {
            logger.LogInformation("Reactivating customer {CustomerId} — subscription now active", customer.Id);

            customer.FailedPaymentCount = 0;
            customer.FirstPaymentFailedAt = null;

            var job = new Job
            {
                Id = Guid.NewGuid(),
                CustomerId = customer.Id,
                JobType = "reactivate",
                Status = JobStatus.Queued,
                TriggeredBy = "stripe-webhook",
                CreatedAt = DateTime.UtcNow,
            };
            db.Jobs.Add(job);
            await db.SaveChangesAsync();

            await hub.Clients.All.SendJobCreated(job.Id.ToString(), customer.Abbreviation, "reactivate");
        }

        await db.SaveChangesAsync();
        await hub.Clients.All.SendInstanceStatusChanged(customer.Id.ToString(), customer.Status.ToString());
    }

    // ── customer.subscription.deleted ───────────────────────────────────────
    private static async Task HandleSubscriptionDeleted(
        Event stripeEvent,
        OrchestratorDbContext db,
        IHubContext<FleetHub, IFleetClient> hub,
        ILogger<StripeEvent> logger)
    {
        var subscription = stripeEvent.Data.Object as Stripe.Subscription;
        if (subscription is null) return;

        var customer = await db.Customers.FirstOrDefaultAsync(
            c => c.StripeSubscriptionId == subscription.Id);
        if (customer is null)
        {
            logger.LogWarning("No customer found for deleted subscription {SubscriptionId}", subscription.Id);
            return;
        }

        logger.LogInformation(
            "Subscription deleted for customer {CustomerId} ({Company}) — creating decommission job",
            customer.Id, customer.CompanyName);

        var job = new Job
        {
            Id = Guid.NewGuid(),
            CustomerId = customer.Id,
            JobType = "decommission",
            Status = JobStatus.Queued,
            TriggeredBy = "stripe-webhook",
            CreatedAt = DateTime.UtcNow,
        };
        db.Jobs.Add(job);
        await db.SaveChangesAsync();

        await hub.Clients.All.SendJobCreated(job.Id.ToString(), customer.Abbreviation, "decommission");
    }
}
