using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Dada.Supervisor;

internal static class OfflineCommandRouter
{
    private const string MutexName = "Dada.P0A.Instance";
    private const string OpenPipeName = "Dada.P0A.Open";
    private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, WriteIndented = true };

    internal static async Task<int> RunAsync(string[] args, ICredentialStore credentials)
    {
        var modifying = IsModifying(args);
        using var instance = modifying ? await SingleInstanceCoordinator.TryAcquireAsync(MutexName, OpenPipeName) : null;
        if (instance is { IsPrimary: false })
        {
            WriteResult("instance_running", false);
            return 3;
        }

        try
        {
            return args[0] switch
            {
                "configure" => RunConfigure(args.Skip(1).ToArray()),
                "secrets" => await RunSecretsAsync(args.Skip(1).ToArray(), credentials),
                "admin-allowlist" => RunAdminAllowlist(args.Skip(1).ToArray(), credentials),
                "doctor" when args.Length == 1 => RunDoctor(credentials),
                "validate-external" => await ControlledExternalValidationLauncher.RunAsync(args.Skip(1).ToArray(), credentials),
                _ => Usage(),
            };
        }
        catch (Exception exception) when (exception is ArgumentException or InvalidOperationException or IOException or UnauthorizedAccessException)
        {
            WriteResult(exception.Message, false);
            return 2;
        }
    }

    private static int RunConfigure(string[] args)
    {
        var store = new InstanceConfigurationStore();
        var current = store.Load();
        InstanceConfiguration next;
        if (args is ["init", var initialDataRoot, var initialAssetRoot])
        {
            next = new InstanceConfiguration(1, current.Revision + 1, ValidateDataRoot(initialDataRoot), ValidateAssetRoot(initialAssetRoot), current.AdminAllowlistHashes, []);
        }
        else if (args is ["data-root", var updatedDataRoot])
        {
            next = current with { Revision = current.Revision + 1, LocalDataRoot = ValidateDataRoot(updatedDataRoot), AdminRecoveryHashes = [] };
        }
        else if (args is ["asset-root", var updatedAssetRoot])
        {
            next = current with { Revision = current.Revision + 1, AssetRoot = ValidateAssetRoot(updatedAssetRoot), AdminRecoveryHashes = [] };
        }
        else
        {
            return Usage();
        }
        store.Save(next);
        WriteResult("configuration_updated", true, next.Revision);
        return 0;
    }

    private static async Task<int> RunSecretsAsync(string[] args, ICredentialStore store)
    {
        if (args.Length != 2 || !TryResolveCredential(args[1], out var target)) return Usage();
        switch (args[0])
        {
            case "status":
                WriteResult(store.IsConfigured(target) ? "configured" : "not_configured", true);
                return 0;
            case "clear":
                store.Delete(target);
                WriteResult("credential_cleared", true);
                return 0;
            case "set":
                var value = ReadHiddenValue();
                if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("credential_value_required");
                store.Write(target, value);
                WriteResult("credential_saved", true);
                return 0;
            case "probe" when target == CredentialCatalog.ApiAmap:
                return AmapProbe.Run(store.Read(target));
            case "probe" when target == CredentialCatalog.WorkerAiGateway:
                return await AiGatewayProbe.RunAsync(store);
            default:
                return Usage();
        }
    }

    private static int RunAdminAllowlist(string[] args, ICredentialStore credentials)
    {
        var configStore = new InstanceConfigurationStore();
        var current = configStore.Load();
        if (args is ["status"])
        {
            WriteAllowlistResult("allowlist_status", current.AdminAllowlistHashes.Count, current.Revision);
            return 0;
        }
        if (args is not [var action, var email] || action is not ("add" or "remove")) return Usage();
        var pepper = credentials.Read(CredentialCatalog.AdminPepper) ?? throw new InvalidOperationException("admin_pepper_not_configured");
        var normalized = email.Trim().ToLowerInvariant();
        if (!normalized.Contains('@') || normalized.Length > 254) throw new ArgumentException("invalid_email");
        var digest = Convert.ToHexString(HMACSHA256.HashData(Encoding.UTF8.GetBytes(pepper), Encoding.UTF8.GetBytes(normalized)));
        var hashes = current.AdminAllowlistHashes.ToHashSet(StringComparer.Ordinal);
        if (action == "add") hashes.Add(digest); else hashes.Remove(digest);
        var recoveries = action == "add" ? new[] { digest } : Array.Empty<string>();
        var orderedHashes = hashes.Order().ToArray();
        if (action == "remove" && orderedHashes.SequenceEqual(current.AdminAllowlistHashes))
        {
            WriteAllowlistResult("allowlist_unchanged", orderedHashes.Length, current.Revision);
            return 0;
        }
        configStore.Save(current with
        {
            Revision = current.Revision + 1,
            AdminAllowlistHashes = orderedHashes,
            AdminRecoveryHashes = recoveries,
        });
        WriteAllowlistResult(action == "add" ? "allowlist_entry_added" : "allowlist_entry_removed", hashes.Count, current.Revision + 1);
        return 0;
    }

    private static int RunDoctor(ICredentialStore credentials)
    {
        var report = DiagnosticReport.Create(
            LoopbackPortGuard.Check(),
            [DiagnosticCheck.Pass("bind_host", "loopback_only"), DiagnosticCheck.Pass("fixed_port", "port_checked")],
            new DiagnosticStorageSummary(0, 0, 0),
            [
                new CredentialStatus("api_resend", credentials.IsConfigured(CredentialCatalog.ApiResend)),
                new CredentialStatus("api_amap", credentials.IsConfigured(CredentialCatalog.ApiAmap)),
                new CredentialStatus("worker_ai_gateway", credentials.IsConfigured(CredentialCatalog.WorkerAiGateway)),
            ]);
        Console.WriteLine(JsonSerializer.Serialize(report, JsonOptions));
        return 0;
    }

    private static string ValidateDataRoot(string path)
    {
        var fullPath = ValidateDirectory(path);
        var probe = Path.Combine(fullPath, $".dada-write-{Guid.NewGuid():N}.tmp");
        using (File.Create(probe)) { }
        File.Delete(probe);
        return fullPath;
    }

    private static string ValidateAssetRoot(string path) => ValidateDirectory(path);

    private static string ValidateDirectory(string path)
    {
        var fullPath = Path.GetFullPath(path);
        if (!Directory.Exists(fullPath)) throw new ArgumentException("directory_not_found");
        return fullPath;
    }

    private static bool TryResolveCredential(string alias, out string target)
    {
        target = alias switch
        {
            "api-resend" => CredentialCatalog.ApiResend,
            "api-amap" => CredentialCatalog.ApiAmap,
            "worker-ai-gateway" => CredentialCatalog.WorkerAiGateway,
            "admin-pepper" => CredentialCatalog.AdminPepper,
            _ => string.Empty,
        };
        return target.Length > 0;
    }

    private static string ReadHiddenValue()
    {
        if (Console.IsInputRedirected) return Console.ReadLine() ?? string.Empty;
        var value = new StringBuilder();
        while (true)
        {
            var key = Console.ReadKey(intercept: true);
            if (key.Key == ConsoleKey.Enter) break;
            if (key.Key == ConsoleKey.Backspace && value.Length > 0) value.Length--;
            else if (!char.IsControl(key.KeyChar)) value.Append(key.KeyChar);
        }
        Console.WriteLine();
        return value.ToString();
    }

    private static bool IsModifying(string[] args) => args.FirstOrDefault() switch
    {
        "configure" => true,
        "secrets" => args.ElementAtOrDefault(1) is "set" or "clear",
        "admin-allowlist" => args.ElementAtOrDefault(1) is "add" or "remove",
        _ => false,
    };

    private static void WriteResult(string code, bool success, int? revisionOrCount = null) =>
        Console.WriteLine(JsonSerializer.Serialize(new { code, revision_or_count = revisionOrCount, success }, JsonOptions));

    private static void WriteAllowlistResult(string code, int count, int revision) =>
        Console.WriteLine(JsonSerializer.Serialize(new { code, count, secure_config_revision = revision, success = true }, JsonOptions));

    private static int Usage()
    {
        WriteResult("usage: configure init|data-root|asset-root; secrets set|status|clear|probe; admin-allowlist add|remove|status; doctor; validate-external", false);
        return 2;
    }
}

internal sealed record InstanceConfiguration(
    [property: JsonPropertyName("schema_version")] int SchemaVersion,
    [property: JsonPropertyName("secure_config_revision")] int Revision,
    string? LocalDataRoot,
    string? AssetRoot,
    IReadOnlyList<string> AdminAllowlistHashes,
    IReadOnlyList<string> AdminRecoveryHashes)
{
    internal static InstanceConfiguration Empty { get; } = new(1, 0, null, null, [], []);
}

internal sealed class InstanceConfigurationStore
{
    private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, WriteIndented = true };
    private readonly string path;

    internal InstanceConfigurationStore(string? path = null)
    {
        this.path = path ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Dada", "P0A", "config", "instance.json");
    }

    internal InstanceConfiguration Load()
    {
        if (!File.Exists(path)) return InstanceConfiguration.Empty;
        var json = File.ReadAllText(path);
        var configuration = JsonSerializer.Deserialize<InstanceConfiguration>(json, JsonOptions) ?? InstanceConfiguration.Empty;
        using var document = JsonDocument.Parse(json);
        if (configuration.Revision == 0 && document.RootElement.TryGetProperty("revision", out var legacyRevision))
        {
            configuration = configuration with { Revision = legacyRevision.GetInt32() };
        }
        return configuration with
        {
            SchemaVersion = configuration.SchemaVersion == 0 ? 1 : configuration.SchemaVersion,
            AdminAllowlistHashes = configuration.AdminAllowlistHashes ?? [],
            AdminRecoveryHashes = configuration.AdminRecoveryHashes ?? [],
        };
    }

    internal void Save(InstanceConfiguration configuration)
    {
        if (configuration.SchemaVersion != 1 || configuration.Revision < 0) throw new InvalidOperationException("secure_config_invalid");
        var validHash = new System.Text.RegularExpressions.Regex("^[A-F0-9]{64}$", System.Text.RegularExpressions.RegexOptions.CultureInvariant);
        if (configuration.AdminAllowlistHashes.Any(hash => !validHash.IsMatch(hash)) ||
            configuration.AdminRecoveryHashes.Any(hash => !validHash.IsMatch(hash)) ||
            configuration.AdminRecoveryHashes.Any(hash => !configuration.AdminAllowlistHashes.Contains(hash, StringComparer.Ordinal)))
        {
            throw new InvalidOperationException("secure_config_invalid");
        }
        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        var temporary = path + ".tmp";
        File.WriteAllText(temporary, JsonSerializer.Serialize(configuration, JsonOptions) + Environment.NewLine);
        File.Move(temporary, path, overwrite: true);
    }
}
