using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using ClaudeDo.Data;
using ClaudeDo.Worker.Online;

namespace ClaudeDo.Worker.Config;

public sealed class WorkerConfig
{
    [JsonPropertyName("db_path")]
    public string DbPath { get; set; } = "~/.claudeDo/todo.db";

    [JsonPropertyName("sandbox_root")]
    public string SandboxRoot { get; set; } = "~/.claudeDo/sandbox";

    [JsonPropertyName("log_root")]
    public string LogRoot { get; set; } = "~/.claudeDo/logs";

    /// <summary>"sibling" → place worktrees next to the target repo; "central" → under <see cref="CentralWorktreeRoot"/>.</summary>
    [JsonPropertyName("worktree_root_strategy")]
    public string WorktreeRootStrategy { get; set; } = "sibling";

    [JsonPropertyName("central_worktree_root")]
    public string CentralWorktreeRoot { get; set; } = "~/.claudeDo/worktrees";

    [JsonPropertyName("queue_backstop_interval_ms")]
    public int QueueBackstopIntervalMs { get; set; } = 30_000;

    [JsonPropertyName("signalr_port")]
    public int SignalRPort { get; set; } = 47_821;

    [JsonPropertyName("claude_bin")]
    public string ClaudeBin { get; set; } = "claude";

    /// <summary>Port for the external MCP endpoint. 0 disables the external listener entirely.</summary>
    [JsonPropertyName("external_mcp_port")]
    public int ExternalMcpPort { get; set; } = 47_822;

    /// <summary>Optional API key clients must pass via X-ClaudeDo-Key header. Null/empty = loopback trust only.</summary>
    [JsonPropertyName("external_mcp_api_key")]
    public string? ExternalMcpApiKey { get; set; }

    [JsonPropertyName("online_inbox")]
    public OnlineInboxConfig OnlineInbox { get; set; } = new();

    /// <summary>
    /// Usage-monitor poll interval while at least one task is Running. Clamped to a minimum
    /// of 60s on load — the endpoint rate-limits (429) on tighter polling.
    /// </summary>
    [JsonPropertyName("usage_poll_interval_active_seconds")]
    public int UsagePollIntervalActiveSeconds { get; set; } = 300;

    /// <summary>
    /// Usage-monitor poll interval while nothing is running. Clamped to a minimum of 60s on load.
    /// </summary>
    [JsonPropertyName("usage_poll_interval_idle_seconds")]
    public int UsagePollIntervalIdleSeconds { get; set; } = 900;

    /// <summary>Base URL of the Bandel ticket-system API. Null = ticket integration off entirely.</summary>
    [JsonPropertyName("ticket_api_base_url")]
    public string? TicketApiBaseUrl { get; set; }

    public static string DefaultConfigPath =>
        Path.Combine(Paths.AppDataRoot(), "worker.config.json");

    /// <summary>
    /// Loads the config from <paramref name="path"/> (defaults to <see cref="DefaultConfigPath"/>).
    /// Missing file → returns defaults. Resolves all path-typed fields to absolute paths.
    /// </summary>
    public static WorkerConfig Load(string? path = null)
    {
        path ??= DefaultConfigPath;

        WorkerConfig cfg;
        if (File.Exists(path))
        {
            var json = File.ReadAllText(path);
            cfg = JsonSerializer.Deserialize<WorkerConfig>(json, JsonOpts)
                ?? throw new InvalidOperationException($"Failed to parse {path}");
        }
        else
        {
            cfg = new WorkerConfig();
        }

        cfg.DbPath = Paths.Expand(cfg.DbPath);
        cfg.SandboxRoot = Paths.Expand(cfg.SandboxRoot);
        cfg.LogRoot = Paths.Expand(cfg.LogRoot);
        cfg.CentralWorktreeRoot = Paths.Expand(cfg.CentralWorktreeRoot);
        cfg.UsagePollIntervalActiveSeconds = Math.Max(60, cfg.UsagePollIntervalActiveSeconds);
        cfg.UsagePollIntervalIdleSeconds = Math.Max(60, cfg.UsagePollIntervalIdleSeconds);

        return cfg;
    }

    /// <summary>
    /// Persists ONLY the <c>online_inbox</c> section back to <paramref name="path"/>
    /// (defaults to <see cref="DefaultConfigPath"/>) without rewriting any other fields.
    /// Reads the existing JSON, replaces the <c>online_inbox</c> node, and writes back indented.
    /// </summary>
    public void SaveOnlineInbox(string? path = null)
        => SaveKey("online_inbox", JsonSerializer.SerializeToNode(OnlineInbox, InboxSerializerOpts), path);

    /// <summary>Persists ONLY <c>claude_bin</c>, same read-modify-write as
    /// <see cref="SaveOnlineInbox"/> — every other field in the file stays untouched.</summary>
    public void SaveClaudeBin(string? path = null)
        => SaveKey("claude_bin", JsonValue.Create(ClaudeBin), path);

    /// <summary>Persistiert NUR <c>ticket_api_base_url</c>, gleiches read-modify-write wie
    /// <see cref="SaveClaudeBin"/> — jedes andere Feld der Datei bleibt unangetastet.</summary>
    public void SaveTicketApiBaseUrl(string? path = null)
        => SaveKey("ticket_api_base_url", JsonValue.Create(TicketApiBaseUrl), path);

    private static void SaveKey(string key, JsonNode? value, string? path)
    {
        path ??= DefaultConfigPath;

        var root = File.Exists(path)
            ? JsonNode.Parse(File.ReadAllText(path)) as JsonObject ?? new JsonObject()
            : new JsonObject();

        root[key] = value;

        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        File.WriteAllText(path, root.ToJsonString(WriteOpts));
    }

    private static readonly JsonSerializerOptions JsonOpts = new()
    {
        ReadCommentHandling = JsonCommentHandling.Skip,
        AllowTrailingCommas = true,
    };

    private static readonly JsonSerializerOptions InboxSerializerOpts = new()
    {
        WriteIndented = false,
    };

    private static readonly JsonSerializerOptions WriteOpts = new()
    {
        WriteIndented = true,
    };
}
