using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
using UnityEngine;

namespace MCPForUnity.Editor.Tools
{
    [McpForUnityTool("manage_packages", AutoRegister = false, Group = "core", RequiresPolling = true, PollAction = "status")]
    public static class ManagePackages
    {
        // Pending async requests keyed by job ID
        private static readonly Dictionary<string, Request> PendingRequests = new();

        // Pending list/search requests keyed by job ID
        private static readonly Dictionary<string, ListRequest> PendingListRequests = new();
        private static readonly Dictionary<string, SearchRequest> PendingSearchRequests = new();

        public static object HandleCommand(JObject @params)
        {
            if (@params == null)
                return new ErrorResponse("Parameters cannot be null.");

            var p = new ToolParams(@params);

            var actionResult = p.GetRequired("action");
            if (!actionResult.IsSuccess)
                return new ErrorResponse(actionResult.ErrorMessage);

            string action = actionResult.Value.ToLowerInvariant();

            try
            {
                switch (action)
                {
                    case "add_package":
                        return AddPackage(p);
                    case "remove_package":
                        return RemovePackage(p);
                    case "status":
                        return GetStatus(p);
                    case "list_packages":
                        return ListPackages(p);
                    case "search_packages":
                        return SearchPackages(p);
                    case "get_package_info":
                        return GetPackageInfo(p);
                    case "list_registries":
                        return ListRegistries();
                    case "add_registry":
                        return AddRegistry(p);
                    case "remove_registry":
                        return RemoveRegistry(p);
                    case "embed_package":
                        return EmbedPackage(p);
                    case "resolve_packages":
                        return ResolvePackages();
                    case "ping":
                        return Ping();
                    default:
                        return new ErrorResponse(
                            $"Unknown action: '{action}'. Supported actions: add_package, remove_package, status, list_packages, search_packages, get_package_info, list_registries, add_registry, remove_registry, embed_package, resolve_packages, ping.");
                }
            }
            catch (Exception ex)
            {
                return new ErrorResponse(ex.Message, new { stackTrace = ex.StackTrace });
            }
        }

        // === add_package ===
        private static object AddPackage(ToolParams p)
        {
            var packageResult = p.GetRequired("package", "'package' parameter is required for add_package.");
            if (!packageResult.IsSuccess)
                return new ErrorResponse(packageResult.ErrorMessage);

            var (isValid, warning, package) = ValidatePackageIdentifier(packageResult.Value);
            if (!isValid)
                return new ErrorResponse(warning);

            try
            {
                var request = Client.Add(package);
                string jobId = PackageJobManager.StartJob("add", package);
                PendingRequests[jobId] = request;

                RegisterCompletionCallback(jobId, request);

                string message = $"Package installation started for '{package}'. Use status action to check progress.";
                if (warning != null)
                    message = $"WARNING: {warning}\n{message}";

                return new PendingResponse(
                    message,
                    pollIntervalSeconds: 3.0,
                    data: new { job_id = jobId, operation = "add", package_ = package, warning }
                );
            }
            catch (Exception e)
            {
                return new ErrorResponse($"Failed to start package installation: {e.Message}");
            }
        }

        // === remove_package ===
        private static object RemovePackage(ToolParams p)
        {
            var packageResult = p.GetRequired("package", "'package' parameter is required for remove_package.");
            if (!packageResult.IsSuccess)
                return new ErrorResponse(packageResult.ErrorMessage);

            string package = packageResult.Value;
            bool force = p.GetBool("force");

            // Check for dependents before removing
            if (!force)
            {
                var dependents = GetDependentPackages(package);
                if (dependents == null)
                {
                    return new ErrorResponse(
                        $"Cannot remove '{package}': failed to look up dependent packages. " +
                        "Set force=true to remove anyway.");
                }
                if (dependents.Length > 0)
                {
                    string depList = string.Join(", ", dependents);
                    return new ErrorResponse(
                        $"Cannot remove '{package}': {dependents.Length} installed package(s) depend on it: {depList}. " +
                        "Set force=true to remove anyway.");
                }
            }

            try
            {
                var request = Client.Remove(package);
                string jobId = PackageJobManager.StartJob("remove", package);
                PendingRequests[jobId] = request;

                RegisterCompletionCallback(jobId, request);

                return new PendingResponse(
                    $"Package removal started for '{package}'. Use status action to check progress.",
                    pollIntervalSeconds: 3.0,
                    data: new { job_id = jobId, operation = "remove", package_ = package }
                );
            }
            catch (Exception e)
            {
                return new ErrorResponse($"Failed to start package removal: {e.Message}");
            }
        }

        // === status ===
        private static object GetStatus(ToolParams p)
        {
            string jobId = p.Get("job_id");

            // Check pending list/search requests first
            if (!string.IsNullOrEmpty(jobId))
            {
                if (PendingListRequests.TryGetValue(jobId, out var listReq))
                    return CheckListRequest(jobId, listReq);

                if (PendingSearchRequests.TryGetValue(jobId, out var searchReq))
                    return CheckSearchRequest(jobId, searchReq);
            }

            PackageJob job;
            if (string.IsNullOrEmpty(jobId))
            {
                job = PackageJobManager.GetLatestJob();
                if (job == null)
                    return new SuccessResponse("No package jobs found.");
            }
            else
            {
                job = PackageJobManager.GetJob(jobId);
                if (job == null)
                    return new ErrorResponse($"No job found with ID '{jobId}'.");
            }

            // If job is still running, check in-memory request or attempt recovery
            if (job.Status == PackageJobStatus.Running)
            {
                if (PendingRequests.TryGetValue(job.JobId, out var req))
                {
                    if (req.IsCompleted)
                    {
                        FinalizeRequest(job.JobId, req);
                        job = PackageJobManager.GetJob(job.JobId);
                    }
                }
                else
                {
                    // No in-memory request (lost after domain reload) — re-run recovery
                    long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                    PackageJobManager.TryRecoverJob(job, nowMs);
                    if (job.Status != PackageJobStatus.Running)
                        PackageJobManager.PersistToSessionState();
                }
            }

            var serialized = PackageJobManager.ToSerializable(job);
            string message = job.Status switch
            {
                PackageJobStatus.Running => $"Job {job.JobId} is still running ({job.Operation} '{job.Package}').",
                PackageJobStatus.Succeeded => $"Job {job.JobId} succeeded ({job.Operation} '{job.Package}').",
                PackageJobStatus.Failed => $"Job {job.JobId} failed ({job.Operation} '{job.Package}'): {job.Error}",
                _ => $"Job {job.JobId}: {job.Status}"
            };

            if (job.Status == PackageJobStatus.Running)
            {
                return new PendingResponse(
                    message,
                    pollIntervalSeconds: 3.0,
                    data: serialized
                );
            }

            return new SuccessResponse(message, serialized);
        }

        // === list_packages ===
        private static object ListPackages(ToolParams p)
        {
            try
            {
                var request = Client.List();
                string jobId = Guid.NewGuid().ToString("N");
                PendingListRequests[jobId] = request;

                // Try immediate completion for fast responses
                if (request.IsCompleted)
                    return CheckListRequest(jobId, request);

                return new PendingResponse(
                    "Listing installed packages...",
                    pollIntervalSeconds: 1.0,
                    data: new { job_id = jobId, operation = "list_packages" }
                );
            }
            catch (Exception e)
            {
                return new ErrorResponse($"Failed to list packages: {e.Message}");
            }
        }

        private static object CheckListRequest(string jobId, ListRequest request)
        {
            if (!request.IsCompleted)
            {
                return new PendingResponse(
                    "Listing installed packages...",
                    pollIntervalSeconds: 1.0,
                    data: new { job_id = jobId, operation = "list_packages" }
                );
            }

            PendingListRequests.Remove(jobId);

            if (request.Status == StatusCode.Failure)
                return new ErrorResponse($"Failed to list packages: {request.Error?.message ?? "Unknown error"}");

            var packages = request.Result
                .Select(pkg => new
                {
                    name = pkg.name,
                    version = pkg.version,
                    display_name = pkg.displayName,
                    source = pkg.source.ToString()
                })
                .ToArray();

            return new SuccessResponse(
                $"Found {packages.Length} installed package(s).",
                new { packages, count = packages.Length }
            );
        }

        // === search_packages ===
        private static object SearchPackages(ToolParams p)
        {
            var queryResult = p.GetRequired("query", "'query' parameter is required for search_packages.");
            if (!queryResult.IsSuccess)
                return new ErrorResponse(queryResult.ErrorMessage);

            try
            {
                var request = Client.Search(queryResult.Value);
                string jobId = Guid.NewGuid().ToString("N");
                PendingSearchRequests[jobId] = request;

                if (request.IsCompleted)
                    return CheckSearchRequest(jobId, request);

                return new PendingResponse(
                    $"Searching packages for '{queryResult.Value}'...",
                    pollIntervalSeconds: 1.0,
                    data: new { job_id = jobId, operation = "search_packages", query = queryResult.Value }
                );
            }
            catch (Exception e)
            {
                return new ErrorResponse($"Failed to search packages: {e.Message}");
            }
        }

        private static object CheckSearchRequest(string jobId, SearchRequest request)
        {
            if (!request.IsCompleted)
            {
                return new PendingResponse(
                    "Searching packages...",
                    pollIntervalSeconds: 1.0,
                    data: new { job_id = jobId, operation = "search_packages" }
                );
            }

            PendingSearchRequests.Remove(jobId);

            if (request.Status == StatusCode.Failure)
                return new ErrorResponse($"Package search failed: {request.Error?.message ?? "Unknown error"}");

            var packages = request.Result
                .Select(pkg => new
                {
                    name = pkg.name,
                    version = pkg.version,
                    display_name = pkg.displayName,
                    description = TruncateDescription(pkg.description)
                })
                .ToArray();

            return new SuccessResponse(
                $"Found {packages.Length} matching package(s).",
                new { packages, count = packages.Length }
            );
        }

        // === get_package_info ===
        private static object GetPackageInfo(ToolParams p)
        {
            var packageResult = p.GetRequired("package", "'package' parameter is required for get_package_info.");
            if (!packageResult.IsSuccess)
                return new ErrorResponse(packageResult.ErrorMessage);

            string package = packageResult.Value;

            try
            {
                var allPackages = PackageInfo.GetAllRegisteredPackages();
                var info = allPackages.FirstOrDefault(pkg =>
                    string.Equals(pkg.name, package, StringComparison.OrdinalIgnoreCase));

                if (info == null)
                    return new ErrorResponse($"Package '{package}' is not installed.");

                var dependencies = info.dependencies
                    .Select(d => new { name = d.name, version = d.version })
                    .ToArray();

                return new SuccessResponse(
                    $"Package '{info.displayName}' ({info.name}@{info.version}).",
                    new
                    {
                        name = info.name,
                        version = info.version,
                        display_name = info.displayName,
                        description = info.description,
                        source = info.source.ToString(),
                        resolved_path = info.resolvedPath,
                        author = info.author?.name,
                        dependencies,
                        dependency_count = dependencies.Length
                    }
                );
            }
            catch (Exception e)
            {
                return new ErrorResponse($"Failed to get package info: {e.Message}");
            }
        }

        // === list_registries ===
        private static object ListRegistries()
        {
            try
            {
                string manifestPath = GetManifestPath();
                if (!File.Exists(manifestPath))
                    return new ErrorResponse("Packages/manifest.json not found.");

                var manifest = JObject.Parse(File.ReadAllText(manifestPath));
                var registries = manifest["scopedRegistries"] as JArray ?? new JArray();

                var result = registries.Select(r => new
                {
                    name = r["name"]?.ToString(),
                    url = r["url"]?.ToString(),
                    scopes = (r["scopes"] as JArray)?.Select(s => s.ToString()).ToArray() ?? Array.Empty<string>()
                }).ToArray();

                return new SuccessResponse(
                    $"Found {result.Length} scoped {(result.Length == 1 ? "registry" : "registries")}.",
                    new { registries = result, count = result.Length }
                );
            }
            catch (Exception e)
            {
                return new ErrorResponse($"Failed to read registries: {e.Message}");
            }
        }

        // === add_registry ===
        private static object AddRegistry(ToolParams p)
        {
            var nameResult = p.GetRequired("name", "'name' parameter is required for add_registry.");
            if (!nameResult.IsSuccess)
                return new ErrorResponse(nameResult.ErrorMessage);

            var urlResult = p.GetRequired("url", "'url' parameter is required for add_registry.");
            if (!urlResult.IsSuccess)
                return new ErrorResponse(urlResult.ErrorMessage);

            string[] scopes = p.GetStringArray("scopes");
            if (scopes == null || scopes.Length == 0)
                return new ErrorResponse("'scopes' parameter is required (array of scope strings).");

            try
            {
                string manifestPath = GetManifestPath();
                if (!File.Exists(manifestPath))
                    return new ErrorResponse("Packages/manifest.json not found.");

                var manifest = JObject.Parse(File.ReadAllText(manifestPath));
                var registries = manifest["scopedRegistries"] as JArray;
                if (registries == null)
                {
                    registries = new JArray();
                    manifest["scopedRegistries"] = registries;
                }

                // Check for duplicate
                foreach (var reg in registries)
                {
                    if (string.Equals(reg["name"]?.ToString(), nameResult.Value, StringComparison.OrdinalIgnoreCase)
                        || string.Equals(reg["url"]?.ToString(), urlResult.Value, StringComparison.OrdinalIgnoreCase))
                    {
                        return new ErrorResponse($"A registry with name '{nameResult.Value}' or URL '{urlResult.Value}' already exists.");
                    }
                }

                var newRegistry = new JObject
                {
                    ["name"] = nameResult.Value,
                    ["url"] = urlResult.Value,
                    ["scopes"] = new JArray(scopes)
                };
                registries.Add(newRegistry);

                File.WriteAllText(manifestPath, manifest.ToString(Formatting.Indented));
                Client.Resolve();

                return new SuccessResponse(
                    $"Added scoped registry '{nameResult.Value}'.",
                    new
                    {
                        name = nameResult.Value,
                        url = urlResult.Value,
                        scopes
                    }
                );
            }
            catch (Exception e)
            {
                return new ErrorResponse($"Failed to add registry: {e.Message}");
            }
        }

        // === remove_registry ===
        private static object RemoveRegistry(ToolParams p)
        {
            string name = p.Get("name");
            string url = p.Get("url");

            if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(url))
                return new ErrorResponse("Either 'name' or 'url' parameter is required for remove_registry.");

            try
            {
                string manifestPath = GetManifestPath();
                if (!File.Exists(manifestPath))
                    return new ErrorResponse("Packages/manifest.json not found.");

                var manifest = JObject.Parse(File.ReadAllText(manifestPath));
                var registries = manifest["scopedRegistries"] as JArray;
                if (registries == null || registries.Count == 0)
                    return new ErrorResponse("No scoped registries configured.");

                JToken toRemove = null;
                foreach (var reg in registries)
                {
                    bool nameMatch = !string.IsNullOrEmpty(name)
                        && string.Equals(reg["name"]?.ToString(), name, StringComparison.OrdinalIgnoreCase);
                    bool urlMatch = !string.IsNullOrEmpty(url)
                        && string.Equals(reg["url"]?.ToString(), url, StringComparison.OrdinalIgnoreCase);

                    if (nameMatch || urlMatch)
                    {
                        toRemove = reg;
                        break;
                    }
                }

                if (toRemove == null)
                {
                    string identifier = !string.IsNullOrEmpty(name) ? $"name '{name}'" : $"URL '{url}'";
                    return new ErrorResponse($"No registry found matching {identifier}.");
                }

                string removedName = toRemove["name"]?.ToString();
                registries.Remove(toRemove);

                if (registries.Count == 0)
                    manifest.Remove("scopedRegistries");

                File.WriteAllText(manifestPath, manifest.ToString(Formatting.Indented));
                Client.Resolve();

                return new SuccessResponse($"Removed scoped registry '{removedName}'.");
            }
            catch (Exception e)
            {
                return new ErrorResponse($"Failed to remove registry: {e.Message}");
            }
        }

        // === embed_package ===
        private static object EmbedPackage(ToolParams p)
        {
            var packageResult = p.GetRequired("package", "'package' parameter is required for embed_package.");
            if (!packageResult.IsSuccess)
                return new ErrorResponse(packageResult.ErrorMessage);

            try
            {
                var request = Client.Embed(packageResult.Value);
                string jobId = PackageJobManager.StartJob("embed", packageResult.Value);
                PendingRequests[jobId] = request;

                RegisterCompletionCallback(jobId, request);

                return new PendingResponse(
                    $"Embedding package '{packageResult.Value}'. Use status action to check progress.",
                    pollIntervalSeconds: 3.0,
                    data: new { job_id = jobId, operation = "embed", package_ = packageResult.Value }
                );
            }
            catch (Exception e)
            {
                return new ErrorResponse($"Failed to embed package: {e.Message}");
            }
        }

        // === resolve_packages ===
        private static object ResolvePackages()
        {
            try
            {
                Client.Resolve();
                return new SuccessResponse("Package resolution triggered. Unity will re-resolve all packages.");
            }
            catch (Exception e)
            {
                return new ErrorResponse($"Failed to trigger package resolution: {e.Message}");
            }
        }

        // === ping ===
        private static object Ping()
        {
            try
            {
                var allPackages = PackageInfo.GetAllRegisteredPackages();
                return new SuccessResponse(
                    "Package manager is available.",
                    new
                    {
                        unity_version = Application.unityVersion,
                        installed_package_count = allPackages.Length,
                        is_compiling = EditorApplication.isCompiling,
                        is_updating = EditorApplication.isUpdating
                    }
                );
            }
            catch (Exception e)
            {
                return new ErrorResponse($"Package manager check failed: {e.Message}");
            }
        }

        // --- Helpers ---

        private static void RegisterCompletionCallback(string jobId, Request request)
        {
            void CheckCompletion()
            {
                if (!PendingRequests.ContainsKey(jobId))
                {
                    EditorApplication.update -= CheckCompletion;
                    return;
                }

                if (!request.IsCompleted)
                    return;

                EditorApplication.update -= CheckCompletion;
                FinalizeRequest(jobId, request);
            }

            EditorApplication.update += CheckCompletion;
        }

        private static void FinalizeRequest(string jobId, Request request)
        {
            PendingRequests.Remove(jobId);

            if (request.Status == StatusCode.Failure)
            {
                PackageJobManager.CompleteJob(jobId, false,
                    error: request.Error?.message ?? "Unknown package manager error");
                return;
            }

            string version = null;
            string name = null;

            if (request is AddRequest addReq && addReq.Result != null)
            {
                version = addReq.Result.version;
                name = addReq.Result.name;
            }
            else if (request is EmbedRequest embedReq && embedReq.Result != null)
            {
                version = embedReq.Result.version;
                name = embedReq.Result.name;
            }

            PackageJobManager.CompleteJob(jobId, true, version: version, name: name);
        }

        private static string GetManifestPath()
        {
            return Path.Combine(Application.dataPath, "..", "Packages", "manifest.json");
        }

        private static string TruncateDescription(string description, int maxLength = 200)
        {
            if (string.IsNullOrEmpty(description) || description.Length <= maxLength)
                return description;

            return description.Substring(0, maxLength) + "...";
        }

        private static (bool isValid, string warning, string normalized) ValidatePackageIdentifier(string package)
        {
            if (string.IsNullOrWhiteSpace(package))
                return (false, "Package identifier cannot be empty.", null);

            // Git URLs — allow but warn
            if (package.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
                package.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
                package.StartsWith("git://", StringComparison.OrdinalIgnoreCase) ||
                package.StartsWith("ssh://", StringComparison.OrdinalIgnoreCase) ||
                package.EndsWith(".git", StringComparison.OrdinalIgnoreCase))
            {
                return (true,
                    $"Installing from git URL. Ensure this is a trusted source — git packages execute code on import.",
                    package);
            }

            // File paths — allow but warn
            if (package.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
            {
                return (true,
                    $"Installing from local path. Ensure this path contains trusted package code.",
                    package);
            }

            // Normal package ID: lowercase the name portion (Unity requires lowercase)
            string normalized = package.Contains('@')
                ? package.Substring(0, package.IndexOf('@')).ToLowerInvariant() + package.Substring(package.IndexOf('@'))
                : package.ToLowerInvariant();

            string name = normalized.Contains('@') ? normalized.Substring(0, normalized.IndexOf('@')) : normalized;
            if (!Regex.IsMatch(name, @"^[a-z][a-z0-9._-]*(\.[a-z0-9._-]+)+$"))
            {
                return (false,
                    $"'{package}' is not a valid package identifier. Expected format: com.company.package or com.company.package@version.",
                    null);
            }

            return (true, null, normalized);
        }

        private static string[] GetDependentPackages(string packageName)
        {
            try
            {
                string name = PackageJobManager.ExtractPackageName(packageName);

                var allPackages = PackageInfo.GetAllRegisteredPackages();
                return allPackages
                    .Where(pkg => pkg.dependencies.Any(d =>
                        string.Equals(d.name, name, StringComparison.OrdinalIgnoreCase)))
                    .Select(pkg => pkg.name)
                    .ToArray();
            }
            catch
            {
                return null;
            }
        }
    }
}
