using System;
using System.Collections.Generic;
using System.Linq;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json;
using UnityEditor;
using UnityEditor.PackageManager;
using PackageInfo = UnityEditor.PackageManager.PackageInfo;

namespace MCPForUnity.Editor.Services
{
    internal enum PackageJobStatus { Running, Succeeded, Failed }

    internal sealed class PackageJob
    {
        public string JobId { get; set; }
        public PackageJobStatus Status { get; set; }
        public string Operation { get; set; }
        public string Package { get; set; }
        public long StartedUnixMs { get; set; }
        public long? FinishedUnixMs { get; set; }
        public long LastUpdateUnixMs { get; set; }
        public string Error { get; set; }
        public string ResultVersion { get; set; }
        public string ResultName { get; set; }
    }

    internal static class PackageJobManager
    {
        private const string SessionKeyJobs = "MCPForUnity.PackageJobsV1";
        private const int MaxJobsToKeep = 10;
        private const long DomainReloadTimeoutMs = 120_000;

        private static readonly object LockObj = new();
        private static readonly Dictionary<string, PackageJob> Jobs = new();

        static PackageJobManager()
        {
            TryRestoreFromSessionState();
        }

        private sealed class PersistedState
        {
            public List<PersistedJob> jobs { get; set; }
        }

        private sealed class PersistedJob
        {
            public string job_id { get; set; }
            public string status { get; set; }
            public string operation { get; set; }
            public string package_ { get; set; }
            public long started_unix_ms { get; set; }
            public long? finished_unix_ms { get; set; }
            public long last_update_unix_ms { get; set; }
            public string error { get; set; }
            public string result_version { get; set; }
            public string result_name { get; set; }
        }

        private static PackageJobStatus ParseStatus(string status)
        {
            if (string.IsNullOrWhiteSpace(status))
                return PackageJobStatus.Running;

            return status.Trim().ToLowerInvariant() switch
            {
                "succeeded" => PackageJobStatus.Succeeded,
                "failed" => PackageJobStatus.Failed,
                _ => PackageJobStatus.Running
            };
        }

        private static void TryRestoreFromSessionState()
        {
            try
            {
                string json = SessionState.GetString(SessionKeyJobs, string.Empty);
                if (string.IsNullOrWhiteSpace(json))
                    return;

                var state = JsonConvert.DeserializeObject<PersistedState>(json);
                if (state?.jobs == null)
                    return;

                lock (LockObj)
                {
                    Jobs.Clear();
                    long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

                    foreach (var pj in state.jobs)
                    {
                        if (pj == null || string.IsNullOrWhiteSpace(pj.job_id))
                            continue;

                        var job = new PackageJob
                        {
                            JobId = pj.job_id,
                            Status = ParseStatus(pj.status),
                            Operation = pj.operation,
                            Package = pj.package_,
                            StartedUnixMs = pj.started_unix_ms,
                            FinishedUnixMs = pj.finished_unix_ms,
                            LastUpdateUnixMs = pj.last_update_unix_ms,
                            Error = pj.error,
                            ResultVersion = pj.result_version,
                            ResultName = pj.result_name
                        };

                        // Domain reload recovery for running jobs
                        if (job.Status == PackageJobStatus.Running)
                        {
                            TryRecoverJob(job, now);
                        }

                        Jobs[pj.job_id] = job;
                    }
                }
            }
            catch (Exception ex)
            {
                McpLog.Warn($"[PackageJobManager] Failed to restore SessionState: {ex.Message}");
            }
        }

        internal static void TryRecoverJob(PackageJob job, long nowMs)
        {
            try
            {
                string packageName = ExtractPackageName(job.Package);
                var allPackages = PackageInfo.GetAllRegisteredPackages();
                var info = FindPackageInfo(allPackages, packageName, job.Package);

                if (job.Operation == "add" || job.Operation == "embed")
                {
                    if (info != null)
                    {
                        job.Status = PackageJobStatus.Succeeded;
                        job.FinishedUnixMs = nowMs;
                        job.LastUpdateUnixMs = nowMs;
                        job.ResultVersion = info.version;
                        job.ResultName = info.name;
                        McpLog.Info($"[PackageJobManager] Recovered {job.Operation} job {job.JobId}: {info.name}@{info.version} installed.");
                    }
                    else if (nowMs - job.StartedUnixMs > DomainReloadTimeoutMs)
                    {
                        job.Status = PackageJobStatus.Failed;
                        job.FinishedUnixMs = nowMs;
                        job.LastUpdateUnixMs = nowMs;
                        job.Error = $"Package {job.Operation} timed out after domain reload.";
                        McpLog.Warn($"[PackageJobManager] Timed out {job.Operation} job {job.JobId} for '{job.Package}'.");
                    }
                }
                else if (job.Operation == "remove")
                {
                    if (info == null)
                    {
                        job.Status = PackageJobStatus.Succeeded;
                        job.FinishedUnixMs = nowMs;
                        job.LastUpdateUnixMs = nowMs;
                        McpLog.Info($"[PackageJobManager] Recovered remove job {job.JobId}: '{packageName}' is no longer installed.");
                    }
                    else if (nowMs - job.StartedUnixMs > DomainReloadTimeoutMs)
                    {
                        job.Status = PackageJobStatus.Failed;
                        job.FinishedUnixMs = nowMs;
                        job.LastUpdateUnixMs = nowMs;
                        job.Error = "Package removal timed out after domain reload.";
                        McpLog.Warn($"[PackageJobManager] Timed out remove job {job.JobId} for '{job.Package}'.");
                    }
                }
            }
            catch (Exception ex)
            {
                McpLog.Warn($"[PackageJobManager] Recovery check failed for job {job.JobId}: {ex.Message}");
            }
        }

        /// <summary>
        /// Find a PackageInfo by name, falling back to packageId or git/local source for non-standard identifiers.
        /// </summary>
        private static PackageInfo FindPackageInfo(PackageInfo[] allPackages, string packageName, string originalIdentifier)
        {
            // Direct name match (handles normal com.company.package identifiers)
            var info = allPackages.FirstOrDefault(p =>
                string.Equals(p.name, packageName, StringComparison.OrdinalIgnoreCase));
            if (info != null)
                return info;

            // For git URLs / file: paths, packageName == originalIdentifier and won't match .name.
            // Try matching by packageId or source (git/local).
            bool isGitOrFile = originalIdentifier.StartsWith("http", StringComparison.OrdinalIgnoreCase)
                               || originalIdentifier.StartsWith("git", StringComparison.OrdinalIgnoreCase)
                               || originalIdentifier.StartsWith("file:", StringComparison.OrdinalIgnoreCase)
                               || originalIdentifier.EndsWith(".git", StringComparison.OrdinalIgnoreCase);

            if (!isGitOrFile)
                return null;

            return allPackages.FirstOrDefault(p =>
                p.source == PackageSource.Git || p.source == PackageSource.Local
                    ? p.packageId != null && p.packageId.Contains(originalIdentifier)
                      || p.resolvedPath != null && p.resolvedPath.Contains(originalIdentifier)
                    : false);
        }

        internal static string ExtractPackageName(string packageIdentifier)
        {
            if (string.IsNullOrEmpty(packageIdentifier))
                return packageIdentifier;

            // Strip version: "com.unity.foo@1.0.0" -> "com.unity.foo"
            int atIndex = packageIdentifier.IndexOf('@');
            if (atIndex > 0)
                return packageIdentifier.Substring(0, atIndex);

            // Git URLs and file: paths — can't reliably extract name, return as-is
            return packageIdentifier;
        }

        internal static void PersistToSessionState()
        {
            try
            {
                PersistedState snapshot;
                lock (LockObj)
                {
                    var jobs = Jobs.Values
                        .OrderByDescending(j => j.LastUpdateUnixMs)
                        .Take(MaxJobsToKeep)
                        .Select(j => new PersistedJob
                        {
                            job_id = j.JobId,
                            status = j.Status.ToString().ToLowerInvariant(),
                            operation = j.Operation,
                            package_ = j.Package,
                            started_unix_ms = j.StartedUnixMs,
                            finished_unix_ms = j.FinishedUnixMs,
                            last_update_unix_ms = j.LastUpdateUnixMs,
                            error = j.Error,
                            result_version = j.ResultVersion,
                            result_name = j.ResultName
                        })
                        .ToList();

                    snapshot = new PersistedState { jobs = jobs };
                }

                SessionState.SetString(SessionKeyJobs, JsonConvert.SerializeObject(snapshot));
            }
            catch (Exception ex)
            {
                McpLog.Warn($"[PackageJobManager] Failed to persist SessionState: {ex.Message}");
            }
        }

        public static string StartJob(string operation, string package)
        {
            string jobId = Guid.NewGuid().ToString("N");
            long started = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

            var job = new PackageJob
            {
                JobId = jobId,
                Status = PackageJobStatus.Running,
                Operation = operation,
                Package = package,
                StartedUnixMs = started,
                FinishedUnixMs = null,
                LastUpdateUnixMs = started,
                Error = null,
                ResultVersion = null,
                ResultName = null
            };

            lock (LockObj)
            {
                Jobs[jobId] = job;
            }
            PersistToSessionState();
            return jobId;
        }

        public static void CompleteJob(string jobId, bool success, string error = null,
            string version = null, string name = null)
        {
            long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
            lock (LockObj)
            {
                if (!Jobs.TryGetValue(jobId, out var job))
                    return;

                job.Status = success ? PackageJobStatus.Succeeded : PackageJobStatus.Failed;
                job.FinishedUnixMs = now;
                job.LastUpdateUnixMs = now;
                job.Error = error;
                job.ResultVersion = version;
                job.ResultName = name;
            }
            PersistToSessionState();
        }

        public static PackageJob GetJob(string jobId)
        {
            if (string.IsNullOrWhiteSpace(jobId))
                return null;

            lock (LockObj)
            {
                Jobs.TryGetValue(jobId, out var job);
                return job;
            }
        }

        public static PackageJob GetLatestJob()
        {
            lock (LockObj)
            {
                return Jobs.Values
                    .OrderByDescending(j => j.StartedUnixMs)
                    .FirstOrDefault();
            }
        }

        public static object ToSerializable(PackageJob job)
        {
            if (job == null)
                return null;

            return new
            {
                job_id = job.JobId,
                status = job.Status.ToString().ToLowerInvariant(),
                operation = job.Operation,
                package_ = job.Package,
                started_unix_ms = job.StartedUnixMs,
                finished_unix_ms = job.FinishedUnixMs,
                last_update_unix_ms = job.LastUpdateUnixMs,
                error = job.Error,
                result_version = job.ResultVersion,
                result_name = job.ResultName
            };
        }
    }
}
