using System.Diagnostics;
using System.Security.Cryptography;

namespace Dada.Supervisor;

internal sealed class SupervisorRuntime : IAsyncDisposable
{
    private static readonly string[] RequiredDataDirectories =
    [
        "db",
        Path.Combine("content", "references"),
        Path.Combine("content", "generated"),
        Path.Combine("content", "exports"),
        "managed-assets",
        "derived-assets",
        "staging",
        Path.Combine("logs", "api"),
        Path.Combine("logs", "worker"),
        Path.Combine("logs", "supervisor"),
    ];
    private readonly ICredentialStore credentials;
    private ManagedComponentSupervisor? api;
    private ManagedComponentSupervisor? worker;
    private StructuredJsonlLogger? logger;

    internal SupervisorRuntime(ICredentialStore credentials)
    {
        this.credentials = credentials;
    }

    internal event Action<SupervisorState>? StateChanged;

    internal async Task<SupervisorState> StartAsync(CancellationToken cancellationToken = default)
    {
        if (LoopbackPortGuard.Check() == SupervisorState.PortInUse) return SupervisorState.PortInUse;
        var configuration = new InstanceConfigurationStore().Load();
        if (configuration.LocalDataRoot is null || configuration.AssetRoot is null ||
            !Directory.Exists(configuration.LocalDataRoot) || !Directory.Exists(configuration.AssetRoot))
        {
            return SupervisorState.StartupFailed;
        }
        EnsureRuntimeDirectories(configuration.LocalDataRoot);
        try
        {
            logger = new StructuredJsonlLogger(Path.Combine(configuration.LocalDataRoot, "logs", "supervisor"), "supervisor");
            logger.Write(new StructuredLogEvent("starting", ErrorCategory: "none"));
        }
        catch (LogWriteException)
        {
            return SupervisorState.StorageUnavailable;
        }
        EnsureAdminPepper();

        var node = Path.Combine(AppContext.BaseDirectory, "runtime", "node.exe");
        var apiEntry = Path.Combine(AppContext.BaseDirectory, "server", "api.mjs");
        var workerEntry = Path.Combine(AppContext.BaseDirectory, "server", "worker.mjs");
        if (!File.Exists(node) || !File.Exists(apiEntry) || !File.Exists(workerEntry)) return SupervisorState.StartupFailed;

        try
        {
            api = CreateComponent(node, apiEntry, ChildRole.Api, SupervisorState.ApiDegraded);
            await api.StartAsync(cancellationToken);

            worker = CreateComponent(node, workerEntry, ChildRole.Worker, SupervisorState.WorkerDegraded);
            await worker.StartAsync(cancellationToken);
            return TryLog(new StructuredLogEvent("ready", ErrorCategory: "none")) ? SupervisorState.Ready : SupervisorState.StorageUnavailable;
        }
        catch
        {
            await StopComponentsAsync();
            return TryLog(new StructuredLogEvent("failed", ErrorCategory: "service_unavailable"))
                ? SupervisorState.StartupFailed
                : SupervisorState.StorageUnavailable;
        }
    }

    internal static void EnsureRuntimeDirectories(string dataRoot)
    {
        foreach (var directory in RequiredDataDirectories)
        {
            Directory.CreateDirectory(Path.Combine(dataRoot, directory));
        }
    }

    private void EnsureAdminPepper()
    {
        if (credentials.IsConfigured(CredentialCatalog.AdminPepper)) return;
        var pepper = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
        credentials.Write(CredentialCatalog.AdminPepper, pepper);
        Array.Clear(System.Text.Encoding.UTF8.GetBytes(pepper));
    }

    private ManagedComponentSupervisor CreateComponent(string node, string entry, ChildRole role, SupervisorState degradedState)
    {
        var component = new ManagedComponentSupervisor(async cancellationToken =>
        {
            var startInfo = new ProcessStartInfo(node);
            startInfo.WorkingDirectory = AppContext.BaseDirectory;
            startInfo.Environment["DADA_SQLITE_NATIVE_BINDING"] = Path.Combine(AppContext.BaseDirectory, "server", "native", "better_sqlite3.node");
            startInfo.Environment["DADA_SUPPORT_GATE_ROOT"] = Path.Combine(AppContext.BaseDirectory, "web", "support-gate");
            startInfo.Environment["DADA_WEB_ROOT"] = Path.Combine(AppContext.BaseDirectory, "web");
            startInfo.Environment["DADA_INSTANCE_CONFIG_PATH"] = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Dada", "P0A", "config", "instance.json");
            startInfo.ArgumentList.Add(entry);
            var child = await ManagedChildProcess.StartAsync(startInfo, role, credentials, cancellationToken);
            child.StatusReceived += status =>
            {
                if (status == "storage_unavailable")
                {
                    TryLog(new StructuredLogEvent("unavailable", ErrorCategory: "log_write_failed"));
                    StateChanged?.Invoke(SupervisorState.StorageUnavailable);
                }
            };
            return child;
        }, degradedState);
        component.StateChanged += state =>
        {
            if (TryLog(new StructuredLogEvent("degraded", ErrorCategory: "service_unavailable"))) StateChanged?.Invoke(state);
        };
        return component;
    }

    private bool TryLog(StructuredLogEvent entry)
    {
        try
        {
            logger?.Write(entry);
            return true;
        }
        catch (LogWriteException)
        {
            StateChanged?.Invoke(SupervisorState.StorageUnavailable);
            return false;
        }
    }

    public async ValueTask DisposeAsync()
    {
        await StopComponentsAsync();
    }

    private async Task StopComponentsAsync()
    {
        var stops = new List<Task>();
        if (worker is not null) stops.Add(worker.DisposeAsync().AsTask());
        if (api is not null) stops.Add(api.DisposeAsync().AsTask());
        try
        {
            await Task.WhenAll(stops);
        }
        catch
        {
        }
        finally
        {
            worker = null;
            api = null;
        }
    }
}
