using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using PowderCoating.Application.DTOs.Registration;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;
using PowderCoating.Infrastructure.Data;
using PowderCoating.Shared.Constants;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text.Json;

namespace PowderCoating.Web.Controllers;

/// <summary>
/// Handles new tenant company sign-up via two paths: a free-trial path (immediate account creation)
/// and a paid path (Stripe Checkout session → payment → account creation on return). A
/// <c>PendingRegistrationSession</c> persisted to the database bridges the Stripe redirect gap so
/// registration data survives even if the user's browser session is lost between leaving for
/// Stripe and returning.
/// </summary>
[AllowAnonymous]
[EnableRateLimiting(AppConstants.RateLimitPolicies.Registration)]
public class RegistrationController : Controller
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly ApplicationDbContext _db;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly ISeedDataService _seedDataService;
    private readonly IAdminNotificationService _adminNotification;
    private readonly IInAppNotificationService _inApp;
    private readonly IPlatformSettingsService _platformSettings;
    private readonly IStripeService _stripeService;
    private readonly IEmailService _emailService;
    private readonly ILogger<RegistrationController> _logger;

    public RegistrationController(
        IUnitOfWork unitOfWork,
        ApplicationDbContext db,
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager,
        ISeedDataService seedDataService,
        IAdminNotificationService adminNotification,
        IInAppNotificationService inApp,
        IPlatformSettingsService platformSettings,
        IStripeService stripeService,
        IEmailService emailService,
        ILogger<RegistrationController> logger)
    {
        _unitOfWork = unitOfWork;
        _db = db;
        _userManager = userManager;
        _signInManager = signInManager;
        _seedDataService = seedDataService;
        _adminNotification = adminNotification;
        _inApp = inApp;
        _platformSettings = platformSettings;
        _stripeService = stripeService;
        _emailService = emailService;
        _logger = logger;
    }

    /// <summary>
    /// Renders the public registration page. Redirects already-authenticated users to the dashboard.
    /// Reads platform settings at render time to gate whether registration is open (max-tenant cap)
    /// and whether trials are enabled (controls which CTA the view shows). Re-populates the form
    /// from <c>TempData["PendingRegistrationJson"]</c> if the user is returning after cancelling a
    /// Stripe Checkout session so they don't have to retype their details.
    /// </summary>
    [HttpGet]
    public async Task<IActionResult> Index()
    {
        if (User.Identity?.IsAuthenticated == true)
            return RedirectToAction("Index", "Dashboard");

        ViewBag.RegistrationOpen = await IsRegistrationOpenAsync();
        await PopulateRegistrationViewBagAsync();

        var defaultPlan = (ViewBag.PlanConfigs as List<SubscriptionPlanConfig>)
            ?.OrderBy(c => c.SortOrder).FirstOrDefault()?.Plan ?? 0;

        // Re-populate form if returning from a cancelled payment
        var prefillJson = TempData["PendingRegistrationJson"] as string;
        var model = prefillJson != null
            ? JsonSerializer.Deserialize<RegisterCompanyDto>(prefillJson) ?? new RegisterCompanyDto { Plan = defaultPlan }
            : new RegisterCompanyDto { Plan = defaultPlan };

        return View(model);
    }

    /// <summary>
    /// Processes the registration form submission. Validates the model, checks that registration is
    /// still open (in case the tenant cap was hit after the page was loaded), and verifies the email
    /// is not already in use. Then branches to <see cref="CreateWithTrialAsync"/> (trials enabled) or
    /// <see cref="RedirectToStripeCheckoutAsync"/> (trials disabled / credit card required).
    /// Duplicate email is checked here rather than relying on a DB unique constraint so we can show
    /// a friendly field-level error message instead of a 500.
    /// </summary>
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create(RegisterCompanyDto model)
    {
        if (!ModelState.IsValid)
        {
            ViewBag.RegistrationOpen = await IsRegistrationOpenAsync();
            await PopulateRegistrationViewBagAsync();
            return View("Index", model);
        }

        if (!await IsRegistrationOpenAsync())
        {
            TempData["Error"] = "Registration is currently closed. Please contact us for more information.";
            ViewBag.RegistrationOpen = false;
            await PopulateRegistrationViewBagAsync();
            return View("Index", model);
        }

        var existing = await _userManager.FindByEmailAsync(model.Email);
        if (existing != null)
        {
            ModelState.AddModelError("Email", "An account with this email address already exists.");
            ViewBag.RegistrationOpen = true;
            await PopulateRegistrationViewBagAsync();
            return View("Index", model);
        }

        var trialsEnabled = await IsTrialsEnabledAsync();

        if (trialsEnabled)
        {
            return await CreateWithTrialAsync(model);
        }
        else
        {
            return await RedirectToStripeCheckoutAsync(model);
        }
    }

    // ─── Trial path ───────────────────────────────────────────────────────────

    /// <summary>
    /// Creates the company and user immediately (no payment required). The trial length is read from
    /// the <c>TrialPeriodDays</c> platform setting (default 7) so SuperAdmins can adjust it without
    /// a deploy. A temporary password is generated and emailed; the user is flagged with
    /// <c>MustChangePassword</c> claim so the first login redirects to the change-password flow.
    /// Company and user creation are NOT in a DB transaction — if user creation fails, the company
    /// row is manually deleted to avoid orphaned tenant records.
    /// </summary>
    private async Task<IActionResult> CreateWithTrialAsync(RegisterCompanyDto model)
    {
        var rawTrialDays = await _platformSettings.GetAsync(PlatformSettingKeys.TrialPeriodDays);
        var trialPeriodDays = int.TryParse(rawTrialDays, out var td) ? td : 7;

        var companyCode = await GenerateUniqueCompanyCodeAsync(model.CompanyName);
        var company = new Company
        {
            CompanyName = model.CompanyName,
            CompanyCode = companyCode,
            Phone = model.CompanyPhone,
            PrimaryContactEmail = model.Email,
            PrimaryContactName = $"{model.FirstName} {model.LastName}",
            SubscriptionPlan = model.Plan,
            SubscriptionStatus = SubscriptionStatus.Active,
            SubscriptionStartDate = DateTime.UtcNow,
            SubscriptionEndDate = DateTime.UtcNow.AddDays(trialPeriodDays),
            IsAnnualBilling = model.IsAnnual,
            IsActive = true,
            CreatedAt = DateTime.UtcNow
        };

        await _unitOfWork.Companies.AddAsync(company);
        await _unitOfWork.CompleteAsync();

        var tempPassword = GenerateTemporaryPassword();
        var user = BuildUser(model.Email, model.FirstName, model.LastName, company.Id);

        var createResult = await _userManager.CreateAsync(user, tempPassword);
        if (!createResult.Succeeded)
        {
            await _unitOfWork.Companies.DeleteAsync(company);
            await _unitOfWork.CompleteAsync();

            foreach (var error in createResult.Errors)
                ModelState.AddModelError(string.Empty, error.Description);

            await PopulateRegistrationViewBagAsync();
            return View("Index", model);
        }

        await _userManager.AddClaimAsync(user, new Claim("MustChangePassword", "true"));
        await FinalizeRegistrationAsync(user, company, model.Plan);
        _ = SendWelcomeEmailAsync(model.Email, model.FirstName, tempPassword, model.Plan,
            DateTime.UtcNow.AddDays(trialPeriodDays), $"{Request.Scheme}://{Request.Host}");

        return RedirectToAction(nameof(Welcome));
    }

    // ─── Credit-card-required path ────────────────────────────────────────────

    /// <summary>
    /// Starts the paid-path registration by creating a Stripe Checkout session and redirecting the
    /// user to Stripe's hosted payment page. A URL-safe GUID token is generated and embedded in both
    /// the success and cancel return URLs. The <c>PendingRegistrationSession</c> is persisted to the
    /// database AFTER Stripe confirms the session is valid — this prevents orphaned DB records when
    /// Stripe rejects the request (e.g. invalid plan price ID). Account creation does not happen
    /// here; it happens in <see cref="PaymentSuccess"/> after Stripe redirects back with the
    /// Checkout session ID.
    /// </summary>
    private async Task<IActionResult> RedirectToStripeCheckoutAsync(RegisterCompanyDto model)
    {
        // Generate a token that travels in the URL — no session cookie needed to survive the Stripe redirect.
        var token = Guid.NewGuid().ToString("N");

        var successUrl = Url.Action(nameof(PaymentSuccess),   "Registration", new { reg_token = token }, Request.Scheme)!;
        var cancelUrl  = Url.Action(nameof(PaymentCancelled), "Registration", new { reg_token = token }, Request.Scheme)!;

        try
        {
            var checkoutUrl = await _stripeService.CreateRegistrationCheckoutSessionAsync(
                model.Plan, model.IsAnnual, model.Email, model.CompanyName, successUrl, cancelUrl);

            // Persist pending data AFTER confirming Stripe accepted the session.
            _db.PendingRegistrationSessions.Add(new PendingRegistrationSession
            {
                Token       = token,
                CompanyName = model.CompanyName,
                CompanyPhone = model.CompanyPhone,
                FirstName   = model.FirstName,
                LastName    = model.LastName,
                Email       = model.Email,
                Plan        = model.Plan,
                IsAnnual    = model.IsAnnual,
                CreatedAt   = DateTime.UtcNow
            });
            await _db.SaveChangesAsync();

            return Redirect(checkoutUrl);
        }
        catch (InvalidOperationException ex)
        {
            _logger.LogError(ex, "Stripe config error during registration checkout for plan {Plan}", model.Plan);
            ModelState.AddModelError(string.Empty, ex.Message);
            await PopulateRegistrationViewBagAsync();
            return View("Index", model);
        }
        catch (Stripe.StripeException ex)
        {
            _logger.LogError(ex, "Stripe API error during registration checkout");
            ModelState.AddModelError(string.Empty,
                "A payment processor error occurred. Please try again or contact support.");
            await PopulateRegistrationViewBagAsync();
            return View("Index", model);
        }
    }

    /// <summary>
    /// Stripe return URL handler called after the customer completes payment in Stripe Checkout.
    /// Looks up the <c>PendingRegistrationSession</c> by the opaque <c>reg_token</c> to recover the
    /// registration data that could not be stored in a session cookie (which may not survive the
    /// Stripe redirect on some browsers/devices). The Stripe session is re-validated before any
    /// local company or user is created, and the pending session is only left marked completed once
    /// registration succeeds. On recoverable failures the completion flag is released so the same
    /// success URL can be retried instead of forcing manual support intervention.
    /// </summary>
    [HttpGet]
    public async Task<IActionResult> PaymentSuccess(string? session_id, string? reg_token)
    {
        if (string.IsNullOrEmpty(session_id))
            return RedirectToAction(nameof(Index));

        if (string.IsNullOrEmpty(reg_token))
        {
            TempData["Error"] = "Your registration session has expired. Please fill in your details again.";
            return RedirectToAction(nameof(Index));
        }

        var pendingSession = await _db.PendingRegistrationSessions
            .FirstOrDefaultAsync(p => p.Token == reg_token);

        if (pendingSession == null)
        {
            TempData["Error"] = "Your registration session was not found. Please fill in your details again.";
            return RedirectToAction(nameof(Index));
        }

        var pending = new PendingRegistration(
            pendingSession.CompanyName, pendingSession.CompanyPhone,
            pendingSession.FirstName, pendingSession.LastName,
            pendingSession.Email, pendingSession.Plan, pendingSession.IsAnnual);

        var existingUser = await _userManager.FindByEmailAsync(pending.Email);

        if (pendingSession.IsCompleted)
        {
            if (existingUser != null)
            {
                await SignInExistingRegistrationUserAsync(existingUser);
                return RedirectToAction(nameof(Welcome));
            }

            TempData["Error"] = "Your registration was already submitted, but we couldn't finish signing you in. Please contact support with reference: " + session_id;
            return RedirectToAction(nameof(Index));
        }

        if (!await _stripeService.IsRegistrationCheckoutPaidAsync(session_id))
        {
            TempData["Error"] = "We couldn't verify a completed payment for this registration session yet. Please try the link again in a moment, or contact support if the issue persists.";
            return RedirectToAction(nameof(Index));
        }

        var keepSessionCompleted = false;
        pendingSession.IsCompleted = true;
        await _db.SaveChangesAsync();

        try
        {
            // Guard against race condition: re-check capacity after Stripe redirect
            if (!await IsRegistrationOpenAsync())
            {
                TempData["Error"] = "Registration is currently closed. Your payment has been received but no account was created. Please contact support.";
                return RedirectToAction(nameof(Index));
            }

            // Recover gracefully if a prior attempt already created the user.
            if (existingUser != null)
            {
                keepSessionCompleted = true;
                await SignInExistingRegistrationUserAsync(existingUser);
                return RedirectToAction(nameof(Welcome));
            }

            var companyCode = await GenerateUniqueCompanyCodeAsync(pending.CompanyName);
            var company = new Company
            {
                CompanyName = pending.CompanyName,
                CompanyCode = companyCode,
                Phone = pending.CompanyPhone,
                PrimaryContactEmail = pending.Email,
                PrimaryContactName = $"{pending.FirstName} {pending.LastName}",
                SubscriptionPlan = pending.Plan,
                SubscriptionStatus = SubscriptionStatus.Active,
                SubscriptionStartDate = DateTime.UtcNow,
                SubscriptionEndDate = DateTime.UtcNow.AddDays(1), // Stripe fulfillment sets real date below
                IsActive = true,
                CreatedAt = DateTime.UtcNow
            };

            await _unitOfWork.Companies.AddAsync(company);
            await _unitOfWork.CompleteAsync();

            var tempPassword = GenerateTemporaryPassword();
            var user = BuildUser(pending.Email, pending.FirstName, pending.LastName, company.Id);

            var createResult = await _userManager.CreateAsync(user, tempPassword);
            if (!createResult.Succeeded)
            {
                await _unitOfWork.Companies.DeleteAsync(company);
                await _unitOfWork.CompleteAsync();

                _logger.LogError("Failed to create user after payment for {Email}: {Errors}",
                    pending.Email, string.Join(", ", createResult.Errors.Select(e => e.Description)));

                TempData["Error"] = "Your payment was received, but we couldn't finish creating your account. Please try the success link again, or contact support with reference: " + session_id;
                return RedirectToAction(nameof(Index));
            }

            // Link the Stripe subscription (sets real SubscriptionEndDate)
            try
            {
                await _stripeService.FulfillRegistrationCheckoutAsync(session_id, company.Id, pending.Plan);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to fulfill registration checkout {SessionId} for company {CompanyId}",
                    session_id, company.Id);
                // Non-fatal — subscription dates can be synced manually later
            }

            await _userManager.AddClaimAsync(user, new Claim("MustChangePassword", "true"));
            await FinalizeRegistrationAsync(user, company, pending.Plan);
            _ = SendWelcomeEmailAsync(pending.Email, pending.FirstName, tempPassword, pending.Plan,
                null, $"{Request.Scheme}://{Request.Host}");

            keepSessionCompleted = true;
            return RedirectToAction(nameof(Welcome));
        }
        finally
        {
            if (!keepSessionCompleted)
            {
                pendingSession.IsCompleted = false;
                await _db.SaveChangesAsync();
            }
        }
    }

    /// <summary>
    /// Stripe cancel URL handler called when the user clicks "Back" or otherwise abandons the Stripe
    /// Checkout page. Recovers the pending registration data from the DB and serialises it into
    /// <c>TempData</c> so the registration form is pre-populated when <see cref="Index"/> renders,
    /// then removes the <c>PendingRegistrationSession</c> record — a new one is created if the user
    /// tries again. No account is created at this point; the user is shown a friendly error message.
    /// </summary>
    [HttpGet]
    public async Task<IActionResult> PaymentCancelled(string? reg_token)
    {
        if (!string.IsNullOrEmpty(reg_token))
        {
            var pendingSession = await _db.PendingRegistrationSessions
                .FirstOrDefaultAsync(p => p.Token == reg_token && !p.IsCompleted);

            if (pendingSession != null)
            {
                // Pre-populate the form so the user doesn't have to retype everything
                TempData["PendingRegistrationJson"] = JsonSerializer.Serialize(new RegisterCompanyDto
                {
                    CompanyName  = pendingSession.CompanyName,
                    CompanyPhone = pendingSession.CompanyPhone,
                    FirstName    = pendingSession.FirstName,
                    LastName     = pendingSession.LastName,
                    Email        = pendingSession.Email,
                    Plan         = pendingSession.Plan,
                    IsAnnual     = pendingSession.IsAnnual
                });

                _db.PendingRegistrationSessions.Remove(pendingSession);
                await _db.SaveChangesAsync();
            }
        }

        TempData["Error"] = "Payment was cancelled — your account has not been created. Please try again whenever you're ready.";
        return RedirectToAction(nameof(Index));
    }

    // ─── Shared post-creation steps ───────────────────────────────────────────

    /// <summary>
    /// Shared post-creation step executed for both the trial and paid paths. Assigns the
    /// Administrator role, seeds company lookup tables, records Terms-of-Service acceptance, sends
    /// admin notifications, and finally signs the user in.
    /// Sign-in is intentionally the LAST step — once <c>SignInAsync</c> runs the HTTP context user
    /// becomes the new company admin, which changes the tenant context for any subsequent
    /// <c>_db</c> writes. Notifications are therefore awaited synchronously (not fire-and-forget)
    /// before sign-in to prevent two specific bugs: (1) in-app notifications written for SuperAdmins
    /// would have their <c>CompanyId=0</c> sentinel overwritten by the tenant context after sign-in;
    /// (2) <c>IPlatformSettingsService</c> reads use a scoped <c>DbContext</c> that can be disposed
    /// if the caller fire-and-forgets.
    /// </summary>
    private async Task FinalizeRegistrationAsync(ApplicationUser user, Company company, int plan)
    {
        await _userManager.AddToRoleAsync(user, "Administrator");

        try { await _seedDataService.SeedCompanyLookupsAsync(company.Id); }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to seed lookups for company {CompanyId}", company.Id);
        }

        // Record ToS acceptance while still anonymous — CompanyId is set explicitly on the entity
        // so UpdateTimestampsAndTenancy won't overwrite it.
        _db.TermsAcceptances.Add(new TermsAcceptance
        {
            UserId = user.Id,
            CompanyId = company.Id,
            TosVersion = AppConstants.Legal.CurrentTosVersion,
            AcceptedAt = DateTime.UtcNow,
            IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
            UserAgent = Request.Headers["User-Agent"].FirstOrDefault()
        });
        await _db.SaveChangesAsync();

        _logger.LogInformation("New company registered: {CompanyName} (ID {CompanyId}) by {Email}",
            company.CompanyName, company.Id, user.Email);

        // Await both notifications BEFORE SignInAsync for two reasons:
        // 1. InAppNotification: CreateForSuperAdminsAsync writes CompanyId=0 (SuperAdmin sentinel).
        //    After SignIn, UpdateTimestampsAndTenancy would overwrite that 0 with the new company's ID,
        //    routing the bell notification to the wrong recipient.
        // 2. AdminEmail: NotifyNewCompanyRegisteredAsync uses IPlatformSettingsService (→ DbContext).
        //    Fire-and-forget tasks can outlive the scoped DbContext, causing silent failures where
        //    GetAdminEmailsAsync() throws ObjectDisposedException and no email is sent at all.
        var planConfig = await _unitOfWork.SubscriptionPlanConfigs.FirstOrDefaultAsync(
            c => c.Plan == plan && c.IsActive, ignoreQueryFilters: true);
        var planName = planConfig?.DisplayName ?? plan.ToString();
        await _adminNotification.NotifyNewCompanyRegisteredAsync(
            company.Id, company.CompanyName, planName,
            $"{user.FirstName} {user.LastName}", user.Email!);
        await _inApp.CreateForSuperAdminsAsync(
            "New Company Registered",
            $"{company.CompanyName} signed up on the {planName} plan.",
            "NewCompany",
            $"/Companies/Details/{company.Id}");

        // Sign in last — after this point the HTTP context user becomes the new company admin,
        // which would change the tenant context for any subsequent _db writes.
        user.LastLoginDate = DateTime.UtcNow;
        await _userManager.UpdateAsync(user);
        await _signInManager.SignInAsync(user, isPersistent: false);
    }

    /// <summary>
    /// Signs in an already-created registration user on idempotent success-link retries.
    /// This is intentionally narrower than <see cref="FinalizeRegistrationAsync"/> because all
    /// registration side effects should already have happened on the original successful run.
    /// </summary>
    private async Task SignInExistingRegistrationUserAsync(ApplicationUser user)
    {
        user.LastLoginDate = DateTime.UtcNow;
        await _userManager.UpdateAsync(user);
        await _signInManager.SignInAsync(user, isPersistent: false);
    }

    /// <summary>
    /// Renders the post-registration welcome page. Requires authentication (the user was just signed
    /// in by <see cref="FinalizeRegistrationAsync"/>). Determines whether the account is on a free
    /// trial by checking for the absence of a <c>StripeSubscriptionId</c> — trial accounts are
    /// created without a subscription ID, which the welcome view uses to display an upgrade prompt
    /// or hide the trial countdown pill.
    /// </summary>
    [HttpGet]
    [Authorize]
    public async Task<IActionResult> Welcome()
    {
        ViewData["Title"] = "Welcome to Powder Coating Logix";
        var user = await _userManager.GetUserAsync(User);
        ViewBag.FirstName = user?.FirstName ?? User.Identity?.Name?.Split('@')[0] ?? "there";

        // Determine if this is a trial or a paid subscription so the view can hide the trial pill accordingly
        if (user != null && user.CompanyId > 0)
        {
            var company = await _unitOfWork.Companies.GetByIdAsync(user.CompanyId, ignoreQueryFilters: true);
            var isOnTrial = company != null && string.IsNullOrEmpty(company.StripeSubscriptionId);
            ViewBag.IsOnTrial = isOnTrial;
            ViewBag.TrialEndDate = company?.SubscriptionEndDate;
        }
        else
        {
            ViewBag.IsOnTrial = false;
            ViewBag.TrialEndDate = null;
        }

        return View();
    }

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

    /// <summary>
    /// Returns whether the free-trial sign-up path is active. Reads the <c>TrialsEnabled</c>
    /// platform setting; a missing or null value is treated as enabled (safe default so a freshly
    /// deployed instance with no platform settings can still onboard users).
    /// </summary>
    private async Task<bool> IsTrialsEnabledAsync()
    {
        var raw = await _platformSettings.GetAsync(PlatformSettingKeys.TrialsEnabled);
        // Null/missing = treat as enabled (safe default)
        return raw == null || !string.Equals(raw, "false", StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Checks whether the platform has capacity for a new tenant. Reads the <c>MaxTenants</c>
    /// platform setting; a blank value or 0 means unlimited. Uses <c>IgnoreQueryFilters</c> on the
    /// company count so soft-deleted companies still count against the cap (preventing circumvention
    /// by deleting and re-registering). Marked <c>internal</c> to allow unit-testing without
    /// mocking the full HTTP pipeline.
    /// </summary>
    internal async Task<bool> IsRegistrationOpenAsync()
    {
        var raw = await _platformSettings.GetAsync(PlatformSettingKeys.MaxTenants);
        // Blank or 0 = unlimited
        if (!int.TryParse(raw, out var max) || max <= 0)
            return true;

        var count = await _unitOfWork.Companies.CountAsync(ignoreQueryFilters: true);
        return count < max;
    }

    /// <summary>
    /// Populates shared ViewBag properties required by the registration view: active subscription
    /// plan configs (sorted by <c>SortOrder</c>), the trials-enabled flag, and the trial period
    /// length in days. Called by both the GET and failed POST paths so the view always has the data
    /// it needs without the caller repeating these lookups.
    /// </summary>
    private async Task PopulateRegistrationViewBagAsync()
    {
        var planConfigs = (await _unitOfWork.SubscriptionPlanConfigs.FindAsync(
                c => c.IsActive, ignoreQueryFilters: true))
            .OrderBy(c => c.SortOrder)
            .ToList();
        ViewBag.PlanConfigs = planConfigs;

        ViewBag.TrialsEnabled = await IsTrialsEnabledAsync();

        var rawTrialDays = await _platformSettings.GetAsync(PlatformSettingKeys.TrialPeriodDays);
        ViewBag.TrialPeriodDays = int.TryParse(rawTrialDays, out var td) ? td : 7;
    }

    /// <summary>
    /// Constructs the <c>ApplicationUser</c> for a new company administrator. All permissions are
    /// granted by default because the first user of a new company must be able to do everything.
    /// <c>EmailConfirmed</c> is set to <c>true</c> immediately — we skip the email-verification
    /// step for registrations because the welcome email with a temp password serves as implicit
    /// confirmation that the address is reachable.
    /// </summary>
    private static ApplicationUser BuildUser(
        string email, string firstName, string lastName, int companyId)
    {
        var user = new ApplicationUser
        {
            UserName    = email,
            Email       = email,
            FirstName   = firstName,
            LastName    = lastName,
            CompanyId   = companyId,
            CompanyRole = "CompanyAdmin",
            HireDate    = DateTime.UtcNow.Date,
            IsActive    = true,
            EmailConfirmed = true,
            CreatedAt   = DateTime.UtcNow
        };
        user.GrantAllPermissions();
        return user;
    }

    /// <summary>
    /// Derives a short, unique company code from the company name (e.g. "Acme Powder Coating" →
    /// "APC"). Stop words (the, and, inc, llc, etc.) are stripped first. One-, two-, or three-word
    /// names each use a different initialism strategy so the code is always 3 characters. A numeric
    /// suffix (2, 3, …) is appended if the derived code is already taken. The collision check loads
    /// all existing codes into memory — acceptable for small tenant counts but should be replaced
    /// with a DB-level unique check if the platform ever scales to thousands of tenants.
    /// </summary>
    private async Task<string> GenerateUniqueCompanyCodeAsync(string companyName)
    {
        var stopWords = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "the", "and", "of", "a", "an", "for", "inc", "llc", "ltd", "co", "corp"
        };

        var words = companyName
            .Split(' ', StringSplitOptions.RemoveEmptyEntries)
            .Where(w => !stopWords.Contains(w))
            .ToList();

        string baseCode;
        if (words.Count >= 3)
            baseCode = new string(words.Take(3).Select(w => char.ToUpper(w[0])).ToArray());
        else if (words.Count == 2)
            baseCode = char.ToUpper(words[0][0]).ToString() + char.ToUpper(words[1][0]).ToString() +
                       char.ToUpper(words[1].Length > 1 ? words[1][1] : words[0][1]);
        else if (words.Count == 1)
            baseCode = words[0].Length >= 4
                ? words[0][..4].ToUpper()
                : words[0].ToUpper().PadRight(3, 'X');
        else
            baseCode = "CO";

        var code = baseCode;
        var allCodes = (await _unitOfWork.Companies.GetAllAsync(ignoreQueryFilters: true))
            .Select(c => c.CompanyCode)
            .Where(c => c != null)
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        int suffix = 2;
        while (allCodes.Contains(code))
        {
            code = baseCode + suffix;
            suffix++;
        }

        return code;
    }

    // ─── Session model ────────────────────────────────────────────────────────

    private sealed record PendingRegistration(
        string CompanyName,
        string? CompanyPhone,
        string FirstName,
        string LastName,
        string Email,
        int Plan,
        bool IsAnnual = false);

    // ─── Temporary password generation ────────────────────────────────────────

    /// <summary>
    /// Generates a cryptographically random 12-character temporary password that satisfies ASP.NET
    /// Core Identity's default complexity rules (uppercase, lowercase, digit, special character).
    /// Ambiguous characters (I, O, 0, 1, l) are excluded from the alphabet to reduce transcription
    /// errors in case the user reads the password from the email rather than copying it.
    /// A Fisher-Yates shuffle with a second batch of random bytes ensures no character class is
    /// predictably in a fixed position (e.g. the guaranteed uppercase is not always at index 0).
    /// </summary>
    private static string GenerateTemporaryPassword()
    {
        const string upper   = "ABCDEFGHJKLMNPQRSTUVWXYZ"; // no I, O
        const string lower   = "abcdefghjkmnpqrstuvwxyz";  // no i, l, o
        const string digits  = "23456789";                  // no 0, 1
        const string special = "!@#$%&*";
        const string all     = upper + lower + digits + special;

        var bytes = new byte[16];
        RandomNumberGenerator.Fill(bytes);

        var pw = new char[12];
        // Guarantee one character of each required class
        pw[0] = upper  [bytes[0] % upper.Length];
        pw[1] = lower  [bytes[1] % lower.Length];
        pw[2] = digits [bytes[2] % digits.Length];
        pw[3] = special[bytes[3] % special.Length];
        for (int i = 4; i < 12; i++)
            pw[i] = all[bytes[i] % all.Length];

        // Fisher-Yates shuffle with fresh random bytes
        RandomNumberGenerator.Fill(bytes);
        for (int i = 11; i > 0; i--)
        {
            int j = bytes[i] % (i + 1);
            (pw[i], pw[j]) = (pw[j], pw[i]);
        }

        return new string(pw);
    }

    // ─── Welcome email ────────────────────────────────────────────────────────

    /// <summary>
    /// Sends the welcome email with temporary password, plan-specific feature highlights, and an
    /// optional trial-expiry blurb. Called fire-and-forget so a transient email failure never blocks
    /// the registration response. Logged at Error because a missing welcome email means the user
    /// cannot retrieve their temporary password and must contact support.
    /// </summary>
    private async Task SendWelcomeEmailAsync(
        string email, string firstName, string tempPassword,
        int plan, DateTime? trialEndDate, string baseUrl)
    {
        try
        {
            var planName = plan switch
            {
                3 => "Starter",
                0 => "Basic",
                1 => "Pro",
                2 => "Enterprise",
                _ => "Starter"
            };

            // Plan-specific feature bullets (plain + html pairs)
            var (plainFeatures, htmlFeatures) = plan switch
            {
                // Starter
                3 => (
                    "• Job & quote management for up to 1 active user\r\n" +
                    "• Customer records and job history\r\n" +
                    "• Inventory tracking and low-stock alerts\r\n" +
                    "• PDF quotes and invoices\r\n" +
                    "• Basic financial reports",

                    "<li>Job &amp; quote management for up to 1 active user</li>" +
                    "<li>Customer records and job history</li>" +
                    "<li>Inventory tracking and low-stock alerts</li>" +
                    "<li>PDF quotes and invoices</li>" +
                    "<li>Basic financial reports</li>"
                ),
                // Basic
                0 => (
                    "• Job & quote management for up to 3 users\r\n" +
                    "• Customer records and job history\r\n" +
                    "• Inventory tracking, purchase orders, and vendor management\r\n" +
                    "• PDF quotes, invoices, and deposits\r\n" +
                    "• Oven scheduler for batch planning\r\n" +
                    "• Equipment and maintenance tracking\r\n" +
                    "• Financial reports and AR aging",

                    "<li>Job &amp; quote management for up to 3 users</li>" +
                    "<li>Customer records and job history</li>" +
                    "<li>Inventory tracking, purchase orders, and vendor management</li>" +
                    "<li>PDF quotes, invoices, and deposits</li>" +
                    "<li>Oven scheduler for batch planning</li>" +
                    "<li>Equipment and maintenance tracking</li>" +
                    "<li>Financial reports and AR aging</li>"
                ),
                // Pro
                1 => (
                    "• Everything in Basic, plus up to 10 users\r\n" +
                    "• AI Photo Quoting — upload a photo, get an instant estimate\r\n" +
                    "• AI Accounting Assistant — receipt scanning, cash flow forecasting, anomaly detection\r\n" +
                    "• Online customer approval portal with Stripe payments\r\n" +
                    "• Advanced analytics and custom reports\r\n" +
                    "• Priority support",

                    "<li>Everything in Basic, plus up to 10 users</li>" +
                    "<li><strong>AI Photo Quoting</strong> — upload a photo, get an instant estimate</li>" +
                    "<li><strong>AI Accounting Assistant</strong> — receipt scanning, cash flow forecasting, anomaly detection</li>" +
                    "<li>Online customer approval portal with Stripe payments</li>" +
                    "<li>Advanced analytics and custom reports</li>" +
                    "<li>Priority support</li>"
                ),
                // Enterprise
                _ => (
                    "• Everything in Pro, with unlimited users and jobs\r\n" +
                    "• AI Photo Quoting and AI Accounting Assistant\r\n" +
                    "• Online customer approval portal with Stripe payments\r\n" +
                    "• Full analytics suite and all report exports\r\n" +
                    "• Dedicated onboarding and priority support\r\n" +
                    "• Custom integrations available on request",

                    "<li>Everything in Pro, with unlimited users and jobs</li>" +
                    "<li><strong>AI Photo Quoting</strong> and <strong>AI Accounting Assistant</strong></li>" +
                    "<li>Online customer approval portal with Stripe payments</li>" +
                    "<li>Full analytics suite and all report exports</li>" +
                    "<li>Dedicated onboarding and priority support</li>" +
                    "<li>Custom integrations available on request</li>"
                )
            };

            var trialPlainBlurb = trialEndDate.HasValue
                ? $"Your {planName} trial is active through {trialEndDate.Value:MMMM d, yyyy}. No credit card is required during the trial — we'll remind you before it ends.\r\n\r\n"
                : string.Empty;

            var trialHtmlBlurb = trialEndDate.HasValue
                ? $"<p style='background:#fff8e1;border-left:4px solid #f59e0b;padding:10px 14px;margin:20px 0;'>" +
                  $"Your <strong>{planName} trial</strong> is active through <strong>{trialEndDate.Value:MMMM d, yyyy}</strong>. " +
                  $"No credit card is required during the trial — we'll remind you before it ends.</p>"
                : string.Empty;

            var subject = $"Welcome to Powder Coating Logix — Your {planName} Account Is Ready";

            var plain =
                $"Hi {firstName},\r\n\r\n" +
                $"Your Powder Coating Logix {planName} account is ready. Here's your temporary password to log in:\r\n\r\n" +
                $"    Temporary password: {tempPassword}\r\n\r\n" +
                $"You'll be prompted to set a new password the first time you log in.\r\n\r\n" +
                trialPlainBlurb +
                $"Your {planName} plan includes:\r\n" +
                plainFeatures + "\r\n\r\n" +
                $"Get started quickly:\r\n" +
                $"  1. Log in at {baseUrl}\r\n" +
                $"  2. Complete the Setup Wizard — it takes about 5 minutes and configures your rates, inventory, and shop profile so quotes price correctly from day one.\r\n\r\n" +
                $"Need help? Our AI assistant is built right into the app — just click the chat icon. You can also reach us at support@powdercoatinglogix.com.\r\n\r\n" +
                $"Welcome aboard,\r\n" +
                $"The Powder Coating Logix Team";

            var html =
                $@"<!DOCTYPE html>
<html>
<head><meta charset='utf-8'></head>
<body style='margin:0;padding:0;background:#f4f4f4;font-family:Arial,sans-serif;'>
  <table width='100%' cellpadding='0' cellspacing='0' style='background:#f4f4f4;padding:30px 0;'>
    <tr><td align='center'>
      <table width='600' cellpadding='0' cellspacing='0' style='background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08);'>

        <!-- Header -->
        <tr>
          <td style='background:#1a3a5c;padding:28px 40px;'>
            <p style='margin:0;color:#ffffff;font-size:22px;font-weight:bold;'>Powder Coating Logix</p>
            <p style='margin:6px 0 0;color:#a8c4e0;font-size:13px;'>Shop management software built for powder coaters</p>
          </td>
        </tr>

        <!-- Body -->
        <tr><td style='padding:36px 40px;color:#333333;font-size:15px;line-height:1.6;'>
          <p style='margin:0 0 16px;'>Hi {firstName},</p>
          <p style='margin:0 0 24px;'>Your <strong>Powder Coating Logix {planName}</strong> account is ready. Here's your temporary password to get started:</p>

          <div style='background:#f0f4f8;border:1px solid #d0dce8;border-radius:6px;padding:16px 24px;margin:0 0 24px;text-align:center;'>
            <p style='margin:0 0 4px;font-size:12px;color:#666;text-transform:uppercase;letter-spacing:0.05em;'>Temporary Password</p>
            <p style='margin:0;font-family:monospace;font-size:22px;font-weight:bold;letter-spacing:0.1em;color:#1a3a5c;'>{tempPassword}</p>
          </div>

          <p style='margin:0 0 24px;color:#555;font-size:14px;'>You'll be prompted to set a new password the first time you log in.</p>

          {trialHtmlBlurb}

          <p style='margin:0 0 10px;font-weight:bold;'>Your {planName} plan includes:</p>
          <ul style='margin:0 0 24px;padding-left:20px;color:#444;'>
            {htmlFeatures}
          </ul>

          <p style='margin:0 0 10px;font-weight:bold;'>Get started quickly:</p>
          <ol style='margin:0 0 24px;padding-left:20px;color:#444;'>
            <li style='margin-bottom:8px;'><a href='{baseUrl}' style='color:#1a3a5c;'>Log in to your account</a></li>
            <li>Complete the <strong>Setup Wizard</strong> — it takes about 5 minutes and configures your rates, inventory, and shop profile so quotes price correctly from day one.</li>
          </ol>

          <p style='margin:0 0 24px;'>Need help? Our <strong>AI assistant</strong> is built right into the app — just click the chat icon. You can also reach us any time at <a href='mailto:support@powdercoatinglogix.com' style='color:#1a3a5c;'>support@powdercoatinglogix.com</a>.</p>

          <p style='margin:0;'>Welcome aboard,<br><strong>The Powder Coating Logix Team</strong></p>
        </td></tr>

        <!-- Footer -->
        <tr>
          <td style='background:#f8f9fa;border-top:1px solid #e9ecef;padding:20px 40px;text-align:center;'>
            <p style='margin:0;font-size:12px;color:#888;'>
              &copy; {DateTime.UtcNow.Year} Powder Coating Logix &nbsp;&bull;&nbsp;
              <a href='mailto:support@powdercoatinglogix.com' style='color:#888;'>support@powdercoatinglogix.com</a>
            </p>
          </td>
        </tr>

      </table>
    </td></tr>
  </table>
</body>
</html>";

            await _emailService.SendEmailAsync(email, firstName, subject, plain, html);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to send welcome email to {Email}", email);
        }
    }
}
