using System.Security.Claims;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Stripe;

namespace JobTrackerApi.Controllers;

[ApiController]
[Route("api/billing")]
public sealed class BillingController : ControllerBase
{
    private const string UserMetadataKey = "jobtracker_user_id";
    private readonly IConfiguration _configuration;
    private readonly UserManager<ApplicationUser> _users;
    private readonly RoleManager<IdentityRole> _roles;
    private readonly ILogger<BillingController> _logger;
    private readonly ExternalOrigin _externalOrigin;
    private readonly IStripeBillingGateway _stripe;

    public BillingController(
        IConfiguration configuration,
        UserManager<ApplicationUser> users,
        RoleManager<IdentityRole> roles,
        ILogger<BillingController> logger,
        ExternalOrigin? externalOrigin = null,
        IStripeBillingGateway? stripe = null)
    {
        _configuration = configuration;
        _users = users;
        _roles = roles;
        _logger = logger;
        _externalOrigin = externalOrigin ?? ExternalOrigin.FromConfiguration(configuration);
        _stripe = stripe ?? new StripeBillingGateway();
    }

    public sealed record BillingRedirectDto(string Url);
    public sealed record BillingStatusDto(bool Enabled, bool CanCheckout, bool CanManage);

    [HttpGet("status")]
    [Authorize(AuthenticationSchemes = "local")]
    public async Task<ActionResult<BillingStatusDto>> Status(CancellationToken cancellationToken)
    {
        var enabled = TryGetConfiguration(out _, out _, out _, out _);
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");
        var user = userId is null ? null : await _users.FindByIdAsync(userId);
        if (user is null) return Unauthorized();

        var entitlements = AccountPlans.ForRoles(await _users.GetRolesAsync(user));
        return Ok(new BillingStatusDto(
            enabled,
            enabled && !entitlements.Ai,
            enabled && !string.IsNullOrWhiteSpace(user.StripeCustomerId)));
    }

    [HttpPost("checkout")]
    [Authorize(AuthenticationSchemes = "local")]
    public async Task<ActionResult<BillingRedirectDto>> Checkout(CancellationToken cancellationToken)
    {
        if (!TryGetConfiguration(out var secretKey, out var premiumPrice, out _, out var publicBaseUrl))
            return Problem(statusCode: StatusCodes.Status503ServiceUnavailable, title: "Billing is not configured.");

        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");
        var user = userId is null ? null : await _users.FindByIdAsync(userId);
        if (user is null) return Unauthorized();

        var currentRoles = await _users.GetRolesAsync(user);
        if (AccountPlans.ForRoles(currentRoles).Ai)
            return Conflict("This account already has Pro access.");

        var metadata = new Dictionary<string, string> { [UserMetadataKey] = user.Id };
        var options = new Stripe.Checkout.SessionCreateOptions
        {
            Mode = "subscription",
            SuccessUrl = $"{publicBaseUrl}/settings?billing=success",
            CancelUrl = $"{publicBaseUrl}/settings?billing=cancelled",
            ClientReferenceId = user.Id,
            Customer = user.StripeCustomerId,
            CustomerEmail = string.IsNullOrWhiteSpace(user.StripeCustomerId) ? user.Email : null,
            Metadata = metadata,
            SubscriptionData = new Stripe.Checkout.SessionSubscriptionDataOptions { Metadata = metadata },
            LineItems = new List<Stripe.Checkout.SessionLineItemOptions>
            {
                new() { Price = premiumPrice, Quantity = 1 },
            },
        };

        try
        {
            var session = await _stripe.CreateCheckoutAsync(secretKey, options, cancellationToken);
            if (string.IsNullOrWhiteSpace(session.Url))
                return Problem(statusCode: StatusCodes.Status502BadGateway, title: "Stripe did not return a checkout URL.");
            return Ok(new BillingRedirectDto(session.Url));
        }
        catch (StripeException ex)
        {
            _logger.LogError(ex, "Stripe checkout creation failed for user {UserId}", user.Id);
            return Problem(statusCode: StatusCodes.Status502BadGateway, title: "Billing checkout is temporarily unavailable.");
        }
    }

    [HttpPost("portal")]
    [Authorize(AuthenticationSchemes = "local")]
    public async Task<ActionResult<BillingRedirectDto>> Portal(CancellationToken cancellationToken)
    {
        if (!TryGetConfiguration(out var secretKey, out _, out _, out var publicBaseUrl))
            return Problem(statusCode: StatusCodes.Status503ServiceUnavailable, title: "Billing is not configured.");

        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");
        var user = userId is null ? null : await _users.FindByIdAsync(userId);
        if (user is null) return Unauthorized();
        if (string.IsNullOrWhiteSpace(user.StripeCustomerId)) return NotFound("No Stripe customer exists for this account.");

        try
        {
            var session = await _stripe.CreatePortalAsync(secretKey, new Stripe.BillingPortal.SessionCreateOptions
                {
                    Customer = user.StripeCustomerId,
                    ReturnUrl = $"{publicBaseUrl}/settings",
                }, cancellationToken);
            return Ok(new BillingRedirectDto(session.Url));
        }
        catch (StripeException ex)
        {
            _logger.LogError(ex, "Stripe billing portal creation failed for user {UserId}", user.Id);
            return Problem(statusCode: StatusCodes.Status502BadGateway, title: "Billing management is temporarily unavailable.");
        }
    }

    [HttpPost("webhook")]
    [AllowAnonymous]
    [RequestSizeLimit(1_000_000)]
    public async Task<IActionResult> Webhook(CancellationToken cancellationToken)
    {
        if (!TryGetConfiguration(out var secretKey, out var premiumPrice, out var webhookSecret, out _))
            return Problem(statusCode: StatusCodes.Status503ServiceUnavailable, title: "Billing is not configured.");

        string json;
        using (var reader = new StreamReader(Request.Body))
            json = await reader.ReadToEndAsync(cancellationToken);

        Event stripeEvent;
        try
        {
            stripeEvent = _stripe.ConstructEvent(json, Request.Headers["Stripe-Signature"].ToString(), webhookSecret);
        }
        catch (StripeException ex)
        {
            _logger.LogWarning(ex, "Rejected a Stripe webhook with an invalid signature");
            return BadRequest("Invalid Stripe signature.");
        }

        if (stripeEvent.Type is not (EventTypes.CustomerSubscriptionCreated
            or EventTypes.CustomerSubscriptionUpdated
            or EventTypes.CustomerSubscriptionDeleted))
            return Ok();

        if (stripeEvent.Data.Object is not Subscription eventSubscription)
            return BadRequest("Stripe subscription payload was missing.");

        Subscription subscription;
        try
        {
            // Stripe does not guarantee webhook delivery order. Re-read the subscription so a late
            // event cannot restore access after a newer cancellation or payment failure.
            subscription = await _stripe.GetSubscriptionAsync(secretKey, eventSubscription.Id, cancellationToken);
        }
        catch (StripeException ex)
        {
            _logger.LogError(ex, "Could not refresh Stripe subscription {SubscriptionId}", eventSubscription.Id);
            return Problem(statusCode: StatusCodes.Status502BadGateway, title: "Could not verify the current subscription state.");
        }

        if (subscription.Items?.Data?.Any(item => item.Price?.Id == premiumPrice) != true)
        {
            _logger.LogWarning("Ignoring Stripe subscription {SubscriptionId} because it does not contain the configured Premium price", subscription.Id);
            return Ok();
        }

        if (!subscription.Metadata.TryGetValue(UserMetadataKey, out var userId) || string.IsNullOrWhiteSpace(userId))
        {
            _logger.LogWarning("Ignoring Stripe subscription {SubscriptionId} because Jobbjakt user metadata is missing", subscription.Id);
            return Ok();
        }

        var user = await _users.FindByIdAsync(userId);
        if (user is null)
        {
            _logger.LogWarning("Ignoring Stripe subscription {SubscriptionId} because user {UserId} no longer exists", subscription.Id, userId);
            return Ok();
        }

        user.StripeCustomerId = subscription.CustomerId;
        user.StripeSubscriptionId = subscription.Id;
        user.StripeSubscriptionStatus = subscription.Status;
        user.StripeLastEventCreatedUtc = stripeEvent.Created;

        var update = await _users.UpdateAsync(user);
        if (!update.Succeeded)
            return Problem(statusCode: StatusCodes.Status500InternalServerError, title: "Could not persist billing state.");

        const string premiumRole = "Premium";
        if (!await _roles.RoleExistsAsync(premiumRole))
        {
            var roleResult = await _roles.CreateAsync(new IdentityRole(premiumRole));
            if (!roleResult.Succeeded)
                return Problem(statusCode: StatusCodes.Status500InternalServerError, title: "Could not provision the Premium role.");
        }

        var hasRole = await _users.IsInRoleAsync(user, premiumRole);
        var shouldHaveRole = AccountPlans.IsPremiumSubscriptionStatus(subscription.Status);
        var roleUpdate = shouldHaveRole && !hasRole
            ? await _users.AddToRoleAsync(user, premiumRole)
            : !shouldHaveRole && hasRole
                ? await _users.RemoveFromRoleAsync(user, premiumRole)
                : IdentityResult.Success;

        if (!roleUpdate.Succeeded)
            return Problem(statusCode: StatusCodes.Status500InternalServerError, title: "Could not update Premium access.");

        return Ok();
    }

    private bool TryGetConfiguration(out string secretKey, out string premiumPrice, out string webhookSecret, out string publicBaseUrl)
    {
        secretKey = (_configuration["Stripe:SecretKey"] ?? string.Empty).Trim();
        premiumPrice = (_configuration["Stripe:PricePremium"] ?? string.Empty).Trim();
        webhookSecret = (_configuration["Stripe:WebhookSecret"] ?? string.Empty).Trim();
        publicBaseUrl = _externalOrigin.BaseUrl;
        return secretKey.Length > 0 && premiumPrice.StartsWith("price_", StringComparison.Ordinal) && webhookSecret.Length > 0;
    }
}
