using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using PowderCoating.Application.DTOs.Kiosk;
using PowderCoating.Application.Interfaces;
using PowderCoating.Application.Services;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;
using PowderCoating.Shared.Constants;
using PowderCoating.Web.Hubs;

namespace PowderCoating.Web.Controllers;

/// <summary>
/// Handles the customer self-service intake kiosk — both the in-person tablet flow
/// (SignalR-triggered, activation-cookie-authenticated) and the remote email-link flow.
///
/// Anonymous intake routes use ignoreQueryFilters:true to load KioskSession by token
/// because the anonymous HTTP context has no CompanyId claim, so the global tenant
/// filter would return nothing without that flag.
///
/// When creating new Customer or Job records from the kiosk, CompanyId is set explicitly
/// from session.CompanyId so the EF SaveChanges interceptor doesn't override it with 0.
/// </summary>
[Authorize]
public class KioskController : Controller
{
    private const string CookieName         = "KioskDevice";
    private const int    InPersonExpireHours = 2;
    private const int    RemoteExpireHours   = 48;

    private readonly IUnitOfWork                  _unitOfWork;
    private readonly IMapper                      _mapper;
    private readonly ILookupCacheService          _lookupCache;
    private readonly IInAppNotificationService    _inApp;
    private readonly IEmailService                _emailService;
    private readonly IHubContext<KioskHub>        _kioskHub;
    private readonly ILogger<KioskController>     _logger;
    private readonly ICompanyLogoService          _logoService;
    private readonly IMemoryCache                 _cache;

    private static string SmsConsentCacheKey(int companyId) => $"kiosk-sms-consent:{companyId}";

    /// <summary>Initialises all dependencies for the kiosk controller.</summary>
    public KioskController(
        IUnitOfWork               unitOfWork,
        IMapper                   mapper,
        ILookupCacheService       lookupCache,
        IInAppNotificationService inApp,
        IEmailService             emailService,
        IHubContext<KioskHub>     kioskHub,
        ILogger<KioskController>  logger,
        ICompanyLogoService       logoService,
        IMemoryCache              cache)
    {
        _unitOfWork   = unitOfWork;
        _mapper       = mapper;
        _lookupCache  = lookupCache;
        _inApp        = inApp;
        _emailService = emailService;
        _kioskHub     = kioskHub;
        _logger       = logger;
        _logoService  = logoService;
        _cache        = cache;
    }

    // =========================================================================
    // WELCOME SCREEN  (in-person tablet idle screen)
    // =========================================================================

    /// <summary>
    /// Idle branded screen displayed on the front-desk tablet.
    /// Validates the KioskDevice cookie; returns 403 if missing or token mismatch.
    /// The view polls /Kiosk/PollSession every 3 seconds and navigates when staff
    /// triggers a session via the Dashboard "Start Intake" button.
    /// </summary>
    [AllowAnonymous]
    public async Task<IActionResult> Welcome()
    {
        var cookie = ReadKioskCookie();
        if (cookie == null)
            return View("KioskError", "This device is not activated as a kiosk. Ask a staff member to activate it at Settings → Kiosk.");

        var company = await _unitOfWork.Companies.GetByIdAsync(cookie.Value.companyId, ignoreQueryFilters: true);
        if (company == null || company.KioskActivationToken != cookie.Value.token)
            return View("KioskError", "Kiosk activation token is invalid or has been revoked. Ask a staff member to re-activate this device.");

        await PopulateKioskViewBag(company);
        ViewBag.ShowInactivityTimer = false; // Welcome screen stays on indefinitely
        return View();
    }

    /// <summary>
    /// Lightweight polling endpoint called every 3 seconds by the kiosk Welcome screen.
    /// Returns the most recent InPerson KioskSession created in the last 60 seconds so
    /// the tablet can navigate without relying on SignalR (which Azure App Service blocks
    /// for anonymous WebSocket/SSE connections through its ingress proxy).
    /// </summary>
    [AllowAnonymous, HttpGet]
    [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
    public async Task<IActionResult> PollSession()
    {
        var cookie = ReadKioskCookie();
        if (cookie == null) return Json(new { hasSession = false });

        var company = await _unitOfWork.Companies.GetByIdAsync(cookie.Value.companyId, ignoreQueryFilters: true);
        if (company == null || company.KioskActivationToken != cookie.Value.token)
            return Json(new { hasSession = false });

        // Check for a staff-pushed SMS consent request before checking for intake sessions.
        if (_cache.TryGetValue(SmsConsentCacheKey(cookie.Value.companyId), out (int customerId, string customerName) pending))
            return Json(new { hasSession = false, smsConsentPending = true, customerId = pending.customerId, customerName = pending.customerName });

        var window  = DateTime.UtcNow.AddSeconds(-60);
        var session = await _unitOfWork.KioskSessions.FirstOrDefaultAsync(
            s => s.CompanyId == cookie.Value.companyId
              && s.SessionType == KioskSessionType.InPerson
              && s.Status == KioskSessionStatus.Active
              && s.CreatedAt >= window,
            ignoreQueryFilters: true);

        if (session == null) return Json(new { hasSession = false });
        return Json(new { hasSession = true, sessionToken = session.SessionToken });
    }

    // =========================================================================
    // SMS CONSENT  (staff pushes to kiosk; customer agrees on tablet)
    // =========================================================================

    /// <summary>
    /// Staff calls this (authenticated) from the Customer Details page to push an SMS
    /// consent request to the front-desk kiosk tablet. Stores the customer ID in
    /// IMemoryCache under a company-scoped key; the kiosk's PollSession endpoint picks
    /// it up and returns smsConsentPending so the tablet can navigate to the consent page.
    /// The cache entry expires in 10 minutes in case the customer never approaches the tablet.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> PushSmsConsent(int customerId)
    {
        var customer = await _unitOfWork.Customers.GetByIdAsync(customerId);
        if (customer == null) return Json(new { success = false, message = "Customer not found." });

        if (customer.NotifyBySms)
            return Json(new { success = false, message = "Customer has already given SMS consent." });

        var companyId = customer.CompanyId;
        var name      = !string.IsNullOrWhiteSpace(customer.ContactFirstName)
            ? $"{customer.ContactFirstName} {customer.ContactLastName}".Trim()
            : customer.CompanyName ?? "Customer";

        _cache.Set(SmsConsentCacheKey(companyId), (customerId, name),
            new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) });

        _logger.LogInformation("SMS consent pushed to kiosk for customer {CustomerId} by staff", customerId);
        return Json(new { success = true });
    }

    /// <summary>
    /// Cancels a pending kiosk SMS consent request, freeing the kiosk to return to the Welcome
    /// screen. Called by staff if they pushed consent accidentally or the customer isn't coming.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public IActionResult CancelSmsConsent()
    {
        var companyId = HttpContext.User.FindFirst("CompanyId")?.Value;
        if (int.TryParse(companyId, out var cid))
            _cache.Remove(SmsConsentCacheKey(cid));
        return Json(new { success = true });
    }

    /// <summary>
    /// Displays the full-screen SMS consent form on the kiosk tablet (anonymous, kiosk layout).
    /// Loads the customer by ID with ignoreQueryFilters because the kiosk has no tenant context.
    /// </summary>
    [AllowAnonymous]
    public async Task<IActionResult> SmsConsent(int id)
    {
        var cookie = ReadKioskCookie();
        if (cookie == null) return Forbid();

        // Clear the pending entry immediately — the kiosk is now showing the form,
        // so Welcome must not redirect again if the customer cancels or navigates back.
        _cache.Remove(SmsConsentCacheKey(cookie.Value.companyId));

        var customer = await _unitOfWork.Customers.GetByIdAsync(id, ignoreQueryFilters: true);
        if (customer == null) return NotFound();

        var company = await _unitOfWork.Companies.GetByIdAsync(cookie.Value.companyId, ignoreQueryFilters: true);
        ViewBag.CompanyName    = company?.CompanyName;
        ViewBag.CompanyLogoUrl = !string.IsNullOrEmpty(company?.LogoFilePath) ? Url.Action("Logo", "Kiosk") : null;
        ViewBag.ShowInactivityTimer = false;
        ViewBag.CustomerName   = !string.IsNullOrWhiteSpace(customer.ContactFirstName)
            ? $"{customer.ContactFirstName} {customer.ContactLastName}".Trim()
            : customer.CompanyName ?? "Customer";

        return View(id);
    }

    /// <summary>
    /// Records the customer's SMS consent from the kiosk tablet.
    /// Sets NotifyBySms, SmsConsentedAt, SmsConsentMethod = "KioskInPerson" on the customer record.
    /// Cache is already cleared by the GET; this handles the agree/decline outcome.
    /// </summary>
    [AllowAnonymous, HttpPost]
    public async Task<IActionResult> SmsConsent(int id, bool agreed)
    {
        var cookie = ReadKioskCookie();
        if (cookie == null) return Forbid();

        if (agreed)
        {
            var customer = await _unitOfWork.Customers.GetByIdAsync(id, ignoreQueryFilters: true);
            if (customer != null)
            {
                customer.NotifyBySms      = true;
                customer.SmsConsentedAt   = DateTime.UtcNow;
                customer.SmsConsentMethod = "KioskInPerson";
                customer.SmsOptedOutAt    = null;
                await _unitOfWork.Customers.UpdateAsync(customer);
                await _unitOfWork.CompleteAsync();
                _logger.LogInformation("SMS consent recorded via kiosk for customer {CustomerId}", id);

                await _inApp.CreateAsync(
                    customer.CompanyId,
                    "SMS Consent Recorded",
                    $"{customer.ContactFirstName} {customer.ContactLastName} agreed to SMS notifications on the kiosk.",
                    "KioskConsent",
                    link: $"/Customers/Details/{id}",
                    customerId: id);
            }
        }

        return Redirect("/Kiosk/Welcome");
    }

    /// <summary>
    /// Serves the company logo for anonymous kiosk pages. Resolves the company from the
    /// KioskDevice cookie so no tenant context is needed on the anonymous request.
    /// </summary>
    [AllowAnonymous]
    [HttpGet, ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any)]
    public async Task<IActionResult> Logo()
    {
        var cookie = ReadKioskCookie();
        if (cookie == null) return NotFound();

        var company = await _unitOfWork.Companies.GetByIdAsync(cookie.Value.companyId, ignoreQueryFilters: true);
        if (company == null || string.IsNullOrEmpty(company.LogoFilePath)) return NotFound();

        var (success, fileContent, contentType, _) = await _logoService.GetCompanyLogoAsync(company.LogoFilePath);
        if (!success || fileContent.Length == 0) return NotFound();

        return File(fileContent, contentType);
    }

    // =========================================================================
    // DEVICE ACTIVATION  (CompanyAdmin-only)
    // =========================================================================

    /// <summary>Shows the kiosk activation page with the current activation status.</summary>
    [Authorize(Policy = AppConstants.Policies.CompanyAdminOnly)]
    public async Task<IActionResult> Activate()
    {
        var companyId = GetCurrentCompanyId();
        var company   = await _unitOfWork.Companies.GetByIdAsync(companyId, ignoreQueryFilters: true);
        ViewBag.IsActivated = !string.IsNullOrEmpty(company?.KioskActivationToken);
        return View();
    }

    /// <summary>
    /// Generates a new activation token, saves it to the Company record,
    /// and writes the KioskDevice cookie so the current browser session becomes the active tablet.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    [Authorize(Policy = AppConstants.Policies.CompanyAdminOnly)]
    public async Task<IActionResult> Activate(string action)
    {
        var companyId = GetCurrentCompanyId();
        var company   = await _unitOfWork.Companies.GetByIdAsync(companyId, ignoreQueryFilters: true);
        if (company == null) return NotFound();

        if (action == "deactivate")
        {
            company.KioskActivationToken = null;
            DeleteKioskCookie();
            TempData["Success"] = "Kiosk deactivated. The tablet will no longer accept intake sessions.";
        }
        else
        {
            var token = Guid.NewGuid().ToString("N");
            company.KioskActivationToken = token;
            WriteKioskCookie(companyId, token);
            TempData["Success"] = "Kiosk activated. Open /Kiosk/Welcome on the tablet and bookmark it.";
        }

        await _unitOfWork.CompleteAsync();
        return RedirectToAction(nameof(Activate));
    }

    // =========================================================================
    // START IN-PERSON SESSION  (any authenticated staff member)
    // =========================================================================

    /// <summary>
    /// Creates an InPerson KioskSession and pushes a SignalR StartIntake event
    /// to all connections in the company's kiosk group so the tablet navigates automatically.
    /// Called via fetch from the Dashboard "Start Intake" button.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    [Authorize]
    public async Task<IActionResult> StartSession()
    {
        var companyId = GetCurrentCompanyId();

        var session = new KioskSession
        {
            SessionType = KioskSessionType.InPerson,
            ExpiresAt   = DateTime.UtcNow.AddHours(InPersonExpireHours),
            CompanyId   = companyId
        };

        await _unitOfWork.KioskSessions.AddAsync(session);
        await _unitOfWork.CompleteAsync();

        await _kioskHub.Clients
            .Group($"kiosk-{companyId}")
            .SendAsync("StartIntake", session.SessionToken.ToString());

        return Json(new { success = true, sessionToken = session.SessionToken });
    }

    // =========================================================================
    // SEND REMOTE LINK  (any authenticated staff member)
    // =========================================================================

    /// <summary>Form for staff to enter a customer's email address and send an intake link.</summary>
    [Authorize]
    public IActionResult SendRemoteLink() => View(new SendRemoteLinkDto());

    /// <summary>
    /// Creates a Remote KioskSession, sends the intake link by email, and redirects back
    /// with a success message. The link contains the session token (GUID) — not guessable.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    [Authorize]
    public async Task<IActionResult> SendRemoteLink(SendRemoteLinkDto dto)
    {
        if (!ModelState.IsValid) return View(dto);

        var companyId = GetCurrentCompanyId();
        var company   = await _unitOfWork.Companies.GetByIdAsync(companyId, ignoreQueryFilters: true);

        var session = new KioskSession
        {
            SessionType      = KioskSessionType.Remote,
            ExpiresAt        = DateTime.UtcNow.AddHours(RemoteExpireHours),
            RemoteLinkEmail  = dto.Email,
            RemoteLinkSentAt = DateTime.UtcNow,
            CompanyId        = companyId
        };

        await _unitOfWork.KioskSessions.AddAsync(session);
        await _unitOfWork.CompleteAsync();

        var link = $"{Request.Scheme}://{Request.Host}/Kiosk/Intake/{session.SessionToken}/Contact";
        var recipientName = string.IsNullOrWhiteSpace(dto.CustomerName) ? "Valued Customer" : dto.CustomerName;
        var companyName   = company?.CompanyName ?? "Us";

        var html = $@"
<div style='font-family:sans-serif;max-width:560px;margin:0 auto;padding:2rem;'>
  <h2 style='color:#1e293b;'>Hi {System.Web.HttpUtility.HtmlEncode(recipientName)},</h2>
  <p style='color:#475569;font-size:1rem;'>
    {System.Web.HttpUtility.HtmlEncode(companyName)} has sent you a quick intake form to fill out before your visit.
    It only takes a couple of minutes.
  </p>
  <a href='{link}' style='display:inline-block;margin:1.5rem 0;padding:1rem 2rem;background:#2563eb;
     color:#fff;font-weight:600;border-radius:8px;text-decoration:none;font-size:1.1rem;'>
     Start My Intake Form
  </a>
  <p style='color:#94a3b8;font-size:0.85rem;'>
    This link expires in 48 hours. If you did not expect this email, you can ignore it.
  </p>
</div>";

        await _emailService.SendEmailAsync(
            dto.Email, recipientName,
            $"Your intake form from {companyName}",
            $"Please visit this link to complete your intake form: {link}",
            htmlBody: html);

        TempData["Success"] = $"Intake link sent to {dto.Email}.";
        return RedirectToAction(nameof(SendRemoteLink));
    }

    // =========================================================================
    // INTAKE STEPS  (anonymous — both InPerson and Remote)
    // =========================================================================

    // ── Step 1: Contact Info ──────────────────────────────────────────────────

    /// <summary>Displays the contact-info form for the given session token.</summary>
    [AllowAnonymous]
    public async Task<IActionResult> Contact(Guid token)
    {
        var session = await LoadSessionAsync(token);
        if (session == null) return View("KioskError", "This intake session could not be found. Please ask a staff member to start a new one.");
        if (!await ValidateSessionState(session)) return RedirectToAction(nameof(Confirmation), new { token });

        await PopulateKioskViewBagFromSession(session);
        ViewBag.KioskStep = 1;
        return View("Intake/Contact", new SubmitKioskContactDto
        {
            FirstName = session.CustomerFirstName,
            LastName  = session.CustomerLastName,
            Phone     = session.CustomerPhone,
            Email     = session.CustomerEmail,
            IsReturningCustomer = session.IsReturningCustomer
        });
    }

    /// <summary>Saves contact info to the session and advances to Step 2.</summary>
    [HttpPost, ValidateAntiForgeryToken]
    [AllowAnonymous]
    public async Task<IActionResult> Contact(Guid token, SubmitKioskContactDto dto)
    {
        var session = await LoadSessionAsync(token);
        if (session == null) return View("KioskError", "Session not found.");

        if (!ModelState.IsValid)
        {
            await PopulateKioskViewBagFromSession(session);
            ViewBag.KioskStep = 1;
            return View("Intake/Contact", dto);
        }

        session.CustomerFirstName   = dto.FirstName.Trim();
        session.CustomerLastName    = dto.LastName.Trim();
        session.CustomerPhone       = dto.Phone.Trim();
        session.CustomerEmail       = dto.Email.Trim().ToLowerInvariant();
        session.IsReturningCustomer = dto.IsReturningCustomer;

        await _unitOfWork.CompleteAsync();
        return RedirectToAction(nameof(Job), new { token });
    }

    // ── Step 2: Job Description ───────────────────────────────────────────────

    /// <summary>Displays the job-description form.</summary>
    [AllowAnonymous]
    public async Task<IActionResult> Job(Guid token)
    {
        var session = await LoadSessionAsync(token);
        if (session == null) return View("KioskError", "Session not found.");
        if (!await ValidateSessionState(session)) return RedirectToAction(nameof(Confirmation), new { token });

        await PopulateKioskViewBagFromSession(session);
        ViewBag.KioskStep = 2;
        return View("Intake/Job", new SubmitKioskJobDto
        {
            JobDescription       = session.JobDescription,
            HowDidYouHearAboutUs = session.HowDidYouHearAboutUs
        });
    }

    /// <summary>Saves the job description and advances to Step 3.</summary>
    [HttpPost, ValidateAntiForgeryToken]
    [AllowAnonymous]
    public async Task<IActionResult> Job(Guid token, SubmitKioskJobDto dto)
    {
        var session = await LoadSessionAsync(token);
        if (session == null) return View("KioskError", "Session not found.");

        if (!ModelState.IsValid)
        {
            await PopulateKioskViewBagFromSession(session);
            ViewBag.KioskStep = 2;
            return View("Intake/Job", dto);
        }

        session.JobDescription       = dto.JobDescription.Trim();
        session.HowDidYouHearAboutUs = dto.HowDidYouHearAboutUs?.Trim();

        await _unitOfWork.CompleteAsync();
        return RedirectToAction(nameof(Terms), new { token });
    }

    // ── Step 3: Terms & Consent ───────────────────────────────────────────────

    /// <summary>Displays the terms, SMS opt-in checkbox, and (for InPerson) signature pad.</summary>
    [AllowAnonymous]
    public async Task<IActionResult> Terms(Guid token)
    {
        var session = await LoadSessionAsync(token);
        if (session == null) return View("KioskError", "Session not found.");
        if (!await ValidateSessionState(session)) return RedirectToAction(nameof(Confirmation), new { token });

        await PopulateKioskViewBagFromSession(session);
        ViewBag.KioskStep  = 3;
        ViewBag.IsInPerson = session.SessionType == KioskSessionType.InPerson;
        return View("Intake/Terms", new SubmitKioskTermsDto());
    }

    /// <summary>
    /// Saves terms agreement, triggers customer/job auto-creation, fires staff notification,
    /// and redirects to the Confirmation screen.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    [AllowAnonymous]
    public async Task<IActionResult> Terms(Guid token, SubmitKioskTermsDto dto)
    {
        var session = await LoadSessionAsync(token);
        if (session == null) return View("KioskError", "Session not found.");

        // Expired/already-submitted sessions go straight to Confirmation
        if (!await ValidateSessionState(session)) return RedirectToAction(nameof(Confirmation), new { token });

        // Require signature for in-person sessions
        if (session.SessionType == KioskSessionType.InPerson &&
            string.IsNullOrEmpty(dto.SignatureDataBase64))
        {
            ModelState.AddModelError("SignatureDataBase64", "Please sign above before continuing.");
        }

        if (!ModelState.IsValid)
        {
            await PopulateKioskViewBagFromSession(session);
            ViewBag.KioskStep  = 3;
            ViewBag.IsInPerson = session.SessionType == KioskSessionType.InPerson;
            return View("Intake/Terms", dto);
        }

        session.AgreedToTerms       = true;
        session.AgreedToTermsAt     = DateTime.UtcNow;
        session.SmsOptIn            = dto.SmsOptIn;
        session.SignatureDataBase64  = dto.SignatureDataBase64;
        session.Status              = KioskSessionStatus.Submitted;
        session.SubmittedAt         = DateTime.UtcNow;

        try
        {
            await ProcessSubmissionAsync(session);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error processing kiosk submission for session {SessionToken}", token);
            // Customer-facing page always succeeds — staff can convert the session manually.
            // Persist the session's agreed/submitted state even if job creation failed.
            try { await _unitOfWork.CompleteAsync(); } catch { /* best-effort */ }
        }

        return RedirectToAction(nameof(Confirmation), new { token });
    }

    // ── Confirmation ──────────────────────────────────────────────────────────

    /// <summary>Thank-you screen shown after a successful submission.</summary>
    [AllowAnonymous]
    public async Task<IActionResult> Confirmation(Guid token)
    {
        var session = await LoadSessionAsync(token);
        if (session == null) return View("KioskError", "Session not found.");

        await PopulateKioskViewBagFromSession(session);
        ViewBag.ShowInactivityTimer = false; // Handled by the countdown JS in the view
        ViewBag.IsInPerson          = session.SessionType == KioskSessionType.InPerson;
        ViewBag.FirstName           = session.CustomerFirstName;
        return View("Intake/Confirmation");
    }

    // =========================================================================
    // STAFF REVIEW  (authenticated)
    // =========================================================================

    /// <summary>
    /// Lists all kiosk intake sessions for the current company — submitted, active, and expired.
    /// Manager or higher access required.
    /// </summary>
    [Authorize]
    public async Task<IActionResult> Intakes(string? filter)
    {
        var companyId = GetCurrentCompanyId();
        var sessions = await _unitOfWork.KioskSessions.FindAsync(
            s => s.CompanyId == companyId, false,
            s => s.LinkedCustomer,
            s => s.LinkedJob);

        var dtos = sessions
            .OrderByDescending(s => s.CreatedAt)
            .Select(s => new KioskSessionListDto
            {
                Id               = s.Id,
                SessionToken     = s.SessionToken,
                SessionType      = s.SessionType,
                Status           = s.Status,
                CustomerFirstName = s.CustomerFirstName,
                CustomerLastName  = s.CustomerLastName,
                CustomerEmail    = s.CustomerEmail,
                CustomerPhone    = s.CustomerPhone,
                JobDescription   = s.JobDescription,
                SmsOptIn         = s.SmsOptIn,
                SubmittedAt      = s.SubmittedAt,
                ExpiresAt        = s.ExpiresAt,
                LinkedCustomerId = s.LinkedCustomerId,
                LinkedJobId      = s.LinkedJobId,
                LinkedQuoteId    = s.LinkedQuoteId,
                RemoteLinkEmail  = s.RemoteLinkEmail
            })
            .ToList();

        // Apply filter tab
        dtos = filter switch
        {
            "submitted" => dtos.Where(d => d.Status == KioskSessionStatus.Submitted).ToList(),
            "active"    => dtos.Where(d => d.Status == KioskSessionStatus.Active && !d.IsExpired).ToList(),
            "expired"   => dtos.Where(d => d.IsExpired || d.Status == KioskSessionStatus.Expired).ToList(),
            _           => dtos
        };

        ViewBag.ActiveFilter = filter ?? "all";
        return View(dtos);
    }

    // =========================================================================
    // PRIVATE HELPERS
    // =========================================================================

    /// <summary>
    /// Loads a KioskSession by SessionToken using ignoreQueryFilters because anonymous requests
    /// have no CompanyId claim, so the global tenant filter would return nothing without it.
    /// </summary>
    private async Task<KioskSession?> LoadSessionAsync(Guid token)
    {
        return await _unitOfWork.KioskSessions.FirstOrDefaultAsync(
            s => s.SessionToken == token && !s.IsDeleted,
            ignoreQueryFilters: true);
    }

    /// <summary>
    /// Validates that the session is still in a usable state.
    /// Returns false (and optionally updates status to Expired) if the session should not proceed.
    /// </summary>
    private async Task<bool> ValidateSessionState(KioskSession session)
    {
        if (session.Status == KioskSessionStatus.Submitted)
            return false; // Already done — redirect to Confirmation (idempotent)

        if (session.Status == KioskSessionStatus.Cancelled)
            return false;

        if (DateTime.UtcNow > session.ExpiresAt && session.Status == KioskSessionStatus.Active)
        {
            session.Status = KioskSessionStatus.Expired;
            await _unitOfWork.CompleteAsync();
            return false;
        }

        return session.Status == KioskSessionStatus.Active;
    }

    /// <summary>
    /// Core submission logic: matches or creates a Customer, creates a Pending Job,
    /// applies SMS consent, and fires a staff in-app notification.
    /// CompanyId is set explicitly on new entities from session.CompanyId so the EF
    /// SaveChanges interceptor does not override it with 0 (the anonymous tenant context).
    /// </summary>
    private async Task ProcessSubmissionAsync(KioskSession session)
    {
        var companyId = session.CompanyId;

        // 1. Match or create Customer
        Customer? customer = null;
        if (!string.IsNullOrEmpty(session.CustomerEmail))
        {
            customer = await _unitOfWork.Customers.FirstOrDefaultAsync(
                c => c.CompanyId == companyId && c.Email == session.CustomerEmail && !c.IsDeleted,
                ignoreQueryFilters: true);
        }

        if (customer == null && !string.IsNullOrEmpty(session.CustomerPhone))
        {
            customer = await _unitOfWork.Customers.FirstOrDefaultAsync(
                c => c.CompanyId == companyId && (c.Phone == session.CustomerPhone || c.MobilePhone == session.CustomerPhone) && !c.IsDeleted,
                ignoreQueryFilters: true);
        }

        bool isNewCustomer = customer == null;
        if (isNewCustomer)
        {
            customer = new Customer
            {
                CompanyId          = companyId,
                ContactFirstName   = session.CustomerFirstName,
                ContactLastName    = session.CustomerLastName,
                Phone              = session.CustomerPhone,
                Email              = session.CustomerEmail,
                IsActive           = true,
                IsCommercial       = false
            };
            await _unitOfWork.Customers.AddAsync(customer);
            await _unitOfWork.CompleteAsync(); // get Customer.Id
        }

        // 2. Apply SMS consent
        if (session.SmsOptIn)
        {
            customer!.NotifyBySms      = true;
            customer.SmsConsentedAt    = session.SubmittedAt ?? DateTime.UtcNow;
            customer.SmsConsentMethod  = session.SessionType == KioskSessionType.InPerson
                                            ? "KioskIntake"
                                            : "RemoteIntake";
        }

        // 3. Resolve company preference: create a Quote (default) or a Job
        var prefs          = await _unitOfWork.CompanyPreferences.FirstOrDefaultAsync(
            p => p.CompanyId == companyId && !p.IsDeleted, ignoreQueryFilters: true);
        var intakeOutput   = prefs?.KioskIntakeOutput ?? "Quote";
        var createQuote    = !string.Equals(intakeOutput, "Job", StringComparison.OrdinalIgnoreCase);

        session.LinkedCustomerId = customer!.Id;

        if (createQuote)
        {
            // 3a. Create a Draft Quote so staff can price and send for approval
            var quoteStatuses = await _lookupCache.GetQuoteStatusLookupsAsync(companyId);
            var draftStatus   = quoteStatuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Quote.Draft);
            if (draftStatus == null)
                throw new InvalidOperationException($"No Draft quote status found for company {companyId}. Run Seed Data from Platform Management.");

            var quoteNumber = await GenerateQuoteNumberAsync(companyId);
            var quote = new Quote
            {
                CompanyId       = companyId,
                CustomerId      = customer.Id,
                QuoteNumber     = quoteNumber,
                QuoteStatusId   = draftStatus.Id,
                Description     = session.JobDescription,
                Notes           = $"Source: {session.SessionType} kiosk intake",
                QuoteDate       = DateTime.UtcNow,
                ExpirationDate  = DateTime.UtcNow.AddDays(prefs?.DefaultQuoteValidityDays ?? 30)
            };

            await _unitOfWork.Quotes.AddAsync(quote);
            await _unitOfWork.CompleteAsync();  // quote.Id now valid

            session.LinkedQuoteId = quote.Id;
        }
        else
        {
            // 3b. Create a Pending Job directly (for shops that price on the spot)
            var jobStatuses   = await _lookupCache.GetJobStatusLookupsAsync(companyId);
            var pendingStatus = jobStatuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Job.Pending);
            if (pendingStatus == null)
                throw new InvalidOperationException($"No Pending job status found for company {companyId}. Run Seed Data from Platform Management.");

            var priorities     = await _lookupCache.GetJobPriorityLookupsAsync(companyId);
            var normalPriority = priorities.FirstOrDefault(p => p.PriorityCode == "NORMAL")
                              ?? priorities.FirstOrDefault();
            if (normalPriority == null)
                throw new InvalidOperationException($"No job priority rows found for company {companyId}. Run Seed Data from Platform Management.");

            var jobNumber = await GenerateJobNumberAsync(companyId);
            var job = new Job
            {
                CompanyId           = companyId,
                CustomerId          = customer.Id,
                JobNumber           = jobNumber,
                JobStatusId         = pendingStatus.Id,
                JobPriorityId       = normalPriority.Id,
                Description         = session.JobDescription,
                SpecialInstructions = $"Source: {session.SessionType} kiosk intake"
            };

            await _unitOfWork.Jobs.AddAsync(job);
            await _unitOfWork.CompleteAsync();  // job.Id now valid

            session.LinkedJobId = job.Id;
        }

        // 4. Persist session links
        await _unitOfWork.CompleteAsync();

        // 5. Fire staff notification
        var jobDesc   = session.JobDescription ?? "";
        var snippet   = jobDesc.Length > 60 ? jobDesc[..60] + "…" : jobDesc;
        var fullName  = $"{session.CustomerFirstName} {session.CustomerLastName}".Trim();
        var intakeLabel = session.SessionType == KioskSessionType.Remote ? "Remote Intake" : "Walk-in Intake";
        await _inApp.CreateAsync(
            companyId,
            $"{intakeLabel} Submitted",
            $"{fullName} completed their intake form — {snippet}",
            "KioskIntake",
            link: $"/Kiosk/Intakes",
            customerId: customer.Id);
    }

    /// <summary>
    /// Generates the next sequential quote number using the company's configured prefix.
    /// Mirrors GenerateQuoteNumberAsync in QuotesController — same format: PREFIX-YYMM-####.
    /// Implemented here because KioskController processes anonymous requests and cannot
    /// rely on ITenantContext to resolve the company ID.
    /// </summary>
    private async Task<string> GenerateQuoteNumberAsync(int companyId)
    {
        var now = DateTime.UtcNow;

        var prefs = await _unitOfWork.CompanyPreferences.FirstOrDefaultAsync(
            p => p.CompanyId == companyId && !p.IsDeleted, ignoreQueryFilters: true);

        var quotePrefix = !string.IsNullOrWhiteSpace(prefs?.QuoteNumberPrefix) ? prefs.QuoteNumberPrefix : "QT";
        var prefix      = $"{quotePrefix}-{now:yy}{now:MM}";

        var lastQuoteNumber = await _unitOfWork.Quotes.GetLastQuoteNumberByPrefixAsync(companyId, prefix);

        if (lastQuoteNumber != null)
        {
            var lastNumberStr = lastQuoteNumber[(prefix.Length + 1)..];
            if (int.TryParse(lastNumberStr, out int lastNumber))
                return $"{prefix}-{(lastNumber + 1):D4}";
        }

        return $"{prefix}-0001";
    }

    /// <summary>
    /// Generates the next sequential job number using the company's configured prefix.
    /// Mirrors the logic in JobsController.GenerateJobNumber() — same format: PREFIX-YYMM-####.
    /// </summary>
    private async Task<string> GenerateJobNumberAsync(int companyId)
    {
        var year   = DateTime.Now.Year.ToString()[2..];
        var month  = DateTime.Now.Month.ToString("D2");

        var prefs = await _unitOfWork.CompanyPreferences.FirstOrDefaultAsync(
            p => p.CompanyId == companyId && !p.IsDeleted, ignoreQueryFilters: true);

        var jobPrefix = !string.IsNullOrWhiteSpace(prefs?.JobNumberPrefix) ? prefs.JobNumberPrefix : "JOB";
        var prefix    = $"{jobPrefix}-{year}{month}";

        var lastJobNumber = await _unitOfWork.Jobs.GetLastJobNumberByPrefixAsync(companyId, prefix);

        if (lastJobNumber != null)
        {
            var lastNumberStr = lastJobNumber[(prefix.Length + 1)..];
            if (int.TryParse(lastNumberStr, out int lastNumber))
                return $"{prefix}-{(lastNumber + 1):D4}";
        }

        return $"{prefix}-0001";
    }

    /// <summary>
    /// Reads the KioskDevice cookie and parses the "{companyId}:{token}" value.
    /// Returns null if the cookie is absent or malformed.
    /// </summary>
    private (int companyId, string token)? ReadKioskCookie()
    {
        if (!Request.Cookies.TryGetValue(CookieName, out var raw) || string.IsNullOrEmpty(raw))
            return null;

        var parts = raw.Split(':', 2);
        if (parts.Length != 2 || !int.TryParse(parts[0], out int id))
            return null;

        return (id, parts[1]);
    }

    /// <summary>Writes a long-lived HttpOnly kiosk device cookie.</summary>
    private void WriteKioskCookie(int companyId, string token)
    {
        Response.Cookies.Append(CookieName, $"{companyId}:{token}", new CookieOptions
        {
            HttpOnly  = true,
            Secure    = true,
            SameSite  = SameSiteMode.Lax,
            MaxAge    = TimeSpan.FromDays(365)
        });
    }

    /// <summary>Removes the kiosk device cookie (deactivation).</summary>
    private void DeleteKioskCookie()
    {
        Response.Cookies.Delete(CookieName);
    }

    /// <summary>Returns the current authenticated user's CompanyId claim.</summary>
    private int GetCurrentCompanyId()
    {
        var claim = User.FindFirst("CompanyId")?.Value;
        return int.TryParse(claim, out int id) ? id : 0;
    }

    /// <summary>Sets ViewBag properties needed by _KioskLayout from a Company entity.</summary>
    private async Task PopulateKioskViewBag(Company company)
    {
        ViewBag.CompanyId      = company.Id;
        ViewBag.CompanyName    = company.CompanyName;
        ViewBag.CompanyLogoUrl = !string.IsNullOrEmpty(company.LogoFilePath)
            ? Url.Action("Logo", "Kiosk")
            : null;
        ViewBag.WelcomeUrl = "/Kiosk/Welcome";

        // Pass the intake output setting so Terms.cshtml can show matching wording
        var prefs = await _unitOfWork.CompanyPreferences.FirstOrDefaultAsync(
            p => p.CompanyId == company.Id && !p.IsDeleted, ignoreQueryFilters: true);
        ViewBag.KioskIntakeOutput = prefs?.KioskIntakeOutput ?? "Quote";
    }

    /// <summary>Loads the company from a session's CompanyId and populates ViewBag.</summary>
    private async Task PopulateKioskViewBagFromSession(KioskSession session)
    {
        var company = await _unitOfWork.Companies.GetByIdAsync(session.CompanyId, ignoreQueryFilters: true);
        if (company != null)
            await PopulateKioskViewBag(company);

        ViewBag.SessionToken = session.SessionToken;
        ViewBag.SessionType  = session.SessionType;

        // In-person kiosk: reset to Welcome screen after 45 s of inactivity so an
        // abandoned tablet doesn't stay on a customer's half-filled form indefinitely.
        // Remote sessions: customer is on their own phone — never redirect; they may
        // take several minutes between steps and have no KioskDevice cookie anyway.
        if (session.SessionType == KioskSessionType.InPerson)
            ViewBag.InactivityTimeoutMs = 45_000;
        else
            ViewBag.ShowInactivityTimer = false;
    }
}
