using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using PowderCoating.Application.DTOs.Timeclock;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;

namespace PowderCoating.Web.Controllers;

/// <summary>
/// Handles employee facility-level timeclock: clock in/out, manager edits, and the
/// tablet kiosk page with PIN-based authentication.
/// Two authentication modes:
///   - Main app actions: standard [Authorize] cookie auth (normal nav users)
///   - Kiosk actions:    device-cookie auth (shared tablet, no user login required)
/// </summary>
[Authorize]
public class TimeclockController : Controller
{
    private const string KioskCookieName = "TimeclockKioskDevice";

    private readonly IUnitOfWork _unitOfWork;
    private readonly IMapper _mapper;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly IPasswordHasher<ApplicationUser> _passwordHasher;
    private readonly ILogger<TimeclockController> _logger;

    /// <summary>Initialises dependencies for the timeclock controller.</summary>
    public TimeclockController(
        IUnitOfWork unitOfWork,
        IMapper mapper,
        UserManager<ApplicationUser> userManager,
        IPasswordHasher<ApplicationUser> passwordHasher,
        ILogger<TimeclockController> logger)
    {
        _unitOfWork     = unitOfWork;
        _mapper         = mapper;
        _userManager    = userManager;
        _passwordHasher = passwordHasher;
        _logger         = logger;
    }

    // =========================================================================
    // MAIN APP — dashboard
    // =========================================================================

    /// <summary>
    /// Timeclock dashboard: current user's punch button, "who's in" grid, and personal history.
    /// </summary>
    public async Task<IActionResult> Index()
    {
        var currentUser = await _userManager.GetUserAsync(User);
        if (currentUser == null) return Forbid();

        var companyId = currentUser.CompanyId;
        var company   = await _unitOfWork.Companies.GetByIdAsync(companyId);

        // Current user's open entry (null = clocked out)
        var openEntry = await _unitOfWork.EmployeeClockEntries.FirstOrDefaultAsync(
            e => e.UserId == currentUser.Id && e.ClockOutTime == null && e.CompanyId == companyId);

        // All open entries for the "Who's In" table
        var activeEntries = await _unitOfWork.EmployeeClockEntries.FindAsync(
            e => e.ClockOutTime == null && e.CompanyId == companyId,
            false,
            e => e.User);

        // Current user's last 14 days of entries
        var since = DateTime.UtcNow.Date.AddDays(-14);
        var myHistory = await _unitOfWork.EmployeeClockEntries.FindAsync(
            e => e.UserId == currentUser.Id && e.ClockInTime >= since && e.CompanyId == companyId);

        var isManager = currentUser.CompanyRole is "CompanyAdmin" or "Manager";

        // Load company employees for the manual-entry dropdown (managers only)
        List<(string Id, string Name)> companyUsers = new();
        if (isManager)
        {
            companyUsers = await _userManager.Users
                .Where(u => u.CompanyId == companyId && u.IsActive)
                .OrderBy(u => u.FirstName).ThenBy(u => u.LastName)
                .Select(u => new { u.Id, u.FirstName, u.LastName })
                .ToListAsync()
                .ContinueWith(t => t.Result
                    .Select(u => (u.Id, $"{u.FirstName} {u.LastName}".Trim()))
                    .ToList());
        }

        ViewBag.CurrentUser          = currentUser;
        ViewBag.OpenEntry            = openEntry != null ? MapEntry(openEntry, currentUser) : null;
        ViewBag.ActiveEntries        = activeEntries.Select(e => MapEntry(e, e.User)).OrderBy(e => e.ClockInTime).ToList();
        ViewBag.MyHistory            = myHistory.OrderByDescending(e => e.ClockInTime).ToList();
        ViewBag.IsManager            = isManager;
        ViewBag.NowUtc               = DateTime.UtcNow;
        ViewBag.CompanyUsers         = companyUsers;
        ViewBag.AllowMultiplePunches = company?.TimeclockAllowMultiplePunchesPerDay ?? true;

        return View();
    }

    // =========================================================================
    // MAIN APP — clock in / out / status
    // =========================================================================

    /// <summary>Returns the current user's open clock entry, or null if clocked out.</summary>
    [HttpGet]
    public async Task<IActionResult> MyStatus()
    {
        var userId    = _userManager.GetUserId(User)!;
        var companyId = GetCurrentCompanyId();

        var openEntry = await _unitOfWork.EmployeeClockEntries.FirstOrDefaultAsync(
            e => e.UserId == userId && e.ClockOutTime == null && e.CompanyId == companyId);

        if (openEntry == null) return Json(new { isClockedIn = false });

        var elapsed = (decimal)(DateTime.UtcNow - openEntry.ClockInTime).TotalHours;
        return Json(new
        {
            isClockedIn = true,
            entryId     = openEntry.Id,
            clockInTime = openEntry.ClockInTime,
            elapsedHours = Math.Round(elapsed, 2)
        });
    }

    /// <summary>Returns all employees with an open clock entry (for the "Who's In" widget).</summary>
    [HttpGet]
    public async Task<IActionResult> ActiveNow()
    {
        var companyId = GetCurrentCompanyId();

        var active = await _unitOfWork.EmployeeClockEntries.FindAsync(
            e => e.ClockOutTime == null && e.CompanyId == companyId,
            false,
            e => e.User);

        var result = active.Select(e => new
        {
            userId      = e.UserId,
            displayName = e.User?.FullName ?? "Unknown",
            clockInTime = e.ClockInTime,
            entryId     = e.Id,
            entryType   = (int)e.EntryType   // 0=Work, 1=Break, 2=Lunch
        });

        return Json(result);
    }

    /// <summary>
    /// Clocks the current user in. Enforces three company-level timeclock settings:
    /// (1) auto-closes stale open entries if AutoClockOutHours is set,
    /// (2) blocks a second punch today if AllowMultiplePunchesPerDay is false,
    /// (3) blocks immediately if already clocked in.
    /// </summary>
    [HttpPost]
    public async Task<IActionResult> ClockIn([FromBody] ClockInRequest request)
    {
        var userId    = _userManager.GetUserId(User)!;
        var companyId = GetCurrentCompanyId();
        var company   = await _unitOfWork.Companies.GetByIdAsync(companyId);
        if (company == null) return Forbid();

        var now = DateTime.UtcNow;

        // Auto clock-out stale open entries if configured
        if (company.TimeclockAutoClockOutHours is > 0)
        {
            var cutoff = now.AddHours(-company.TimeclockAutoClockOutHours.Value);
            var stale  = await _unitOfWork.EmployeeClockEntries.FindAsync(
                e => e.UserId == userId && e.ClockOutTime == null
                  && e.CompanyId == companyId && e.ClockInTime <= cutoff);
            foreach (var staleEntry in stale)
            {
                staleEntry.ClockOutTime = staleEntry.ClockInTime.AddHours(company.TimeclockAutoClockOutHours.Value);
                staleEntry.HoursWorked  = company.TimeclockAutoClockOutHours.Value;
                staleEntry.Notes        = (staleEntry.Notes + " [Auto clocked out]").Trim();
            }
            if (stale.Any()) await _unitOfWork.CompleteAsync();
        }

        // Still clocked in?
        var alreadyOpen = await _unitOfWork.EmployeeClockEntries.FirstOrDefaultAsync(
            e => e.UserId == userId && e.ClockOutTime == null && e.CompanyId == companyId);
        if (alreadyOpen != null)
            return BadRequest(new { message = "You are already clocked in. Please clock out first." });

        // Block second punch today if setting disallows it
        if (!company.TimeclockAllowMultiplePunchesPerDay)
        {
            var todayStart = now.Date;
            var punchedToday = await _unitOfWork.EmployeeClockEntries.AnyAsync(
                e => e.UserId == userId && e.CompanyId == companyId && e.ClockInTime >= todayStart);
            if (punchedToday)
                return BadRequest(new { message = "Multiple clock-ins per day are not allowed. You have already clocked in today." });
        }

        var entry = new EmployeeClockEntry
        {
            UserId      = userId,
            CompanyId   = companyId,
            ClockInTime = now,
            Notes       = request.Notes?.Trim()
        };

        await _unitOfWork.EmployeeClockEntries.AddAsync(entry);
        await _unitOfWork.CompleteAsync();

        _logger.LogInformation("User {UserId} clocked in (entry {EntryId})", userId, entry.Id);
        return Json(new { success = true, entryId = entry.Id, clockInTime = entry.ClockInTime });
    }

    /// <summary>Clocks the current user out, storing the elapsed hours on the entry.</summary>
    [HttpPost]
    public async Task<IActionResult> ClockOut([FromBody] ClockOutRequest request)
    {
        var userId    = _userManager.GetUserId(User)!;
        var companyId = GetCurrentCompanyId();

        var entry = await _unitOfWork.EmployeeClockEntries.GetByIdAsync(request.EntryId);
        if (entry == null || entry.UserId != userId || entry.CompanyId != companyId)
            return NotFound(new { message = "Clock entry not found." });

        if (entry.ClockOutTime != null)
            return BadRequest(new { message = "This entry is already clocked out." });

        var now = DateTime.UtcNow;
        entry.ClockOutTime = now;
        entry.HoursWorked  = Math.Round((decimal)(now - entry.ClockInTime).TotalHours, 2);
        if (!string.IsNullOrWhiteSpace(request.Notes))
            entry.Notes = request.Notes.Trim();

        await _unitOfWork.CompleteAsync();

        _logger.LogInformation("User {UserId} clocked out (entry {EntryId}, {Hours}h)", userId, entry.Id, entry.HoursWorked);
        return Json(new { success = true, hoursWorked = entry.HoursWorked, clockOutTime = entry.ClockOutTime });
    }

    // =========================================================================
    // MAIN APP — manager: history, edit, delete
    // =========================================================================

    /// <summary>Returns paginated clock entries. Managers can pass any userId; others only see their own.</summary>
    [HttpGet]
    public async Task<IActionResult> History(DateTime? from, DateTime? to, string? userId)
    {
        var currentUser = await _userManager.GetUserAsync(User);
        if (currentUser == null) return Forbid();

        var companyId  = currentUser.CompanyId;
        var isManager  = currentUser.CompanyRole is "CompanyAdmin" or "Manager";
        var filterUser = isManager ? userId : currentUser.Id;

        var start = (from ?? DateTime.UtcNow.AddDays(-30)).Date;
        var end   = (to ?? DateTime.UtcNow).Date.AddDays(1);

        var entries = await _unitOfWork.EmployeeClockEntries.FindAsync(
            e => e.CompanyId == companyId
              && e.ClockInTime >= start
              && e.ClockInTime < end
              && (filterUser == null || e.UserId == filterUser),
            false,
            e => e.User);

        var dtos = entries.OrderByDescending(e => e.ClockInTime)
            .Select(e => MapEntry(e, e.User))
            .ToList();

        return Json(dtos);
    }

    /// <summary>Manager-only: edit clock-in/out times or notes on any entry in the company.</summary>
    [HttpPost]
    public async Task<IActionResult> Edit([FromBody] EditClockEntryRequest request)
    {
        if (!await IsManagerAsync()) return Forbid();

        var companyId = GetCurrentCompanyId();
        var entry = await _unitOfWork.EmployeeClockEntries.GetByIdAsync(request.Id);
        if (entry == null || entry.CompanyId != companyId)
            return NotFound(new { message = "Entry not found." });

        entry.ClockInTime  = request.ClockInTime;
        entry.ClockOutTime = request.ClockOutTime;
        entry.Notes        = request.Notes?.Trim();

        if (request.ClockOutTime.HasValue)
            entry.HoursWorked = Math.Round(
                (decimal)(request.ClockOutTime.Value - request.ClockInTime).TotalHours, 2);
        else
            entry.HoursWorked = null;

        await _unitOfWork.CompleteAsync();
        return Json(new { success = true });
    }

    /// <summary>Manager-only: soft-delete a clock entry.</summary>
    [HttpPost]
    public async Task<IActionResult> Delete([FromBody] int id)
    {
        if (!await IsManagerAsync()) return Forbid();

        var companyId = GetCurrentCompanyId();
        var entry = await _unitOfWork.EmployeeClockEntries.GetByIdAsync(id);
        if (entry == null || entry.CompanyId != companyId)
            return NotFound(new { message = "Entry not found." });

        await _unitOfWork.EmployeeClockEntries.SoftDeleteAsync(id);
        await _unitOfWork.CompleteAsync();
        return Json(new { success = true });
    }

    /// <summary>
    /// Closes the current open Work entry and opens a Break or Lunch entry.
    /// The caller must have an open Work segment; calling while already on a break returns 400.
    /// Multi-punches-per-day setting is intentionally bypassed — taking a break is inherently
    /// a second punch in the same day and should always be allowed.
    /// </summary>
    [HttpPost]
    public async Task<IActionResult> GoOnBreak([FromBody] GoOnBreakRequest request)
    {
        if (request.BreakType is not (ClockEntryType.Break or ClockEntryType.Lunch))
            return BadRequest(new { message = "Invalid break type." });

        var userId    = _userManager.GetUserId(User)!;
        var companyId = GetCurrentCompanyId();

        var openEntry = await _unitOfWork.EmployeeClockEntries.FirstOrDefaultAsync(
            e => e.UserId == userId && e.ClockOutTime == null && e.CompanyId == companyId);

        if (openEntry == null)
            return BadRequest(new { message = "You are not currently clocked in." });
        if (openEntry.EntryType != ClockEntryType.Work)
            return BadRequest(new { message = "You are already on a break." });

        var now = DateTime.UtcNow;

        // Close the work segment
        openEntry.ClockOutTime = now;
        openEntry.HoursWorked  = Math.Round((decimal)(now - openEntry.ClockInTime).TotalHours, 2);

        // Open the break/lunch segment
        var breakEntry = new EmployeeClockEntry
        {
            UserId      = userId,
            CompanyId   = companyId,
            ClockInTime = now,
            EntryType   = request.BreakType
        };
        await _unitOfWork.EmployeeClockEntries.AddAsync(breakEntry);
        await _unitOfWork.CompleteAsync();

        _logger.LogInformation("User {UserId} started {BreakType} (entry {EntryId})", userId, request.BreakType, breakEntry.Id);
        return Json(new { success = true, entryId = breakEntry.Id });
    }

    /// <summary>
    /// Closes the current open Break/Lunch entry and opens a new Work entry.
    /// The caller must have an open Break or Lunch segment; calling while working returns 400.
    /// </summary>
    [HttpPost]
    public async Task<IActionResult> ReturnFromBreak()
    {
        var userId    = _userManager.GetUserId(User)!;
        var companyId = GetCurrentCompanyId();

        var openEntry = await _unitOfWork.EmployeeClockEntries.FirstOrDefaultAsync(
            e => e.UserId == userId && e.ClockOutTime == null && e.CompanyId == companyId);

        if (openEntry == null || openEntry.EntryType == ClockEntryType.Work)
            return BadRequest(new { message = "You are not currently on a break." });

        var now = DateTime.UtcNow;

        // Close the break segment
        openEntry.ClockOutTime = now;
        openEntry.HoursWorked  = Math.Round((decimal)(now - openEntry.ClockInTime).TotalHours, 2);

        // Open a new work segment
        var workEntry = new EmployeeClockEntry
        {
            UserId      = userId,
            CompanyId   = companyId,
            ClockInTime = now,
            EntryType   = ClockEntryType.Work
        };
        await _unitOfWork.EmployeeClockEntries.AddAsync(workEntry);
        await _unitOfWork.CompleteAsync();

        _logger.LogInformation("User {UserId} returned from {BreakType} (new entry {EntryId})", userId, openEntry.EntryType, workEntry.Id);
        return Json(new { success = true, entryId = workEntry.Id });
    }

    /// <summary>
    /// Manager-only: create a manual time entry for any active employee in the company.
    /// Used to record missed punches, paper timesheets, or schedule corrections.
    /// ClockInTime and ClockOutTime are expected as UTC ISO strings from the JS client.
    /// </summary>
    [HttpPost]
    public async Task<IActionResult> ManualEntry([FromBody] ManualEntryRequest request)
    {
        if (!await IsManagerAsync()) return Forbid();

        var companyId = GetCurrentCompanyId();

        // Verify the target user belongs to this company
        var targetUser = await _userManager.FindByIdAsync(request.UserId);
        if (targetUser == null || targetUser.CompanyId != companyId || !targetUser.IsActive)
            return NotFound(new { message = "Employee not found." });

        if (request.ClockOutTime.HasValue && request.ClockOutTime.Value <= request.ClockInTime)
            return BadRequest(new { message = "Clock-out time must be after clock-in time." });

        var entry = new EmployeeClockEntry
        {
            UserId      = request.UserId,
            CompanyId   = companyId,
            ClockInTime = request.ClockInTime,
            ClockOutTime = request.ClockOutTime,
            Notes       = request.Notes?.Trim()
        };

        if (request.ClockOutTime.HasValue)
            entry.HoursWorked = Math.Round(
                (decimal)(request.ClockOutTime.Value - request.ClockInTime).TotalHours, 2);

        await _unitOfWork.EmployeeClockEntries.AddAsync(entry);
        await _unitOfWork.CompleteAsync();

        _logger.LogInformation(
            "Manager {ManagerId} created manual entry for {TargetUserId} (entry {EntryId})",
            _userManager.GetUserId(User), request.UserId, entry.Id);

        return Json(new { success = true, entryId = entry.Id });
    }

    // =========================================================================
    // KIOSK — device activation
    // =========================================================================

    /// <summary>
    /// Shows the kiosk activation form where a manager can optionally name the device
    /// before activating it. Navigate to this URL on each tablet to register it.
    /// </summary>
    [HttpGet("Timeclock/Kiosk/Activate")]
    public async Task<IActionResult> KioskActivate()
    {
        if (!await IsManagerAsync()) return Forbid();
        return View();
    }

    /// <summary>
    /// Creates a new <see cref="TimeclockKioskDevice"/> row and writes the device cookie for
    /// this tablet. Multiple devices per company are supported — each tablet gets its own token.
    /// </summary>
    [HttpPost("Timeclock/Kiosk/Activate")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> KioskActivatePost(string? deviceName)
    {
        if (!await IsManagerAsync()) return Forbid();

        var companyId = GetCurrentCompanyId();
        var token     = Guid.NewGuid().ToString("N");

        var device = new TimeclockKioskDevice
        {
            CompanyId   = companyId,
            Token       = token,
            DeviceName  = string.IsNullOrWhiteSpace(deviceName) ? null : deviceName.Trim(),
            ActivatedAt = DateTime.UtcNow
        };

        await _unitOfWork.TimeclockKioskDevices.AddAsync(device);
        await _unitOfWork.CompleteAsync();

        WriteTimeclockKioskCookie(companyId, token);

        TempData["Success"] = "Kiosk activated on this device.";
        return RedirectToAction(nameof(Kiosk));
    }

    /// <summary>Deactivates a specific kiosk device by ID. Deletes its row so the cookie is invalidated on next use.</summary>
    [HttpPost]
    public async Task<IActionResult> KioskDeactivate(int id)
    {
        if (!await IsManagerAsync()) return Json(new { success = false, message = "Forbidden." });

        var companyId = GetCurrentCompanyId();
        var device    = await _unitOfWork.TimeclockKioskDevices.GetByIdAsync(id);
        if (device == null || device.CompanyId != companyId)
            return Json(new { success = false, message = "Device not found." });

        await _unitOfWork.TimeclockKioskDevices.DeleteAsync(device);
        await _unitOfWork.CompleteAsync();

        // If the current browser holds this device's cookie, clear it too
        var cookie = ReadTimeclockKioskCookie();
        if (cookie?.token == device.Token)
            DeleteTimeclockKioskCookie();

        return Json(new { success = true });
    }

    /// <summary>Returns the list of active kiosk devices for this company. Used by the Settings tab.</summary>
    [HttpGet]
    public async Task<IActionResult> KioskDevices()
    {
        if (!await IsManagerAsync()) return Forbid();

        var companyId = GetCurrentCompanyId();
        var devices   = await _unitOfWork.TimeclockKioskDevices.FindAsync(
            d => d.CompanyId == companyId);

        var result = devices.OrderBy(d => d.ActivatedAt).Select(d => new
        {
            id          = d.Id,
            deviceName  = d.DeviceName ?? "Unnamed Device",
            activatedAt = d.ActivatedAt,
            lastSeenAt  = d.LastSeenAt
        });

        return Json(result);
    }

    // =========================================================================
    // KIOSK — tablet UI
    // =========================================================================

    /// <summary>
    /// Tablet timeclock kiosk page. Requires device-cookie auth; no user login needed.
    /// Validates the cookie token against <see cref="TimeclockKioskDevice"/> rows (supports multiple tablets per company).
    /// </summary>
    [AllowAnonymous]
    [HttpGet("Timeclock/Kiosk")]
    public async Task<IActionResult> Kiosk()
    {
        var (device, company) = await ValidateKioskCookieAsync();
        if (device == null || company == null)
            return View("KioskError", "This device is not activated as a timeclock kiosk. Ask a manager to activate it at Timeclock &rsaquo; Activate Kiosk.");

        if (!company.TimeclockEnabled)
            return View("KioskError", "Timeclock is currently disabled for this company.");

        await TouchDeviceAsync(device);

        ViewBag.CompanyName = company.CompanyName;
        ViewBag.CompanyId   = company.Id;
        return View();
    }

    /// <summary>
    /// Returns the list of active employees who have a kiosk PIN set, along with their current clock-in status.
    /// Called by the kiosk page on load and after each punch.
    /// </summary>
    [AllowAnonymous, HttpGet]
    public async Task<IActionResult> KioskEmployees()
    {
        var cookie = ReadTimeclockKioskCookie();
        if (cookie == null) return Forbid();

        // Validate device (no need to touch LastSeenAt on every poll)
        var device = await _unitOfWork.TimeclockKioskDevices.FirstOrDefaultAsync(
            d => d.CompanyId == cookie.Value.companyId && d.Token == cookie.Value.token,
            true);
        if (device == null) return Forbid();

        var companyId = cookie.Value.companyId;

        var users = await _userManager.Users
            .Where(u => u.CompanyId == companyId && u.IsActive && u.KioskPin != null)
            .ToListAsync();

        // Get current open entries for this company
        var openUserIds = (await _unitOfWork.EmployeeClockEntries.FindAsync(
                e => e.ClockOutTime == null && e.CompanyId == companyId))
            .Select(e => e.UserId)
            .ToHashSet();

        var employees = users.Select(u => new KioskEmployeeDto
        {
            UserId      = u.Id,
            DisplayName = u.FullName,
            Initials    = BuildInitials(u),
            IsClockedIn = openUserIds.Contains(u.Id)
        }).OrderBy(e => e.DisplayName).ToList();

        return Json(employees);
    }

    /// <summary>
    /// Kiosk punch endpoint: validates PIN and clocks the employee in or out automatically.
    /// Returns the action taken ("clockIn" or "clockOut") and the segment/daily totals.
    /// </summary>
    [AllowAnonymous, HttpPost]
    public async Task<IActionResult> KioskPunch([FromBody] KioskPunchRequest request)
    {
        var cookie = ReadTimeclockKioskCookie();
        if (cookie == null) return Forbid();

        var companyId = cookie.Value.companyId;

        var device = await _unitOfWork.TimeclockKioskDevices.FirstOrDefaultAsync(
            d => d.CompanyId == companyId && d.Token == cookie.Value.token, true);
        if (device == null) return Forbid();

        var company = await _unitOfWork.Companies.GetByIdAsync(companyId, ignoreQueryFilters: true);
        if (company == null || !company.TimeclockEnabled) return Forbid();

        var user = await _userManager.FindByIdAsync(request.UserId);
        if (user == null || user.CompanyId != companyId || !user.IsActive || user.KioskPin == null)
            return BadRequest(new { message = "Employee not found or kiosk disabled." });

        // Verify PIN
        var hashResult = _passwordHasher.VerifyHashedPassword(user, user.KioskPin, request.Pin);
        if (hashResult == PasswordVerificationResult.Failed)
            return BadRequest(new { message = "Incorrect PIN. Please try again." });

        var now = DateTime.UtcNow;

        // Auto clock-out stale open entries (shared logic with main-app ClockIn)
        if (company.TimeclockAutoClockOutHours is > 0)
        {
            var cutoff = now.AddHours(-company.TimeclockAutoClockOutHours.Value);
            var stale  = await _unitOfWork.EmployeeClockEntries.FindAsync(
                e => e.UserId == user.Id && e.ClockOutTime == null
                  && e.CompanyId == companyId && e.ClockInTime <= cutoff);
            foreach (var s in stale)
            {
                s.ClockOutTime = s.ClockInTime.AddHours(company.TimeclockAutoClockOutHours.Value);
                s.HoursWorked  = company.TimeclockAutoClockOutHours.Value;
                s.Notes        = (s.Notes + " [Auto clocked out]").Trim();
            }
            if (stale.Any()) await _unitOfWork.CompleteAsync();
        }

        // Check for open entry
        var openEntry = await _unitOfWork.EmployeeClockEntries.FirstOrDefaultAsync(
            e => e.UserId == user.Id && e.ClockOutTime == null && e.CompanyId == companyId);

        string action;
        decimal segmentHours = 0;

        if (openEntry != null)
        {
            // Clock out
            openEntry.ClockOutTime = now;
            openEntry.HoursWorked  = Math.Round((decimal)(now - openEntry.ClockInTime).TotalHours, 2);
            await _unitOfWork.CompleteAsync();
            action       = "clockOut";
            segmentHours = openEntry.HoursWorked.Value;
        }
        else
        {
            // Block second punch today if not allowed
            if (!company.TimeclockAllowMultiplePunchesPerDay)
            {
                var todayPunched = await _unitOfWork.EmployeeClockEntries.AnyAsync(
                    e => e.UserId == user.Id && e.CompanyId == companyId && e.ClockInTime >= now.Date);
                if (todayPunched)
                    return BadRequest(new { message = "Multiple clock-ins per day are not allowed." });
            }

            // Clock in
            var entry = new EmployeeClockEntry
            {
                UserId      = user.Id,
                CompanyId   = companyId,
                ClockInTime = now
            };
            await _unitOfWork.EmployeeClockEntries.AddAsync(entry);
            await _unitOfWork.CompleteAsync();
            action = "clockIn";
        }

        // Compute today's running total (all completed segments + any still-open segment)
        var todayStart = now.Date;
        var todayEntries = await _unitOfWork.EmployeeClockEntries.FindAsync(
            e => e.UserId == user.Id && e.CompanyId == companyId && e.ClockInTime >= todayStart);

        decimal dailyTotal = todayEntries.Sum(e =>
            e.HoursWorked ?? (decimal)(now - e.ClockInTime).TotalHours);
        dailyTotal = Math.Round(dailyTotal, 2);

        return Json(new
        {
            action,
            displayName  = user.FullName,
            timestamp    = now,
            segmentHours,
            dailyTotal,
            segmentCount = todayEntries.Count()
        });
    }

    // =========================================================================
    // PIN management (called from Users UI via AJAX)
    // =========================================================================

    /// <summary>Sets or clears a 4-digit kiosk PIN for the specified employee. Manager-only.</summary>
    [HttpPost]
    public async Task<IActionResult> SetKioskPin(string userId, string? pin)
    {
        if (!await IsManagerAsync()) return Forbid();

        var companyId = GetCurrentCompanyId();
        var user = await _userManager.FindByIdAsync(userId);
        if (user == null || user.CompanyId != companyId)
            return NotFound(new { message = "User not found." });

        if (string.IsNullOrWhiteSpace(pin))
        {
            user.KioskPin = null;
        }
        else
        {
            if (pin.Length != 4 || !pin.All(char.IsDigit))
                return BadRequest(new { message = "PIN must be exactly 4 digits." });
            user.KioskPin = _passwordHasher.HashPassword(user, pin);
        }

        await _userManager.UpdateAsync(user);
        return Json(new { success = true });
    }

    // =========================================================================
    // Helpers
    // =========================================================================

    /// <summary>
    /// Validates the kiosk device cookie against the <see cref="TimeclockKioskDevice"/> table.
    /// Returns (null, null) if the cookie is absent or the token no longer exists.
    /// Queries with ignoreQueryFilters because the kiosk runs without a logged-in user context.
    /// </summary>
    private async Task<(TimeclockKioskDevice? device, Company? company)> ValidateKioskCookieAsync()
    {
        var cookie = ReadTimeclockKioskCookie();
        if (cookie == null) return (null, null);

        var device = await _unitOfWork.TimeclockKioskDevices.FirstOrDefaultAsync(
            d => d.CompanyId == cookie.Value.companyId && d.Token == cookie.Value.token,
            true);
        if (device == null) return (null, null);

        var company = await _unitOfWork.Companies.GetByIdAsync(cookie.Value.companyId, ignoreQueryFilters: true);
        return (device, company);
    }

    /// <summary>Updates <see cref="TimeclockKioskDevice.LastSeenAt"/> without blocking the response.</summary>
    private async Task TouchDeviceAsync(TimeclockKioskDevice device)
    {
        device.LastSeenAt = DateTime.UtcNow;
        await _unitOfWork.TimeclockKioskDevices.UpdateAsync(device);
        await _unitOfWork.CompleteAsync();
    }

    private int GetCurrentCompanyId()
    {
        var claim = User.FindFirst("CompanyId")?.Value;
        return int.TryParse(claim, out int id) ? id : 0;
    }

    private async Task<bool> IsManagerAsync()
    {
        var user = await _userManager.GetUserAsync(User);
        return user?.CompanyRole is "CompanyAdmin" or "Manager";
    }

    private (int companyId, string token)? ReadTimeclockKioskCookie()
    {
        if (!Request.Cookies.TryGetValue(KioskCookieName, 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]);
    }

    private void WriteTimeclockKioskCookie(int companyId, string token)
    {
        Response.Cookies.Append(KioskCookieName, $"{companyId}:{token}", new CookieOptions
        {
            HttpOnly = true,
            Secure   = true,
            SameSite = SameSiteMode.Lax,
            MaxAge   = TimeSpan.FromDays(365)
        });
    }

    private void DeleteTimeclockKioskCookie()
    {
        Response.Cookies.Delete(KioskCookieName);
    }

    private static EmployeeClockEntryDto MapEntry(EmployeeClockEntry e, ApplicationUser? user) =>
        new()
        {
            Id              = e.Id,
            UserId          = e.UserId,
            UserDisplayName = user?.FullName ?? "Unknown",
            ClockInTime     = e.ClockInTime,
            ClockOutTime    = e.ClockOutTime,
            HoursWorked     = e.HoursWorked,
            EntryType       = e.EntryType,
            Notes           = e.Notes
        };

    private static string BuildInitials(ApplicationUser u)
    {
        var first = string.IsNullOrEmpty(u.FirstName) ? "" : u.FirstName[0].ToString();
        var last  = string.IsNullOrEmpty(u.LastName)  ? "" : u.LastName[0].ToString();
        return (first + last).ToUpperInvariant();
    }
}
