using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text.Json;
using Dada.Supervisor;

internal static class Program
{
    private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };

    [STAThread]
    private static async Task<int> Main(string[] args)
    {
        if (args.FirstOrDefault() == "--credential-child")
        {
            return await RunCredentialChildAsync();
        }

        if (args.FirstOrDefault() == "--credential-echo")
        {
            Console.Write(await Console.In.ReadToEndAsync());
            return 0;
        }

        if (args.FirstOrDefault() == "--instance-probe")
        {
            using var instance = await SingleInstanceCoordinator.TryAcquireAsync(args[1], args[2]);
            Console.Write(instance.IsPrimary ? "primary" : "secondary");
            return 0;
        }

        if (args.FirstOrDefault() == "--managed-child")
        {
            return await RunManagedChildAsync(args);
        }

        try
        {
            var security = await TestCredentialBoundaryAsync();
            var supervisor = await TestSupervisorLifecycleAsync();
            await TestAmapProbeSecurityAsync();
            TestSecureConfigurationPersistence();
            TestRuntimeDirectoryBootstrap();
            TestStructuredLogging();
            WriteEvidence(Environment.GetEnvironmentVariable("DADA_EVIDENCE_DIR_SEC"), security);
            WriteEvidence(Environment.GetEnvironmentVariable("DADA_EVIDENCE_DIR_SUP"), supervisor);
            Console.WriteLine(JsonSerializer.Serialize(new { security = "passed", supervisor = "passed" }, JsonOptions));
            return 0;
        }
        catch (Exception exception)
        {
            Console.Error.WriteLine(exception);
            return 1;
        }
    }

    private static async Task TestAmapProbeSecurityAsync()
    {
        using var handler = AmapProbe.CreateHandler();
        False(handler.AllowAutoRedirect, "Amap probe redirects disabled");
        Equal(1, handler.MaxConnectionsPerServer, "Amap probe per-server connection cap");
        True(AmapProbe.IsAllowedEndpoint(new Uri("https://restapi.amap.com/v3/geocode/regeo")), "Amap fixed HTTPS endpoint accepted");
        False(AmapProbe.IsAllowedEndpoint(new Uri("http://restapi.amap.com/v3/geocode/regeo")), "Amap HTTP endpoint rejected");
        False(AmapProbe.IsAllowedEndpoint(new Uri("https://example.invalid/v3/geocode/regeo")), "Amap alternate host rejected");
        using var oversized = new ByteArrayContent(new byte[AmapProbe.MaximumResponseBytes + 1]);
        await ThrowsAsync<InvalidDataException>(
            () => AmapProbe.ReadBoundedJsonAsync(oversized),
            "Amap oversized response must be rejected before parsing");
    }

    private static void TestStructuredLogging()
    {
        Equal(10 * 1024 * 1024L, StructuredJsonlLogger.SizeLimitBytes, "Supervisor log byte limit");
        Equal(10, StructuredJsonlLogger.FileLimit, "Supervisor log file limit");
        Equal(30, StructuredJsonlLogger.RetentionDays, "Supervisor log retention");
        var directory = Path.Combine(Path.GetTempPath(), $"dada-supervisor-log-{Guid.NewGuid():N}");
        try
        {
            var logger = new StructuredJsonlLogger(directory, "supervisor", sizeLimitBytes: 512, fileLimit: 10);
            for (var index = 0; index < 40; index++)
            {
                logger.Write(new StructuredLogEvent("completed", $"corr_{index}", $"obj_{index}", index, "none"));
            }
            var files = Directory.GetFiles(directory, "*.jsonl");
            True(files.Length <= StructuredJsonlLogger.FileLimit, "Supervisor log file cap");
            True(files.All(path => new FileInfo(path).Length <= 512), "Supervisor log rotation byte boundary");
            foreach (var line in files.SelectMany(File.ReadLines))
            {
                using var document = JsonDocument.Parse(line);
                var keys = document.RootElement.EnumerateObject().Select(property => property.Name).ToHashSet(StringComparer.Ordinal);
                True(keys.IsSubsetOf(["schema_version", "timestamp", "component", "status_category", "correlation_id", "object_id", "duration_ms", "error_category"]), "Supervisor log field allowlist");
            }
        }
        finally
        {
            if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true);
        }
    }

    private static void TestSecureConfigurationPersistence()
    {
        var directory = Path.Combine(Path.GetTempPath(), $"dada-secure-config-{Guid.NewGuid():N}");
        var path = Path.Combine(directory, "instance.json");
        var email = "synthetic-admin@example.invalid";
        var pepper = $"synthetic-pepper-{Guid.NewGuid():N}";
        var digest = Convert.ToHexString(System.Security.Cryptography.HMACSHA256.HashData(
            System.Text.Encoding.UTF8.GetBytes(pepper),
            System.Text.Encoding.UTF8.GetBytes(email)));
        try
        {
            var store = new InstanceConfigurationStore(path);
            store.Save(new InstanceConfiguration(1, 7, null, null, [digest], [digest]));
            var raw = File.ReadAllText(path);
            True(raw.Contains("\"secure_config_revision\": 7", StringComparison.Ordinal), "secure config revision field");
            True(raw.Contains("\"schema_version\": 1", StringComparison.Ordinal), "secure config schema field");
            False(raw.Contains(email, StringComparison.OrdinalIgnoreCase), "allowlist email persisted");
            False(raw.Contains(pepper, StringComparison.Ordinal), "admin pepper persisted");
            False(File.Exists(path + ".tmp"), "secure config temporary file residual");
            var loaded = store.Load();
            Equal(7, loaded.Revision, "secure config revision round trip");
            EqualSequence(new[] { digest }, loaded.AdminRecoveryHashes, "secure config recovery marker");
        }
        finally
        {
            if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true);
        }
    }

    private static void TestRuntimeDirectoryBootstrap()
    {
        var root = Path.Combine(Path.GetTempPath(), $"dada-runtime-root-{Guid.NewGuid():N}");
        try
        {
            Directory.CreateDirectory(root);
            SupervisorRuntime.EnsureRuntimeDirectories(root);
            foreach (var relativePath in new[]
            {
                "db", "content/references", "content/generated", "content/exports",
                "managed-assets", "derived-assets", "staging",
                "logs/api", "logs/worker", "logs/supervisor",
            })
            {
                True(Directory.Exists(Path.Combine(root, relativePath)), $"runtime directory missing: {relativePath}");
            }
        }
        finally
        {
            if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
        }
    }

    private static async Task<object> TestCredentialBoundaryAsync()
    {
        var store = new TestCredentialStore();
        var marker = $"wp0-{Guid.NewGuid():N}";
        store.Write(CredentialCatalog.ApiResend, marker + "-mail");
        store.Write(CredentialCatalog.ApiAmap, marker + "-map");
        store.Write(CredentialCatalog.AdminPepper, marker + "-admin");
        store.Write(CredentialCatalog.WorkerAiGateway, marker + "-ai");

        var apiProbe = await LaunchCredentialProbeAsync(ChildRole.Api, store);
        EqualSequence(new[] { CredentialCatalog.AdminPepper, CredentialCatalog.ApiAmap, CredentialCatalog.ApiResend }, apiProbe.Names.Order().ToArray(), "API credential scope");
        False(apiProbe.EnvironmentContainsMarker, "credential leaked into child environment");
        False(apiProbe.ArgumentsContainMarker, "credential leaked into child arguments");

        var workerProbe = await LaunchCredentialProbeAsync(ChildRole.Worker, store);
        EqualSequence(new[] { CredentialCatalog.WorkerAiGateway }, workerProbe.Names, "Worker credential scope");
        var leakProbe = await CredentialProcessLauncher.RunToCompletionAsync(
            new ProcessStartInfo(Environment.ProcessPath!, "--credential-echo"), ChildRole.Worker, store);
        True(leakProbe.SensitiveOutputDetected, "credential echo must be detected");
        Equal(string.Empty, leakProbe.StandardOutput, "credential echo output discarded");
        Equal(string.Empty, leakProbe.StandardError, "credential echo error discarded");
        True(AiGatewayProbe.TryValidateOutput("{\"code\":\"ai_probe_passed\",\"mime_type\":\"image/png\",\"pixel_height\":1080,\"pixel_width\":1080,\"real_calls\":1,\"success\":true}", out _), "AI probe success output accepted");
        False(AiGatewayProbe.TryValidateOutput("{\"code\":\"ai_probe_passed\",\"raw_body\":\"private\",\"real_calls\":1,\"success\":true}", out _), "AI probe private output rejected");

        var externalArguments = new[]
        {
            "--service", "ai-gateway-service-id",
            "--model", "gpt-image-2",
            "--run-id", "wp7-02-supervisor-probe",
            "--max-real-calls", "120",
            "--confirm-controlled-real",
            "--execute-controlled-real",
        };
        EqualSequence(externalArguments, ControlledExternalValidationLauncher.ValidateArguments(externalArguments), "controlled external argument allowlist");
        var stableGeminiArguments = externalArguments.ToArray();
        stableGeminiArguments[3] = "gemini-3.1-flash-image";
        EqualSequence(stableGeminiArguments, ControlledExternalValidationLauncher.ValidateArguments(stableGeminiArguments), "stable Gemini external argument allowlist");
        var previewGeminiArguments = externalArguments.ToArray();
        previewGeminiArguments[3] = "gemini-3.1-flash-image-preview";
        Throws<ArgumentException>(
            () => ControlledExternalValidationLauncher.ValidateArguments(previewGeminiArguments),
            "preview Gemini external argument rejected");
        Throws<ArgumentException>(
            () => ControlledExternalValidationLauncher.ValidateArguments(externalArguments.Where(value => value != "--confirm-controlled-real").ToArray()),
            "controlled external confirmation required");

        store.Delete(CredentialCatalog.WorkerAiGateway);
        await ThrowsAsync<MissingCredentialException>(
            () => LaunchCredentialProbeAsync(ChildRole.Worker, store),
            "cleared credential must block the next child start");

        var report = DiagnosticReport.Create(
            SupervisorState.WorkerDegraded,
            new[]
            {
                DiagnosticCheck.Pass("fixed_port", "loopback_ready"),
                DiagnosticCheck.Warning("worker_health", "worker_unavailable"),
            },
            new DiagnosticStorageSummary(1024, 2048, 512),
            new[] { new CredentialStatus("worker_ai_gateway", false) });
        var diagnosticJson = JsonSerializer.Serialize(report, JsonOptions);
        False(diagnosticJson.Contains(marker, StringComparison.Ordinal), "diagnostic report contains credential data");
        False(diagnosticJson.Contains("stack", StringComparison.OrdinalIgnoreCase), "diagnostic report exposes stack data");

        return new
        {
            processEnvironmentScan = new
            {
                api = apiProbe,
                worker = workerProbe,
                marker_absent = true,
                status = "passed",
            },
            pipeAcl = new
            {
                channel = "inherited_anonymous_standard_input",
                child_only = true,
                one_shot = true,
                status = "passed",
            },
            redaction = new
            {
                allowed_fields = report.GetType().GetProperties().Select(property => property.Name).Order().ToArray(),
                marker_absent = true,
                status = "passed",
            },
        };
    }

    private static async Task<CredentialProbe> LaunchCredentialProbeAsync(ChildRole role, ICredentialStore store)
    {
        var result = await CredentialProcessLauncher.RunToCompletionAsync(new ProcessStartInfo(Environment.ProcessPath!, "--credential-child"), role, store);
        Equal(0, result.ExitCode, "credential child exit code");
        False(result.SensitiveOutputDetected, "credential child output contains injected value");
        return JsonSerializer.Deserialize<CredentialProbe>(result.StandardOutput, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
            ?? throw new InvalidOperationException("Credential child returned invalid JSON.");
    }

    private static async Task<int> RunCredentialChildAsync()
    {
        using var input = new StreamReader(Console.OpenStandardInput());
        var json = await input.ReadToEndAsync();
        using var document = JsonDocument.Parse(json);
        var names = document.RootElement.EnumerateObject().Select(property => property.Name).Order().ToArray();
        var values = document.RootElement.EnumerateObject().Select(property => property.Value.GetString() ?? string.Empty).ToArray();
        var markerPrefix = values.FirstOrDefault()?.Split('-').Take(2).Aggregate((left, right) => left + "-" + right) ?? string.Empty;
        var environmentContainsMarker = !string.IsNullOrEmpty(markerPrefix) && Environment.GetEnvironmentVariables().Values.Cast<object?>().Any(value => value?.ToString()?.Contains(markerPrefix, StringComparison.Ordinal) == true);
        var argumentsContainMarker = !string.IsNullOrEmpty(markerPrefix) && Environment.GetCommandLineArgs().Any(value => value.Contains(markerPrefix, StringComparison.Ordinal));
        Console.Write(JsonSerializer.Serialize(new CredentialProbe(names, environmentContainsMarker, argumentsContainMarker)));
        return 0;
    }

    private static async Task<object> TestSupervisorLifecycleAsync()
    {
        EqualSequence(new[] { 1, 5, 15 }, RestartPolicy.Delays.Select(delay => (int)delay.TotalSeconds).ToArray(), "restart backoff");
        Equal(3, RestartPolicy.MaximumRestarts, "restart limit");
        False(RestartPolicy.CanRestart(3, TimeSpan.FromMinutes(1)), "fourth restart must be blocked");

        using var occupied = new TcpListener(IPAddress.Loopback, LoopbackEndpoint.Port);
        occupied.Start();
        var portState = LoopbackPortGuard.Check();
        Equal(SupervisorState.PortInUse, portState, "fixed occupied port state");
        occupied.Stop();
        Equal(SupervisorState.Starting, LoopbackPortGuard.Check(), "fixed free port state");

        var mutexName = $"Dada.P0A.Instance.Tests.{Guid.NewGuid():N}";
        var pipeName = $"Dada.P0A.Open.Tests.{Guid.NewGuid():N}";
        using var primary = await SingleInstanceCoordinator.TryAcquireAsync(mutexName, pipeName);
        True(primary.IsPrimary, "first instance must be primary");
        var notification = primary.WaitForOpenRequestAsync(TimeSpan.FromSeconds(5));
        using var secondary = Process.Start(new ProcessStartInfo(Environment.ProcessPath!, $"--instance-probe {mutexName} {pipeName}")
        {
            RedirectStandardOutput = true,
            UseShellExecute = false,
        }) ?? throw new InvalidOperationException("Unable to start second-instance probe.");
        var probeResult = await secondary.StandardOutput.ReadToEndAsync();
        await secondary.WaitForExitAsync();
        Equal("secondary", probeResult, "second instance result");
        True(await notification, "primary did not receive second-instance open request");

        var childStore = new TestCredentialStore();
        childStore.Write(CredentialCatalog.ApiResend, $"probe-{Guid.NewGuid():N}-mail");
        childStore.Write(CredentialCatalog.ApiAmap, $"probe-{Guid.NewGuid():N}-map");
        childStore.Write(CredentialCatalog.AdminPepper, $"probe-{Guid.NewGuid():N}-admin");
        var childStart = new ProcessStartInfo(Environment.ProcessPath!);
        childStart.ArgumentList.Add("--managed-child");
        var managed = await ManagedChildProcess.StartAsync(childStart, ChildRole.Api, childStore);
        var managedPid = managed.Process.Id;
        False(managed.Process.HasExited, "managed child should be running after ready");
        await managed.StopAsync();
        True(managed.Process.HasExited, "managed child did not stop through the control pipe");
        await managed.DisposeAsync();
        var residual = Process.GetProcesses().Count(process => process.Id == managedPid);
        Equal(0, residual, "managed child residual process");

        var readyActions = SupervisorActions.For(SupervisorState.Ready);
        True(readyActions.Contains(SupervisorAction.OpenProduct), "ready state open action");
        var failedActions = SupervisorActions.For(SupervisorState.StartupFailed);
        EqualSequence(new[] { SupervisorAction.Retry, SupervisorAction.OpenDiagnostics, SupervisorAction.Exit }, failedActions, "startup failure actions");
        False(SupervisorActions.For(SupervisorState.StorageFull).Contains(SupervisorAction.RestartServices), "storage-full state must not offer service restart");
        False(SupervisorActions.For(SupervisorState.StorageUnavailable).Contains(SupervisorAction.OpenProduct), "storage-unavailable state must not open the product");

        using (var menuForm = new SupervisorForm(SupervisorState.Ready))
        {
            EqualSequence(new[] { "打开 Dada", "运行状态", "打开诊断", "重新启动服务", "退出 Dada" }, menuForm.TrayMenuLabels, "tray menu order");
        }
        using (var diagnostics = new DiagnosticsForm(SupervisorState.StorageUnavailable))
        {
            True(diagnostics.CopyPayload.Contains("log_write_failed", StringComparison.Ordinal), "diagnostic log status");
            False(diagnostics.CopyPayload.Contains("CredentialBlob", StringComparison.Ordinal), "diagnostic credential redaction");
        }

        var screenshotPath = Path.Combine(Environment.GetEnvironmentVariable("DADA_EVIDENCE_DIR_SUP") ?? Path.GetTempPath(), "screenshots", "system-ui.png");
        Directory.CreateDirectory(Path.GetDirectoryName(screenshotPath)!);
        CaptureWindow(new SupervisorForm(SupervisorState.WorkerDegraded), screenshotPath);
        CaptureWindow(new SupervisorForm(SupervisorState.Starting), Path.Combine(Path.GetDirectoryName(screenshotPath)!, "starting.png"));
        CaptureWindow(new SupervisorForm(SupervisorState.StartupFailed), Path.Combine(Path.GetDirectoryName(screenshotPath)!, "startup-failed.png"));
        CaptureWindow(new SupervisorForm(SupervisorState.PortInUse), Path.Combine(Path.GetDirectoryName(screenshotPath)!, "port-in-use.png"));
        CaptureWindow(new SupervisorForm(SupervisorState.ApiDegraded), Path.Combine(Path.GetDirectoryName(screenshotPath)!, "api-degraded.png"));
        CaptureWindow(new SupervisorForm(SupervisorState.StorageFull), Path.Combine(Path.GetDirectoryName(screenshotPath)!, "storage-full.png"));
        CaptureWindow(new SupervisorForm(SupervisorState.StorageUnavailable), Path.Combine(Path.GetDirectoryName(screenshotPath)!, "storage-unavailable.png"));
        CaptureWindow(new DiagnosticsForm(SupervisorState.WorkerDegraded), Path.Combine(Path.GetDirectoryName(screenshotPath)!, "diagnostics.png"));

        return new
        {
            port = new { host = LoopbackEndpoint.Host, port = LoopbackEndpoint.Port, alternate_port_attempted = false, status = "passed" },
            processTree = new { second_instance = "rejected_and_notified", managed_child_pid = managedPid, residual_processes = residual, shutdown_deadline_seconds = (int)ManagedChildProcess.ShutdownDeadline.TotalSeconds, status = "passed" },
            supervisorEvents = new { restart_delays_seconds = new[] { 1, 5, 15 }, maximum_restarts = 3, system_ui_states = new[] { "starting", "ready", "startup_failed", "port_in_use", "api_degraded", "worker_degraded", "storage_full", "storage_unavailable", "diagnostics_running", "diagnostics_ready" }, terminal_state = "worker_degraded", status = "passed" },
        };
    }

    private static void CaptureWindow(Form form, string path)
    {
        using (form)
        using (var bitmap = new Bitmap(form.ClientSize.Width, form.ClientSize.Height))
        {
            form.Show();
            Application.DoEvents();
            form.DrawToBitmap(bitmap, form.ClientRectangle);
            bitmap.Save(path);
            form.Hide();
        }
    }

    private static async Task<int> RunManagedChildAsync(string[] args)
    {
        using var input = new StreamReader(Console.OpenStandardInput());
        var payload = await input.ReadToEndAsync();
        using var credentials = JsonDocument.Parse(payload);
        var pipeIndex = Array.IndexOf(args, "--dada-control-pipe");
        if (pipeIndex < 0 || pipeIndex + 1 >= args.Length) return 4;
        await using var pipe = new System.IO.Pipes.NamedPipeClientStream(".", args[pipeIndex + 1], System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous);
        await pipe.ConnectAsync(5000);
        using var reader = new StreamReader(pipe, leaveOpen: true);
        await using var writer = new StreamWriter(pipe, leaveOpen: true) { AutoFlush = true };
        await writer.WriteLineAsync("ready");
        var command = await reader.ReadLineAsync();
        return command == "shutdown" ? 0 : 5;
    }

    private static void WriteEvidence(string? directory, object result)
    {
        if (string.IsNullOrWhiteSpace(directory)) return;
        Directory.CreateDirectory(directory);
        var root = JsonSerializer.SerializeToElement(result, JsonOptions);
        foreach (var property in root.EnumerateObject())
        {
            var fileName = property.Name switch
            {
                "processEnvironmentScan" => "process-env-scan.json",
                "pipeAcl" => "pipe-acl.json",
                "processTree" => "process-tree.json",
                "supervisorEvents" => "supervisor-events.json",
                _ => property.Name + ".json",
            };
            File.WriteAllText(Path.Combine(directory, fileName), JsonSerializer.Serialize(property.Value, JsonOptions) + Environment.NewLine);
        }
    }

    private static void True(bool value, string message)
    {
        if (!value) throw new InvalidOperationException(message);
    }

    private static void False(bool value, string message) => True(!value, message);

    private static void Equal<T>(T expected, T actual, string message) where T : notnull
    {
        if (!EqualityComparer<T>.Default.Equals(expected, actual))
        {
            throw new InvalidOperationException($"{message}: expected {expected}, got {actual}");
        }
    }

    private static void EqualSequence<T>(IReadOnlyList<T> expected, IReadOnlyList<T> actual, string message)
    {
        if (!expected.SequenceEqual(actual))
        {
            throw new InvalidOperationException($"{message}: expected [{string.Join(",", expected)}], got [{string.Join(",", actual)}]");
        }
    }

    private static async Task ThrowsAsync<TException>(Func<Task> action, string message) where TException : Exception
    {
        try
        {
            await action();
        }
        catch (TException)
        {
            return;
        }
        throw new InvalidOperationException(message);
    }

    private static void Throws<TException>(Action action, string message) where TException : Exception
    {
        try
        {
            action();
        }
        catch (TException)
        {
            return;
        }
        throw new InvalidOperationException(message);
    }

    private sealed record CredentialProbe(string[] Names, bool EnvironmentContainsMarker, bool ArgumentsContainMarker);

    private sealed class TestCredentialStore : ICredentialStore
    {
        private readonly Dictionary<string, string> values = new(StringComparer.Ordinal);
        public void Delete(string target) => values.Remove(target);
        public bool IsConfigured(string target) => values.ContainsKey(target);
        public string? Read(string target) => values.GetValueOrDefault(target);
        public void Write(string target, string value) => values[target] = value;
    }
}
