using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.Json;

namespace Dada.Supervisor;

internal enum ChildRole
{
    Api,
    Worker,
}

internal static class CredentialCatalog
{
    internal const string ApiResend = "Dada/P0A/api/resend";
    internal const string ApiAmap = "Dada/P0A/api/amap";
    internal const string WorkerAiGateway = "Dada/P0A/worker/ai-gateway";
    internal const string AdminPepper = "Dada/P0A/admin/pepper";

    internal static IReadOnlyList<string> RequiredFor(ChildRole role) => role switch
    {
        ChildRole.Api => [ApiResend, ApiAmap, AdminPepper],
        ChildRole.Worker => [WorkerAiGateway],
        _ => throw new ArgumentOutOfRangeException(nameof(role)),
    };
}

internal interface ICredentialStore
{
    string? Read(string target);
    void Write(string target, string value);
    void Delete(string target);
    bool IsConfigured(string target);
}

internal sealed class MissingCredentialException(string target)
    : InvalidOperationException($"Required credential is not configured: {target}");

internal sealed record CredentialProcessResult(int ExitCode, string StandardOutput, string StandardError, bool SensitiveOutputDetected);

internal static class CredentialProcessLauncher
{
    internal static async Task<CredentialProcessResult> RunToCompletionAsync(
        ProcessStartInfo startInfo,
        ChildRole role,
        ICredentialStore store,
        CancellationToken cancellationToken = default)
    {
        var credentials = new Dictionary<string, string>(StringComparer.Ordinal);
        foreach (var target in CredentialCatalog.RequiredFor(role))
        {
            var value = store.Read(target);
            if (role == ChildRole.Worker && string.IsNullOrWhiteSpace(value)) throw new MissingCredentialException(target);
            credentials[target] = value ?? string.Empty;
        }

        startInfo.UseShellExecute = false;
        startInfo.CreateNoWindow = true;
        startInfo.RedirectStandardInput = true;
        startInfo.RedirectStandardOutput = true;
        startInfo.RedirectStandardError = true;
        using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Unable to start credential child process.");
        var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
        var errorTask = process.StandardError.ReadToEndAsync(cancellationToken);
        var payload = JsonSerializer.SerializeToUtf8Bytes(credentials);
        try
        {
            await process.StandardInput.BaseStream.WriteAsync(payload, cancellationToken);
            await process.StandardInput.BaseStream.FlushAsync(cancellationToken);
            process.StandardInput.Close();
            await process.WaitForExitAsync(cancellationToken);
            var output = await outputTask;
            var error = await errorTask;
            var sensitive = credentials.Values.Where(value => value.Length > 0).Any(value =>
                output.Contains(value, StringComparison.Ordinal) || error.Contains(value, StringComparison.Ordinal));
            return sensitive
                ? new CredentialProcessResult(1, string.Empty, string.Empty, true)
                : new CredentialProcessResult(process.ExitCode, output, error, false);
        }
        catch
        {
            if (!process.HasExited) process.Kill(entireProcessTree: true);
            throw;
        }
        finally
        {
            Array.Clear(payload);
            foreach (var target in credentials.Keys.ToArray()) credentials[target] = string.Empty;
            credentials.Clear();
        }
    }

    internal static async Task<Process> StartAsync(
        ProcessStartInfo startInfo,
        ChildRole role,
        ICredentialStore store,
        CancellationToken cancellationToken = default)
    {
        var credentials = new Dictionary<string, string>(StringComparer.Ordinal);
        foreach (var target in CredentialCatalog.RequiredFor(role))
        {
            var value = store.Read(target);
            if (role == ChildRole.Worker && string.IsNullOrWhiteSpace(value)) throw new MissingCredentialException(target);
            credentials[target] = value ?? string.Empty;
        }

        startInfo.UseShellExecute = false;
        startInfo.CreateNoWindow = true;
        startInfo.RedirectStandardInput = true;
        var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Unable to start managed child process.");
        var payload = JsonSerializer.SerializeToUtf8Bytes(credentials);
        try
        {
            await process.StandardInput.BaseStream.WriteAsync(payload, cancellationToken);
            await process.StandardInput.BaseStream.FlushAsync(cancellationToken);
        }
        catch
        {
            if (!process.HasExited) process.Kill(entireProcessTree: true);
            process.Dispose();
            throw;
        }
        finally
        {
            Array.Clear(payload);
            process.StandardInput.Close();
            foreach (var target in credentials.Keys.ToArray()) credentials[target] = string.Empty;
            credentials.Clear();
        }
        return process;
    }
}

internal sealed class WindowsCredentialStore : ICredentialStore
{
    public string? Read(string target)
    {
        if (!CredRead(target, CredentialType.Generic, 0, out var pointer))
        {
            var error = Marshal.GetLastWin32Error();
            if (error == ErrorNotFound) return null;
            throw new Win32Exception(error);
        }

        try
        {
            var credential = Marshal.PtrToStructure<NativeCredential>(pointer);
            return credential.CredentialBlobSize == 0
                ? string.Empty
                : Marshal.PtrToStringUni(credential.CredentialBlob, credential.CredentialBlobSize / sizeof(char));
        }
        finally
        {
            CredFree(pointer);
        }
    }

    public void Write(string target, string value)
    {
        var bytes = System.Text.Encoding.Unicode.GetBytes(value);
        if (bytes.Length > MaximumGenericBlobSize) throw new ArgumentOutOfRangeException(nameof(value));
        var pointer = Marshal.AllocHGlobal(bytes.Length);
        try
        {
            Marshal.Copy(bytes, 0, pointer, bytes.Length);
            var credential = new NativeCredential
            {
                Type = CredentialType.Generic,
                TargetName = target,
                CredentialBlobSize = bytes.Length,
                CredentialBlob = pointer,
                Persist = CredentialPersistence.LocalMachine,
                UserName = Environment.UserName,
            };
            if (!CredWrite(ref credential, 0)) throw new Win32Exception(Marshal.GetLastWin32Error());
        }
        finally
        {
            Marshal.Copy(new byte[bytes.Length], 0, pointer, bytes.Length);
            Marshal.FreeHGlobal(pointer);
            Array.Clear(bytes);
        }
    }

    public void Delete(string target)
    {
        if (CredDelete(target, CredentialType.Generic, 0)) return;
        var error = Marshal.GetLastWin32Error();
        if (error != ErrorNotFound) throw new Win32Exception(error);
    }

    public bool IsConfigured(string target) => Read(target) is not null;

    private const int ErrorNotFound = 1168;
    private const int MaximumGenericBlobSize = 5 * 512;

    [DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern bool CredRead(string target, CredentialType type, int flags, out IntPtr credential);

    [DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern bool CredWrite([In] ref NativeCredential credential, int flags);

    [DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern bool CredDelete(string target, CredentialType type, int flags);

    [DllImport("Advapi32.dll")]
    private static extern void CredFree(IntPtr buffer);

    private enum CredentialType : uint { Generic = 1 }
    private enum CredentialPersistence : uint { LocalMachine = 2 }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    private struct NativeCredential
    {
        public uint Flags;
        public CredentialType Type;
        public string TargetName;
        public string? Comment;
        public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
        public int CredentialBlobSize;
        public IntPtr CredentialBlob;
        public CredentialPersistence Persist;
        public int AttributeCount;
        public IntPtr Attributes;
        public string? TargetAlias;
        public string UserName;
    }
}
