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

namespace CodeRemoteManager.SetupApp;

public sealed record SetupConfigurationInput(
    string CodexCliPath,
    string TailnetOrigin,
    string? ProjectPath);

public sealed record SetupConfigurationDefaults(
    string DataDir,
    string DesktopCodexHome);

public sealed class ConfigurationMigrationException(string code) : Exception(code);

public static class ConfigurationMigration
{
    public const int CurrentSchemaVersion = 1;

    public static JsonObject BuildCandidate(
        string? existingJson,
        SetupConfigurationInput input,
        SetupConfigurationDefaults defaults)
    {
        JsonObject config;
        if (existingJson is null)
        {
            config = CreateBootstrapConfiguration(input, defaults);
        }
        else
        {
            try
            {
                config = JsonNode.Parse(existingJson) as JsonObject
                    ?? throw new ConfigurationMigrationException("configuration_json_invalid");
            }
            catch (JsonException)
            {
                throw new ConfigurationMigrationException("configuration_json_invalid");
            }
        }

        if (config.TryGetPropertyValue("schemaVersion", out var schemaNode) &&
            (schemaNode is not JsonValue schemaValue ||
             !schemaValue.TryGetValue<int>(out var schemaVersion) ||
             schemaVersion != CurrentSchemaVersion))
        {
            throw new ConfigurationMigrationException("configuration_schema_unsupported");
        }

        ValidateUniqueIds(config, "accounts");
        ValidateUniqueIds(config, "workspaces");

        config["schemaVersion"] = CurrentSchemaVersion;
        config["codexCliPath"] = input.CodexCliPath;
        config["permissionCeiling"] ??= FullPermissionCeiling();
        MergeAllowedOrigins(config, input.TailnetOrigin);
        MergeWorkspace(config, input.ProjectPath);
        return config;
    }

    private static JsonObject CreateBootstrapConfiguration(
        SetupConfigurationInput input,
        SetupConfigurationDefaults defaults)
    {
        if (input.ProjectPath is null)
            throw new ConfigurationMigrationException("configuration_project_required");
        var projectPath = Path.GetFullPath(input.ProjectPath);

        return new JsonObject
        {
            ["engine"] = "codex",
            ["bindHost"] = "127.0.0.1",
            ["port"] = 43110,
            ["dataDir"] = defaults.DataDir,
            ["projectsRoot"] = Directory.GetParent(projectPath)?.FullName ?? projectPath,
            ["accounts"] = new JsonArray
            {
                new JsonObject
                {
                    ["id"] = "desktop-codex",
                    ["label"] = "Codex",
                    ["codexHome"] = defaults.DesktopCodexHome
                }
            },
            ["workspaces"] = new JsonArray()
        };
    }

    private static JsonObject FullPermissionCeiling() => new()
    {
        ["permissionProfile"] = ":danger-full-access",
        ["approvalPolicy"] = "never"
    };

    private static void ValidateUniqueIds(JsonObject config, string listName)
    {
        if (config[listName] is not JsonArray items)
            throw new ConfigurationMigrationException("configuration_invalid");

        var ids = new HashSet<string>(StringComparer.Ordinal);
        foreach (var node in items)
        {
            if (node is not JsonObject item ||
                item["id"] is not JsonValue idValue ||
                !idValue.TryGetValue<string>(out var id) ||
                string.IsNullOrEmpty(id))
            {
                throw new ConfigurationMigrationException("configuration_invalid");
            }
            if (!ids.Add(id)) throw new ConfigurationMigrationException("configuration_duplicate_id");
        }
    }

    private static void MergeAllowedOrigins(JsonObject config, string tailnetOrigin)
    {
        var origins = config["allowedOrigins"] switch
        {
            null => new JsonArray(),
            JsonArray existing => existing,
            _ => throw new InvalidOperationException("configuration_invalid")
        };
        config["allowedOrigins"] = origins;

        var configured = new HashSet<string>(StringComparer.Ordinal);
        foreach (var node in origins)
        {
            if (node is not JsonValue value || !value.TryGetValue<string>(out var origin))
                throw new InvalidOperationException("configuration_invalid");
            configured.Add(origin);
        }

        foreach (var origin in new[] { "http://127.0.0.1:43111", tailnetOrigin })
        {
            if (configured.Add(origin)) origins.Add(origin);
        }
    }

    private static void MergeWorkspace(JsonObject config, string? selectedPath)
    {
        if (selectedPath is null) return;
        if (config["workspaces"] is not JsonArray workspaces)
            throw new InvalidOperationException("configuration_invalid");

        var projectPath = Path.GetFullPath(selectedPath);
        foreach (var node in workspaces)
        {
            if (node is not JsonObject workspace ||
                workspace["path"] is not JsonValue pathValue ||
                !pathValue.TryGetValue<string>(out var path))
                throw new InvalidOperationException("configuration_invalid");
            if (string.Equals(Path.GetFullPath(path), projectPath, StringComparison.OrdinalIgnoreCase)) return;
        }

        var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(projectPath)))
            .Substring(0, 12)
            .ToLowerInvariant();
        var baseId = $"workspace-{hash}";
        var id = baseId;
        var suffix = 2;
        var configuredIds = workspaces
            .OfType<JsonObject>()
            .Select(workspace => workspace["id"]?.GetValue<string>())
            .Where(value => value is not null)
            .ToHashSet(StringComparer.Ordinal);
        while (configuredIds.Contains(id)) id = $"{baseId}-{suffix++}";

        workspaces.Add(new JsonObject
        {
            ["id"] = id,
            ["label"] = Path.GetFileName(projectPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)),
            ["path"] = projectPath
        });
    }
}
