using CopilotCoordinator.Models;

namespace CopilotCoordinator.Services;

/// <summary>
/// Camera lock manager. Ported from legacy CameraLocksService.cs (~363 lines).
/// In-memory state only — locks are lost on restart (same as legacy AppServer).
/// Locks expire after configurable timeout (default 5 minutes).
/// </summary>
public class LockManager
{
    private readonly SemaphoreSlim _locksLock = new(1, 1);
    private readonly Dictionary<int, CameraLock> _locks = new();

    private readonly SemaphoreSlim _requestsLock = new(1, 1);
    private readonly Dictionary<int, List<(string KeyboardId, CameraLockPriority Priority, DateTime CreatedAt)>> _requests = new();

    private readonly TimeSpan _expirationTimeout;
    private readonly TimeSpan _warningBefore;
    private readonly WsBroadcaster _broadcaster;
    private readonly ILogger<LockManager> _logger;

    // Track which locks have already been warned about (to avoid repeat notifications)
    private readonly Dictionary<int, DateTime> _warnedLocks = new();

    public LockManager(IConfiguration config, WsBroadcaster broadcaster, ILogger<LockManager> logger)
    {
        _expirationTimeout = config.GetValue("LockExpiration:Timeout", TimeSpan.FromMinutes(5));
        _warningBefore = config.GetValue("LockExpiration:WarningBefore", TimeSpan.FromMinutes(1));
        _broadcaster = broadcaster;
        _logger = logger;
    }

    public async Task<IReadOnlyList<CameraLock>> GetAllLocks()
    {
        await _locksLock.WaitAsync();
        try { return _locks.Values.ToList(); }
        finally { _locksLock.Release(); }
    }

    public async Task<IReadOnlyList<int>> GetLockedCameraIds(string keyboardId)
    {
        await _locksLock.WaitAsync();
        try
        {
            return _locks.Values
                .Where(l => l.OwnerName.Equals(keyboardId, StringComparison.OrdinalIgnoreCase))
                .Select(l => l.CameraId)
                .ToList();
        }
        finally { _locksLock.Release(); }
    }

    public async Task<CameraLockResult> TryLock(int cameraId, string keyboardId, CameraLockPriority priority)
    {
        _logger.LogInformation("TryLock camera {CameraId} by {Keyboard} priority {Priority}", cameraId, keyboardId, priority);

        await _locksLock.WaitAsync();
        try
        {
            if (!_locks.TryGetValue(cameraId, out var existing))
            {
                // Camera is free — acquire lock
                var now = DateTime.UtcNow;
                var newLock = new CameraLock(cameraId, priority, keyboardId, now, now + _expirationTimeout);
                _locks[cameraId] = newLock;

                _logger.LogInformation("Camera {CameraId} locked by {Keyboard}", cameraId, keyboardId);
                await _broadcaster.Broadcast("lock_acquired", newLock);

                // If there's a pending takeover request, notify the new owner
                await NotifyPendingTakeover(cameraId);

                return new CameraLockResult(true, newLock);
            }

            // Camera already locked — check if priority can take over
            if (CanTakeOver(priority, existing.Priority))
            {
                var previousOwner = existing.OwnerName;
                var now = DateTime.UtcNow;
                var newLock = new CameraLock(cameraId, priority, keyboardId, now, now + _expirationTimeout);
                _locks[cameraId] = newLock;

                _logger.LogInformation("Camera {CameraId} taken over from {Previous} by {Keyboard}", cameraId, previousOwner, keyboardId);

                // Notify previous owner
                await _broadcaster.SendTo(previousOwner, "lock_notification", new CameraLockNotification(
                    CameraLockNotificationType.TakenOver, cameraId, keyboardId));

                await _broadcaster.Broadcast("lock_acquired", newLock);
                return new CameraLockResult(true, newLock);
            }

            // Cannot take over — return current lock info
            _logger.LogInformation("Camera {CameraId} lock denied for {Keyboard} — held by {Owner}", cameraId, keyboardId, existing.OwnerName);
            return new CameraLockResult(false, existing);
        }
        finally { _locksLock.Release(); }
    }

    public async Task Unlock(int cameraId, string keyboardId)
    {
        _logger.LogInformation("Unlock camera {CameraId} by {Keyboard}", cameraId, keyboardId);

        await _locksLock.WaitAsync();
        try
        {
            if (!_locks.TryGetValue(cameraId, out var existing))
            {
                _logger.LogWarning("Cannot unlock camera {CameraId} — not locked", cameraId);
                return;
            }

            _locks.Remove(cameraId);
            _warnedLocks.Remove(cameraId);

            _logger.LogInformation("Camera {CameraId} unlocked", cameraId);
        }
        finally { _locksLock.Release(); }

        await _broadcaster.Broadcast("lock_released", new { cameraId });

        // Notify the owner
        await _broadcaster.SendTo(keyboardId, "lock_notification", new CameraLockNotification(
            CameraLockNotificationType.Unlocked, cameraId, keyboardId));
    }

    public async Task RequestTakeover(int cameraId, string keyboardId, CameraLockPriority priority)
    {
        _logger.LogInformation("Takeover requested for camera {CameraId} by {Keyboard}", cameraId, keyboardId);

        string? currentOwner;
        await _locksLock.WaitAsync();
        try
        {
            if (!_locks.TryGetValue(cameraId, out var existing))
                return;
            currentOwner = existing.OwnerName;
        }
        finally { _locksLock.Release(); }

        // Queue the request
        await _requestsLock.WaitAsync();
        bool isFirst;
        try
        {
            if (!_requests.TryGetValue(cameraId, out var list))
            {
                list = new();
                _requests[cameraId] = list;
            }
            list.Add((keyboardId, priority, DateTime.UtcNow));
            list.Sort((a, b) => a.CreatedAt.CompareTo(b.CreatedAt));
            isFirst = list.Count == 1;
        }
        finally { _requestsLock.Release(); }

        // Only notify current owner if this is the first request (avoid spamming)
        if (isFirst)
        {
            await _broadcaster.SendTo(currentOwner, "lock_notification", new CameraLockNotification(
                CameraLockNotificationType.ConfirmTakeOver, cameraId, keyboardId));
        }
    }

    public async Task ConfirmTakeover(int cameraId, string requestingKeyboardId, bool confirm)
    {
        _logger.LogInformation("Takeover confirm for camera {CameraId} by {Keyboard}: {Confirm}", cameraId, requestingKeyboardId, confirm);

        // Remove the request
        await _requestsLock.WaitAsync();
        try
        {
            if (_requests.TryGetValue(cameraId, out var list))
            {
                list.RemoveAll(r => r.KeyboardId == requestingKeyboardId);
                if (list.Count == 0) _requests.Remove(cameraId);
            }
        }
        finally { _requestsLock.Release(); }

        if (confirm)
        {
            // Unlock the current owner
            await _locksLock.WaitAsync();
            try
            {
                if (_locks.TryGetValue(cameraId, out var existing))
                {
                    _locks.Remove(cameraId);
                    _warnedLocks.Remove(cameraId);
                }
            }
            finally { _locksLock.Release(); }

            await _broadcaster.Broadcast("lock_released", new { cameraId });
        }

        // Notify the requester of the result
        await _broadcaster.SendTo(requestingKeyboardId, "lock_notification", new CameraLockNotification(
            confirm ? CameraLockNotificationType.Confirmed : CameraLockNotificationType.Rejected,
            cameraId, requestingKeyboardId));
    }

    public async Task ResetExpiration(int cameraId, string keyboardId)
    {
        await _locksLock.WaitAsync();
        try
        {
            if (_locks.TryGetValue(cameraId, out var existing) &&
                existing.OwnerName.Equals(keyboardId, StringComparison.OrdinalIgnoreCase))
            {
                var newExpiry = DateTime.UtcNow + _expirationTimeout;
                _locks[cameraId] = existing with { ExpiresAt = newExpiry };
                _warnedLocks.Remove(cameraId);
            }
        }
        finally { _locksLock.Release(); }
    }

    /// <summary>
    /// Called every second by the expiration timer. Checks for expired and soon-to-expire locks.
    /// Ported from legacy CameraLockExpirationWorker.cs.
    /// </summary>
    public async Task CheckExpirations()
    {
        var now = DateTime.UtcNow;
        var toUnlock = new List<(int CameraId, string OwnerName)>();
        var toWarn = new List<(int CameraId, string OwnerName)>();

        await _locksLock.WaitAsync();
        try
        {
            foreach (var (id, lck) in _locks)
            {
                if (lck.ExpiresAt <= now)
                {
                    toUnlock.Add((id, lck.OwnerName));
                }
                else if (lck.ExpiresAt <= now + _warningBefore)
                {
                    if (!_warnedLocks.TryGetValue(id, out var prevExpiry) || prevExpiry != lck.ExpiresAt)
                    {
                        toWarn.Add((id, lck.OwnerName));
                        _warnedLocks[id] = lck.ExpiresAt;
                    }
                }
            }
        }
        finally { _locksLock.Release(); }

        // Process expirations outside the lock
        foreach (var (cameraId, owner) in toUnlock)
        {
            _logger.LogInformation("Lock expired for camera {CameraId}", cameraId);
            await Unlock(cameraId, owner);
        }

        foreach (var (cameraId, owner) in toWarn)
        {
            await _broadcaster.SendTo(owner, "lock_notification", new CameraLockNotification(
                CameraLockNotificationType.ExpireSoon, cameraId, owner));
            _logger.LogInformation("Lock expiring soon for camera {CameraId}, notified {Owner}", cameraId, owner);
        }
    }

    private async Task NotifyPendingTakeover(int cameraId)
    {
        await _requestsLock.WaitAsync();
        try
        {
            if (_requests.TryGetValue(cameraId, out var list) && list.Count > 0)
            {
                var first = list[0];
                // Will be handled by existing owner through ConfirmTakeOver flow
            }
        }
        finally { _requestsLock.Release(); }
    }

    private static bool CanTakeOver(CameraLockPriority requester, CameraLockPriority holder)
    {
        // High priority can take over Low priority (same as legacy CameraLockPriorityExtensions.CanTakeOver)
        return requester == CameraLockPriority.High && holder == CameraLockPriority.Low;
    }
}
