using System;
using System.Globalization;
using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;

namespace AIRuntime.Editor
{
    /// <summary>Batch-only, one-dispatch adapter. Invoke without -quit.</summary>
    [InitializeOnLoad]
    public static class AIRuntimeKanbanCli
    {
        private const string ReloadExitKey = "AIRuntime.Kanban.Cli.ReloadExit";
        private static AIRuntimeKanbanSession _session;
        private static string _reportPath;
        private static DateTimeOffset _started;
        private static double _deadline;
        private static bool _running;
        private static FileStream _reportReservation;

        static AIRuntimeKanbanCli()
        {
            // The report was checkpointed before reload. Never dispatch again.
            if (Application.isBatchMode && SessionState.GetInt(ReloadExitKey, -1) >= 0)
                EditorApplication.delayCall += () =>
                {
                    int code = SessionState.GetInt(ReloadExitKey, 5);
                    SessionState.EraseInt(ReloadExitKey);
                    EditorApplication.Exit(code);
                };
        }

        public static void Run()
        {
            if (!Application.isBatchMode)
                throw new InvalidOperationException("CLI runner requires -batchmode in a separate, unopened project checkout.");
            if (_running) throw new InvalidOperationException("CLI runner is already active.");
            _started = DateTimeOffset.UtcNow;
            _reportPath = null;
            _ownsReport = false;
            try
            {
                string[] args = Environment.GetCommandLineArgs();
                _reportPath = Path.GetFullPath(Required(args, "-airuntimeResult"));
                string board = Path.GetFullPath(Required(args, "-airuntimeBoard"));
                string task = Required(args, "-airuntimeTaskId");
                if (args.Any(a => string.Equals(a, "-quit", StringComparison.OrdinalIgnoreCase)))
                    throw new ArgumentException("Omit -quit: the runner exits only after saving its result.");
                if (!File.Exists(board)) throw new FileNotFoundException("Board does not exist.", board);
                if (File.Exists(_reportPath) || Directory.Exists(_reportPath))
                    throw new ArgumentException("Result path must be a new file; existing files are never overwritten.");
                if (string.Equals(board, _reportPath, StringComparison.OrdinalIgnoreCase) ||
                    _reportPath.StartsWith(board + ".", StringComparison.OrdinalIgnoreCase))
                    throw new ArgumentException("Result must not replace the board or its lock/backup files.");
                string seconds = Optional(args, "-airuntimeTimeoutSeconds") ?? "600";
                if (!double.TryParse(seconds, NumberStyles.Float, CultureInfo.InvariantCulture, out double timeout) ||
                    double.IsNaN(timeout) || double.IsInfinity(timeout) || timeout < 1 || timeout > 86400)
                    throw new ArgumentException("Timeout must be between 1 and 86400 seconds.");
                var saved = JObject.Parse(File.ReadAllText(board));
                if ((bool?)saved["SchedulerEnabled"] == true)
                    throw new InvalidOperationException("Pause the board before running a CLI step.");
                if (!(saved["Tasks"] is JArray tasks) || tasks.Count(t => (string)t["Id"] == task) != 1)
                    throw new ArgumentException("Task ID must identify exactly one card in this board.");
                Directory.CreateDirectory(Path.GetDirectoryName(_reportPath));
                // Probe output access before allowing provider/tool work.
                _reportReservation = new FileStream(_reportPath + ".cli.lock", FileMode.CreateNew, FileAccess.Write, FileShare.None);
                _ownsReport = true;
                _deadline = EditorApplication.timeSinceStartup + timeout;
                _session = AIRuntimeKanbanSession.CreateServiceSession(board);
                _session.PrepareCommandLineStep(task);
                _running = true;
                EditorApplication.update += Tick;
                AssemblyReloadEvents.beforeAssemblyReload += BeforeReload;
                Debug.Log("[Kanban CLI] Waiting for one persisted dispatch result: " + task);
            }
            catch (Exception exception)
            {
                // Do not overwrite an existing file on invalid invocation.
                Finish(new JObject { ["Outcome"] = "InvalidRequest", ["Reason"] = exception.Message }, 2);
            }
        }

        internal static string Required(string[] args, string name) =>
            Optional(args, name) ?? throw new ArgumentException("Missing required argument: " + name);

        private static string Optional(string[] args, string name)
        {
            int[] matches = args.Select((value, index) => new { value, index })
                .Where(p => string.Equals(p.value, name, StringComparison.OrdinalIgnoreCase))
                .Select(p => p.index).ToArray();
            if (matches.Length == 0) return null;
            int at = matches[0];
            if (matches.Length != 1 || at + 1 >= args.Length || string.IsNullOrWhiteSpace(args[at + 1]) || args[at + 1].StartsWith("-"))
                throw new ArgumentException("Expected one value for " + name);
            return args[at + 1];
        }

        private static void Tick()
        {
            try
            {
                if (EditorApplication.timeSinceStartup >= _deadline)
                {
                    Finish(_session.StopCommandLineStep("TimedOut", "CLI deadline reached; active work was checkpointed and cancelled."));
                    return;
                }
                var result = _session.PollCommandLineStep();
                if (result != null) Finish(result);
            }
            catch (Exception exception)
            {
                Finish(_session.StopCommandLineStep("Failed", exception.ToString()));
            }
        }

        private static void BeforeReload()
        {
            if (!_running) return;
            var result = _session.StopCommandLineStep("Interrupted", "Unity assembly reload interrupted the CLI step. Resume explicitly after inspecting the checkpoint.");
            int code = WriteResult(result, ExitCode((string)result["Outcome"]));
            SessionState.SetInt(ReloadExitKey, code);
            Detach();
            UnityEngine.Object.DestroyImmediate(_session);
            _session = null;
        }

        private static int ExitCode(string outcome) => outcome == "Completed" ? 0 :
            outcome == "Blocked" ? 3 : outcome == "TimedOut" ? 4 : outcome == "Interrupted" ? 5 : 1;

        private static void Finish(JObject result, int? code = null)
        {
            int exit = WriteResult(result, code ?? ExitCode((string)result["Outcome"]));
            Detach();
            if (_session != null) UnityEngine.Object.DestroyImmediate(_session);
            _session = null;
            EditorApplication.Exit(exit);
        }

        private static bool _ownsReport;

        private static int WriteResult(JObject result, int code)
        {
            result["SchemaVersion"] = 1;
            result["ExitCode"] = code;
            result["StartedAtUtc"] = _started.ToString("O");
            result["FinishedAtUtc"] = DateTimeOffset.UtcNow.ToString("O");
            result["ProjectPath"] = Directory.GetParent(Application.dataPath).FullName;
            try
            {
                if (!_ownsReport) throw new IOException("No result file was reserved; see Editor log for invocation error.");
                string temp = _reportPath + "." + Guid.NewGuid().ToString("N") + ".tmp";
                try
                {
                    File.WriteAllText(temp, result.ToString());
                    // Publish a new file atomically; never replace somebody else's
                    // report. A held sidecar reserves this invocation's output.
                    File.Move(temp, _reportPath);
                }
                finally { if (File.Exists(temp)) File.Delete(temp); }
            }
            catch (Exception exception)
            {
                Debug.LogError("[Kanban CLI] Result write failed: " + exception.Message);
                code = code == 2 && !_ownsReport ? 2 : 1;
                result["ExitCode"] = code;
                if (code != 2) result["Outcome"] = "Failed";
                result["ReportError"] = exception.Message;
            }
            Debug.Log("[Kanban CLI] " + result.ToString(Newtonsoft.Json.Formatting.None));
            return code;
        }

        private static void Detach()
        {
            _running = false;
            EditorApplication.update -= Tick;
            AssemblyReloadEvents.beforeAssemblyReload -= BeforeReload;
            if (_reportReservation != null)
            {
                _reportReservation.Dispose();
                _reportReservation = null;
                try { File.Delete(_reportPath + ".cli.lock"); }
                catch (IOException exception) { Debug.LogWarning("[Kanban CLI] Could not remove output reservation: " + exception.Message); }
                catch (UnauthorizedAccessException exception) { Debug.LogWarning("[Kanban CLI] Could not remove output reservation: " + exception.Message); }
            }
        }
    }
}
