using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;

namespace Streamingle.Editor
{
    internal sealed class AICameraCliRunner : IDisposable
    {
        internal const string ExecutableName = "CWCameraWorker.exe";
        internal const string EventProtocolVersion = "1";

        private readonly ConcurrentQueue<ProcessMessage> messages =
            new ConcurrentQueue<ProcessMessage>();

        private Process process;
        private string cancellationFile = string.Empty;
        private bool cancellationRequested;
        private bool exitObserved;
        private int exitCode;

        internal bool IsRunning
        {
            get
            {
                if (process == null || exitObserved)
                {
                    return false;
                }

                try
                {
                    return !process.HasExited;
                }
                catch (InvalidOperationException)
                {
                    return false;
                }
            }
        }

        internal bool CancellationRequested => cancellationRequested;

        internal void Start(ProcessRequest request)
        {
            if (process != null)
            {
                throw new InvalidOperationException(
                    "A camera worker process is already attached to this runner.");
            }

            ValidateRequest(request);
            cancellationFile = Path.GetFullPath(request.CancellationFile);
            var cancellationDirectory = Path.GetDirectoryName(cancellationFile);
            if (string.IsNullOrWhiteSpace(cancellationDirectory))
            {
                throw new InvalidOperationException(
                    "The camera worker cancellation directory is invalid.");
            }

            Directory.CreateDirectory(cancellationDirectory);
            if (File.Exists(cancellationFile))
            {
                File.Delete(cancellationFile);
            }

            var startInfo = new ProcessStartInfo
            {
                FileName = Path.GetFullPath(request.ExecutablePath),
                Arguments = JoinArguments(request.Arguments),
                WorkingDirectory = Path.GetFullPath(request.WorkingDirectory),
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                CreateNoWindow = true
            };
            startInfo.EnvironmentVariables["PYTHONUTF8"] = "1";
            startInfo.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8";

            process = new Process
            {
                StartInfo = startInfo,
                EnableRaisingEvents = false
            };
            process.OutputDataReceived += OnOutputDataReceived;
            process.ErrorDataReceived += OnErrorDataReceived;

            try
            {
                if (!process.Start())
                {
                    throw new InvalidOperationException(
                        "CWCameraWorker could not be started.");
                }

                process.BeginOutputReadLine();
                process.BeginErrorReadLine();
            }
            catch
            {
                DisposeProcess();
                throw;
            }
        }

        internal bool TryDequeueMessage(out ProcessMessage message)
        {
            return messages.TryDequeue(out message);
        }

        internal bool TryGetExitCode(out int completedExitCode)
        {
            completedExitCode = 0;
            if (process == null)
            {
                return false;
            }

            if (!exitObserved)
            {
                try
                {
                    if (!process.HasExited)
                    {
                        return false;
                    }

                    // The parameterless overload also waits for redirected
                    // asynchronous output handlers to receive their final line.
                    process.WaitForExit();
                    exitCode = process.ExitCode;
                    exitObserved = true;
                }
                catch (InvalidOperationException)
                {
                    return false;
                }
            }

            completedExitCode = exitCode;
            return true;
        }

        internal bool RequestCancellation()
        {
            if (process == null || exitObserved || cancellationRequested)
            {
                return false;
            }

            var directory = Path.GetDirectoryName(cancellationFile);
            if (string.IsNullOrWhiteSpace(directory))
            {
                throw new InvalidOperationException(
                    "The camera worker cancellation path is unavailable.");
            }

            Directory.CreateDirectory(directory);
            File.WriteAllText(
                cancellationFile,
                DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture),
                new UTF8Encoding(false));
            cancellationRequested = true;
            return true;
        }

        internal void CompleteAndDispose()
        {
            if (process != null && !exitObserved)
            {
                throw new InvalidOperationException(
                    "A running camera worker cannot be disposed as completed.");
            }

            DisposeProcess();
            TryDeleteCancellationFile();
        }

        public void Dispose()
        {
            DisposeProcess();
            if (exitObserved)
            {
                TryDeleteCancellationFile();
            }
        }

        internal static string ResolveExecutable(
            string configuredPath,
            string cwAiRoot,
            string projectRoot,
            string packageRoot = "")
        {
            var candidates = new List<string>();
            AddCandidate(candidates, configuredPath);
            AddCandidate(
                candidates,
                Environment.GetEnvironmentVariable(
                    "CWAI_WORKER_EXECUTABLE"));

            AddRootCandidates(candidates, cwAiRoot);
            if (!string.IsNullOrWhiteSpace(projectRoot))
            {
                AddCandidate(
                    candidates,
                    Path.Combine(
                        projectRoot,
                        "Tools",
                        "CWCameraWorker",
                        ExecutableName));
            }

            var physicalPackageRoot = packageRoot;
            if (string.IsNullOrWhiteSpace(physicalPackageRoot))
            {
                try
                {
                    physicalPackageRoot = FileUtil.GetPhysicalPath(
                        "Packages/com.mingle.cw-ai");
                }
                catch (Exception)
                {
                    physicalPackageRoot = string.Empty;
                }
            }

            if (!string.IsNullOrWhiteSpace(physicalPackageRoot))
            {
                AddCandidate(
                    candidates,
                    Path.Combine(
                        physicalPackageRoot,
                        "Tools~",
                        "CWCameraWorker",
                        ExecutableName));
                AddCandidate(
                    candidates,
                    Path.Combine(
                        physicalPackageRoot,
                        "Tools~",
                        ExecutableName));
            }

            return candidates
                .Where(value => !string.IsNullOrWhiteSpace(value))
                .Select(Path.GetFullPath)
                .FirstOrDefault(File.Exists) ?? string.Empty;
        }

        internal static string JoinArguments(IEnumerable<string> arguments)
        {
            if (arguments == null)
            {
                throw new ArgumentNullException(nameof(arguments));
            }

            return string.Join(" ", arguments.Select(QuoteArgument));
        }

        internal static string QuoteArgument(string argument)
        {
            argument ??= string.Empty;
            if (argument.Length > 0 &&
                argument.All(character =>
                    !char.IsWhiteSpace(character) && character != '"'))
            {
                return argument;
            }

            var result = new StringBuilder(argument.Length + 2);
            result.Append('"');
            var backslashCount = 0;
            foreach (var character in argument)
            {
                if (character == '\\')
                {
                    backslashCount++;
                    continue;
                }

                if (character == '"')
                {
                    result.Append('\\', backslashCount * 2 + 1);
                    result.Append('"');
                    backslashCount = 0;
                    continue;
                }

                result.Append('\\', backslashCount);
                backslashCount = 0;
                result.Append(character);
            }

            result.Append('\\', backslashCount * 2);
            result.Append('"');
            return result.ToString();
        }

        internal static bool TryParseEvent(
            string line,
            out CliEvent parsedEvent)
        {
            parsedEvent = default;
            var normalized = (line ?? string.Empty).Trim();
            const string prefix = "CWAI_EVENT ";
            if (normalized.StartsWith(prefix, StringComparison.Ordinal))
            {
                normalized = normalized.Substring(prefix.Length).TrimStart();
            }

            if (normalized.Length < 2 ||
                normalized[0] != '{' ||
                normalized[normalized.Length - 1] != '}')
            {
                return false;
            }

            CliEventPayload payload;
            try
            {
                payload = JsonUtility.FromJson<CliEventPayload>(normalized);
            }
            catch (ArgumentException)
            {
                return false;
            }

            var eventType = FirstNotEmpty(
                payload?.type,
                payload?.eventType,
                payload?.@event);
            if (string.IsNullOrWhiteSpace(eventType))
            {
                return false;
            }

            var hasProgress = ContainsJsonProperty(normalized, "progress");
            var hasEta = ContainsJsonProperty(normalized, "etaSeconds");
            parsedEvent = new CliEvent(
                eventType.Trim(),
                payload.protocolVersion ?? string.Empty,
                payload.jobId ?? string.Empty,
                payload.phase ?? string.Empty,
                payload.message ?? string.Empty,
                hasProgress
                    ? Mathf.Clamp01(payload.progress)
                    : -1f,
                hasEta && IsFinite(payload.etaSeconds)
                    ? Math.Max(0d, payload.etaSeconds)
                    : -1d,
                payload.outputPath ?? string.Empty,
                payload.error ?? string.Empty);
            return true;
        }

        private static void ValidateRequest(ProcessRequest request)
        {
            if (string.IsNullOrWhiteSpace(request.ExecutablePath) ||
                !File.Exists(request.ExecutablePath))
            {
                throw new FileNotFoundException(
                    "CWCameraWorker.exe was not found.",
                    request.ExecutablePath);
            }

            if (string.IsNullOrWhiteSpace(request.WorkingDirectory) ||
                !Directory.Exists(request.WorkingDirectory))
            {
                throw new DirectoryNotFoundException(
                    "The camera worker working directory was not found: " +
                    request.WorkingDirectory);
            }

            if (request.Arguments == null || request.Arguments.Count == 0)
            {
                throw new ArgumentException(
                    "The camera worker command is required.",
                    nameof(request));
            }

            if (string.IsNullOrWhiteSpace(request.CancellationFile))
            {
                throw new ArgumentException(
                    "The camera worker cancellation file is required.",
                    nameof(request));
            }
        }

        private static void AddRootCandidates(
            ICollection<string> candidates,
            string root)
        {
            if (string.IsNullOrWhiteSpace(root))
            {
                return;
            }

            AddCandidate(
                candidates,
                Path.Combine(root, "dist", "CWCameraWorker", ExecutableName));
            AddCandidate(
                candidates,
                Path.Combine(root, "dist", ExecutableName));
            AddCandidate(
                candidates,
                Path.Combine(root, "tools", "CWCameraWorker", ExecutableName));
            AddCandidate(
                candidates,
                Path.Combine(root, "CWCameraWorker", ExecutableName));
            AddCandidate(candidates, Path.Combine(root, ExecutableName));
        }

        private static void AddCandidate(
            ICollection<string> candidates,
            string candidate)
        {
            if (!string.IsNullOrWhiteSpace(candidate))
            {
                candidates.Add(candidate.Trim());
            }
        }

        private static string FirstNotEmpty(params string[] values)
        {
            return values.FirstOrDefault(
                       value => !string.IsNullOrWhiteSpace(value)) ??
                   string.Empty;
        }

        private static bool ContainsJsonProperty(string json, string name)
        {
            return json.IndexOf(
                       "\"" + name + "\"",
                       StringComparison.Ordinal) >= 0;
        }

        private static bool IsFinite(double value)
        {
            return !double.IsNaN(value) && !double.IsInfinity(value);
        }

        private void OnOutputDataReceived(object sender, DataReceivedEventArgs args)
        {
            EnqueueLine(args.Data, false);
        }

        private void OnErrorDataReceived(object sender, DataReceivedEventArgs args)
        {
            EnqueueLine(args.Data, true);
        }

        private void EnqueueLine(string line, bool isError)
        {
            if (string.IsNullOrWhiteSpace(line))
            {
                return;
            }

            if (TryParseEvent(line, out var parsedEvent))
            {
                messages.Enqueue(new ProcessMessage(parsedEvent));
                return;
            }

            messages.Enqueue(new ProcessMessage(line, isError));
        }

        private void DisposeProcess()
        {
            if (process == null)
            {
                return;
            }

            process.OutputDataReceived -= OnOutputDataReceived;
            process.ErrorDataReceived -= OnErrorDataReceived;
            process.Dispose();
            process = null;
        }

        private void TryDeleteCancellationFile()
        {
            if (string.IsNullOrWhiteSpace(cancellationFile) ||
                !File.Exists(cancellationFile))
            {
                return;
            }

            try
            {
                File.Delete(cancellationFile);
            }
            catch (IOException)
            {
                // A stale sentinel is deleted before the next job starts.
            }
            catch (UnauthorizedAccessException)
            {
                // Preserve the completed result even if local cleanup is denied.
            }
        }

        [Serializable]
        private sealed class CliEventPayload
        {
            public string type;
            public string eventType;
            public string @event;
            public string protocolVersion;
            public string jobId;
            public string phase;
            public string message;
            public float progress;
            public double etaSeconds;
            public string outputPath;
            public string error;
        }

        internal readonly struct ProcessRequest
        {
            internal ProcessRequest(
                string executablePath,
                string workingDirectory,
                IReadOnlyList<string> arguments,
                string cancellationFile)
            {
                ExecutablePath = executablePath;
                WorkingDirectory = workingDirectory;
                Arguments = arguments;
                CancellationFile = cancellationFile;
            }

            internal string ExecutablePath { get; }
            internal string WorkingDirectory { get; }
            internal IReadOnlyList<string> Arguments { get; }
            internal string CancellationFile { get; }
        }

        internal readonly struct ProcessMessage
        {
            internal ProcessMessage(string text, bool isError)
            {
                Text = text ?? string.Empty;
                IsError = isError;
                IsEvent = false;
                Event = default;
            }

            internal ProcessMessage(CliEvent parsedEvent)
            {
                Text = string.Empty;
                IsError = false;
                IsEvent = true;
                Event = parsedEvent;
            }

            internal string Text { get; }
            internal bool IsError { get; }
            internal bool IsEvent { get; }
            internal CliEvent Event { get; }
        }

        internal readonly struct CliEvent
        {
            internal CliEvent(
                string eventType,
                string protocolVersion,
                string jobId,
                string phase,
                string message,
                float progress,
                double etaSeconds,
                string outputPath,
                string error)
            {
                EventType = eventType;
                ProtocolVersion = protocolVersion;
                JobId = jobId;
                Phase = phase;
                Message = message;
                Progress = progress;
                EtaSeconds = etaSeconds;
                OutputPath = outputPath;
                Error = error;
            }

            internal string EventType { get; }
            internal string ProtocolVersion { get; }
            internal string JobId { get; }
            internal string Phase { get; }
            internal string Message { get; }
            internal float Progress { get; }
            internal double EtaSeconds { get; }
            internal string OutputPath { get; }
            internal string Error { get; }
        }
    }
}
