using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AIRuntime.Runtime.Providers.Fallback;
using UnityEditor;
using UnityEngine;

namespace AIRuntime.Editor
{
    public sealed partial class AIRuntimeKanbanSession
    {
        private const int MaximumAutomaticProviderRetries = 3;
        private const double ReviewPromotionSeconds = 300;
        private static string _initialServiceBoardPath;
        private FileStream _schedulerOwnerLock;
        private string _schedulerOwnerLockPath;
        private bool _schedulerPersistenceFailed;
        private double _nextLeaseHeartbeat;
        [SerializeField] internal bool IsServiceSession;
        // Per-session seam for deterministic editor integration tests; normal
        // sessions always use AIRuntimeKanbanAgentRunner.
        internal Func<string, CancellationToken, Task<string>> RunnerOverrideForTests;

        [Serializable]
        private sealed class DispatchAttempt
        {
            public string Id;
            public string AgentId;
            public string OwnerId;
            public string StartedAtUtc;
            public string LeaseUntilUtc;
            public string LastCheckpointAtUtc;
            public string FinishedAtUtc;
            public string Outcome;
        }

        internal string BoardPath => _boardFilePath;
        internal double NextPollSeconds
        {
            get
            {
                if (_activeAgentRuns.Count > 0) return .25;
                // Events wake the service immediately. A bounded reconciliation
                // poll catches external edits, compilation and missed events.
                double delay = 5;
                var now = DateTimeOffset.UtcNow;
                foreach (var task in _board.Tasks.Where(task => task != null))
                    if (TryUtc(task.NextAttemptAtUtc, out var due))
                        delay = Math.Min(delay, Math.Max(.1, (due - now).TotalSeconds));
                foreach (var cooldown in _board.ProviderCooldowns)
                    if (cooldown != null && !cooldown.Released && TryUtc(cooldown.UntilUtc, out var due))
                        delay = Math.Min(delay, Math.Max(.1, (due - now).TotalSeconds));
                return delay;
            }
        }

        internal static AIRuntimeKanbanSession CreateServiceSession(string path)
        {
            _initialServiceBoardPath = path;
            try
            {
                var session = CreateInstance<AIRuntimeKanbanSession>();
                // A bounded command never persists continuous execution intent.
                session._autoRunAgents = session._autoRunAgents || session._board.SchedulerEnabled;
                return session;
            }
            finally { _initialServiceBoardPath = null; }
        }

        internal void StopAfterSchedulerFailure(Exception exception)
        {
            _autoRunAgents = false;
            _schedulerPersistenceFailed = true;
            AbandonOwnedRuns();
            _statusMessage = "Scheduler stopped: " + exception.Message;
            Debug.LogException(exception);
            Repaint();
        }

        private void CancelOwnedRuns()
        {
            foreach (var run in _activeAgentRuns.Values)
            {
                try { run.Cancellation?.Cancel(); }
                catch (ObjectDisposedException) { }
            }
        }

        private void AbandonOwnedRuns()
        {
            CancelOwnedRuns();
            foreach (var run in _activeAgentRuns.Values) run.Cancellation?.Dispose();
            _activeAgentRuns.Clear();
        }

        private void RetireRejectedAttempt(PendingAgentCompletion completion)
        {
            if (string.IsNullOrEmpty(completion.AttemptId) ||
                !_activeAgentRuns.TryGetValue(completion.AgentId, out var run) ||
                run.TaskId != completion.TaskId || run.AttemptId != completion.AttemptId) return;
            _activeAgentRuns.Remove(completion.AgentId);
            run.Cancellation?.Dispose();
            _discardedAgentRunTaskIds.Remove(completion.TaskId);
        }

        private bool TryAcquireSchedulerOwnership()
        {
            string path = NormalizeBoardPath() + ".scheduler.lock";
            if (_schedulerOwnerLock != null && _schedulerOwnerLockPath == path) return true;
            if (_activeAgentRuns.Count > 0 && _schedulerOwnerLock != null) return false;
            _schedulerOwnerLock?.Dispose();
            _schedulerOwnerLock = null;
            try
            {
                Directory.CreateDirectory(Path.GetDirectoryName(path));
                _schedulerOwnerLock = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
                _schedulerOwnerLockPath = path;
                return true;
            }
            catch (IOException) { _statusMessage = "Another scheduler owns this board"; return false; }
            catch (UnauthorizedAccessException) { _statusMessage = "Cannot acquire board execution ownership"; return false; }
        }

        private bool PersistSchedulerState()
        {
            MarkDirty();
            int before = _board.Revision;
            SaveBoard(showDialog: false);
            bool saved = !_isDirty && !_boardWriteConflict && _board.Revision > before;
            _schedulerPersistenceFailed = !saved;
            if (!saved) AbandonOwnedRuns();
            return saved;
        }

        private bool TryClaimDispatch(KanbanTask task)
        {
            if (_boardWriteConflict || _schedulerPersistenceFailed || !TryAcquireSchedulerOwnership()) return false;
            if (HasLiveDispatchLease(task, DateTimeOffset.UtcNow)) return false;
            SynchronizeMemoryAttempts();
            task.Dispatch = new DispatchAttempt
            {
                Id = Guid.NewGuid().ToString("N"), OwnerId = _boardWriterId,
                StartedAtUtc = DateTimeOffset.UtcNow.ToString("O"),
                LeaseUntilUtc = DateTimeOffset.UtcNow.AddMinutes(2).ToString("O"), Outcome = "Running"
            };
            BeginMemoryAttempt(task);
            task.NextAttemptAtUtc = string.Empty;
            // Called before any provider or tool work. Failure to save is failure to dispatch.
            return PersistSchedulerState();
        }

        private static bool HasLiveDispatchLease(KanbanTask task, DateTimeOffset now)
        {
            return task?.Dispatch?.Outcome == "Running" &&
                   TryUtc(task.Dispatch.LeaseUntilUtc, out var until) && until > now;
        }

        private bool AcceptAttempt(string taskId, string attemptId)
        {
            var task = FindTask(taskId);
            if (task == null || _boardWriteConflict || _schedulerPersistenceFailed) return false;
            // Legacy/test records predate attempt IDs. A real dispatched attempt
            // always carries its ID, including through every queued callback.
            if (string.IsNullOrEmpty(attemptId)) return task.Dispatch == null;
            return task.Dispatch?.Id == attemptId && task.Dispatch.OwnerId == _boardWriterId &&
                   task.Dispatch.Outcome == "Running" &&
                   _activeAgentRuns.Values.Any(run => run.TaskId == taskId && run.AttemptId == attemptId);
        }

        private void RequireAttempt(string taskId, string attemptId)
        {
            _nextExternalBoardCheckEditorTime = 0;
            TryObserveExternalBoardChange();
            if (!AcceptAttempt(taskId, attemptId))
                throw new OperationCanceledException("This dispatch attempt no longer owns the card.");
        }

        private void RefreshDispatchLeases()
        {
            if (_boardWriteConflict || _schedulerPersistenceFailed ||
                EditorApplication.timeSinceStartup < _nextLeaseHeartbeat) return;
            _nextLeaseHeartbeat = EditorApplication.timeSinceStartup + 30;
            bool changed = false;
            foreach (var run in _activeAgentRuns.Values)
            {
                var task = FindTask(run.TaskId);
                if (task?.Dispatch == null || !AcceptAttempt(run.TaskId, run.AttemptId)) continue;
                task.Dispatch.LeaseUntilUtc = DateTimeOffset.UtcNow.AddMinutes(2).ToString("O");
                changed = true;
            }
            if (changed) PersistSchedulerState();
        }

        private static bool TryUtc(string value, out DateTimeOffset result) =>
            DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture,
                DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out result);

        private static bool CanAutomaticallyRetry(string error, ProviderFailureKind? kind)
        {
            string value = error ?? string.Empty;
            if (kind == ProviderFailureKind.Authentication || kind == ProviderFailureKind.InvalidRequest ||
                kind == ProviderFailureKind.Cancelled || kind == ProviderFailureKind.ModelUnavailable ||
                value.IndexOf("[Authentication]", StringComparison.OrdinalIgnoreCase) >= 0 ||
                value.IndexOf("[InvalidRequest]", StringComparison.OrdinalIgnoreCase) >= 0 ||
                value.IndexOf("HTTP 401", StringComparison.OrdinalIgnoreCase) >= 0 ||
                value.IndexOf("HTTP 403", StringComparison.OrdinalIgnoreCase) >= 0) return false;
            return IsTransientProviderAvailabilityFailure(error, kind);
        }

        private bool ScheduleProviderRetry(KanbanTask task, string error, ProviderFailureKind? kind, bool review)
        {
            task.NextAttemptAtUtc = string.Empty;
            if (!CanAutomaticallyRetry(error, kind)) return false;
            task.RetryReview = review;
            task.ReviewStatus = "Retry Available";
            task.Status = review ? TaskStatus.Review : TaskStatus.Todo;
            task.RetryReason = error;
            task.BlockedReason = "Automatic provider retry limit reached. Resume explicitly after checking provider availability.";
            if (task.AutomaticRetryCount >= MaximumAutomaticProviderRetries) return true;
            task.AutomaticRetryCount++;
            var delay = TimeSpan.FromSeconds(30 * Math.Pow(2, task.AutomaticRetryCount - 1));
            task.NextAttemptAtUtc = DateTimeOffset.UtcNow.Add(delay).ToString("O");
            task.BlockedReason = $"Provider retry {task.AutomaticRetryCount}/{MaximumAutomaticProviderRetries} scheduled for {task.NextAttemptAtUtc}.";
            task.NextAgentMessage = "[ProviderRetryAvailable] Resume this durable attempt. Inspect existing artifacts and execution memory; complete only unfinished work.";
            return true;
        }

        private void ReleaseDueProviderRetries()
        {
            bool changed = false;
            foreach (var task in _board.Tasks)
            {
                if (task == null || task.ReviewStatus != "Retry Available" ||
                    (task.Status != TaskStatus.Todo && task.Status != TaskStatus.Review) ||
                    !TryUtc(task.NextAttemptAtUtc, out var due) || due > DateTimeOffset.UtcNow) continue;
                task.NextAttemptAtUtc = string.Empty;
                task.Status = task.RetryReview ? TaskStatus.Review : TaskStatus.Todo;
                task.ReviewStatus = task.RetryReview ? "Pending" : string.Empty;
                task.AgentError = string.Empty;
                task.BlockedReason = string.Empty;
                TouchTask(task);
                changed = true;
            }
            // A due timestamp grants eligibility only; normal gates/dependencies
            // still decide whether a provider run may start.
            if (changed) PersistSchedulerState();
        }

        private void TrackReviewWaitTimes()
        {
            bool changed = false;
            foreach (var task in _board.Tasks.Where(task => task != null))
            {
                if (task.Status == TaskStatus.Review && string.IsNullOrEmpty(task.ReviewQueuedAtUtc))
                {
                    task.ReviewQueuedAtUtc = DateTimeOffset.UtcNow.ToString("O");
                    changed = true;
                }
                else if (task.Status != TaskStatus.Review && !string.IsNullOrEmpty(task.ReviewQueuedAtUtc))
                {
                    task.ReviewQueuedAtUtc = string.Empty;
                    changed = true;
                }
            }
            if (changed) PersistSchedulerState();
        }

        private static bool IsAgedReview(KanbanTask task, DateTimeOffset now) =>
            task?.Status == TaskStatus.Review && TryUtc(task.ReviewQueuedAtUtc, out var queued) &&
            (now - queued).TotalSeconds >= ReviewPromotionSeconds;

        /// <summary>Explicit reset cleanup; never used as an automatic load migration.</summary>
        public static void RepairResetRootInputsForCommandLine()
        {
            if (!int.TryParse(Environment.GetEnvironmentVariable("AIRUNTIME_KANBAN_EXPECTED_REVISION"), out int expected))
                throw new InvalidOperationException("AIRUNTIME_KANBAN_EXPECTED_REVISION is required.");
            var session = CreateInstance<AIRuntimeKanbanSession>();
            try
            {
                if (session._board.Revision != expected)
                    throw new InvalidOperationException("The board revision changed; inspect it before retrying reset cleanup.");
                int removed = session.RemoveMissingResetRootInputs();
                if (removed > 0 && !session.PersistSchedulerState())
                    throw new IOException("Reset cleanup could not be saved; newer disk state was preserved.");
                Debug.Log($"[Kanban] Reset cleanup removed {removed} missing input(s); revision={session._board.Revision}; scheduler remains paused.");
            }
            finally { DestroyImmediate(session); }
        }

        private int RemoveMissingResetRootInputs()
        {
            if (_board.SchedulerEnabled || _autoRunAgents || _activeAgentRuns.Count != 0 || _board.Tasks.Count != 1)
                throw new InvalidOperationException("Reset cleanup requires a paused board with exactly one fresh Planning root.");
            var root = _board.Tasks[0];
            if (root == null || root.Status != TaskStatus.Todo || GetWorkflowPhase(root) != WorkflowPhasePlanning ||
                !string.IsNullOrWhiteSpace(root.ParentTaskId) || !string.IsNullOrWhiteSpace(root.ExecutionMemory) ||
                HasLiveDispatchLease(root, DateTimeOffset.UtcNow))
                throw new InvalidOperationException("The root is not in a fresh reset state.");
            string projectRoot = Environment.GetEnvironmentVariable("AIRUNTIME_KANBAN_WORKSPACE_ROOT");
            if (string.IsNullOrWhiteSpace(projectRoot)) projectRoot = Directory.GetParent(Application.dataPath).FullName;
            projectRoot = Path.GetFullPath(projectRoot);
            int removed = root.ConsumedArtifactPaths.RemoveAll(path =>
            {
                if (string.IsNullOrWhiteSpace(path) || !path.Replace('\\', '/').StartsWith("Assets/", StringComparison.Ordinal))
                    return false;
                string absolute = Path.GetFullPath(Path.Combine(projectRoot, path));
                string assetsRoot = Path.Combine(projectRoot, "Assets") + Path.DirectorySeparatorChar;
                return absolute.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase) &&
                       !File.Exists(absolute) && !Directory.Exists(absolute);
            });
            if (removed > 0) TouchTask(root);
            return removed;
        }
    }
}
