using System.Text.Json;

namespace ClaudeDo.Data.Models;

/// <summary>Per-model run defaults: which reasoning effort and turn budget a session gets when
/// it runs under <paramref name="Model"/> and nothing more specific overrides it.</summary>
public sealed record ModelPreset(string Model, string Effort, int MaxTurns);

/// <summary>
/// Serialization + lookup for <see cref="AppSettingsEntity.ModelPresets"/> (a JSON array on the
/// singleton settings row). One row per <see cref="ModelRegistry.Aliases"/> entry; the row supplies
/// the *global* effort and max-turns defaults, which list- and task-level overrides still beat.
/// </summary>
public static class ModelPresets
{
    private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);

    /// <summary>Shipping defaults — one row per known model alias.</summary>
    public static IReadOnlyList<ModelPreset> Defaults { get; } = new[]
    {
        new ModelPreset("haiku",  "medium", 20),
        new ModelPreset("sonnet", "high",   30),
        new ModelPreset("opus",   "high",   40),
        new ModelPreset("fable",  "high",   25),
    };

    public static string SerializeDefaults() => Serialize(Defaults);

    public static string Serialize(IEnumerable<ModelPreset> presets)
        => JsonSerializer.Serialize(Normalize(presets), Json);

    /// <summary>Parses the stored JSON, falling back to <see cref="Defaults"/> for anything absent
    /// or malformed — a bad settings row must never stop a run from starting.</summary>
    public static IReadOnlyList<ModelPreset> Parse(string? json)
    {
        if (string.IsNullOrWhiteSpace(json)) return Defaults;
        try
        {
            var parsed = JsonSerializer.Deserialize<List<ModelPreset>>(json, Json);
            return parsed is { Count: > 0 } ? Normalize(parsed) : Defaults;
        }
        catch (JsonException)
        {
            return Defaults;
        }
    }

    /// <summary>The preset for <paramref name="model"/>, or the default row for it. Never null so
    /// callers don't have to special-case an unconfigured or unknown model. <paramref name="model"/>
    /// is resolved through <see cref="ModelRegistry.TryNormalizeAlias"/> first, so a full CLI model
    /// id (e.g. "claude-sonnet-4-6") still hits its alias's preset row instead of falling through.
    /// If <paramref name="model"/> normalizes to nothing recognized, <paramref name="fallbackMaxTurns"/>
    /// (typically <c>AppSettings.DefaultMaxTurns</c>) is used instead of a hardcoded number, so a
    /// truly unknown model is no harsher than the configured global default.</summary>
    public static ModelPreset For(IReadOnlyList<ModelPreset> presets, string? model, int fallbackMaxTurns = 30)
    {
        var alias = ModelRegistry.TryNormalizeAlias(model) ?? (model ?? ModelRegistry.DefaultAlias).Trim();
        foreach (var p in presets)
            if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase))
                return p;
        foreach (var p in Defaults)
            if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase))
                return p;
        return new ModelPreset(alias, EffortRegistry.DefaultLevel, fallbackMaxTurns);
    }

    public static ModelPreset For(string? json, string? model, int fallbackMaxTurns = 30)
        => For(Parse(json), model, fallbackMaxTurns);

    // One row per known alias, canonical order, clamped values — so an edited or partial list
    // still round-trips into something every consumer can rely on.
    private static IReadOnlyList<ModelPreset> Normalize(IEnumerable<ModelPreset> presets)
    {
        var byModel = new Dictionary<string, ModelPreset>(StringComparer.OrdinalIgnoreCase);
        foreach (var p in presets)
        {
            if (p is null || string.IsNullOrWhiteSpace(p.Model)) continue;
            byModel[p.Model.Trim()] = p;
        }

        var result = new List<ModelPreset>();
        foreach (var fallback in Defaults)
        {
            var source = byModel.TryGetValue(fallback.Model, out var found) ? found : fallback;
            var effort = TryNormalizeEffort(source.Effort) ?? fallback.Effort;
            var turns = source.MaxTurns is >= 1 and <= 200 ? source.MaxTurns : fallback.MaxTurns;
            result.Add(new ModelPreset(fallback.Model, effort, turns));
        }
        return result;
    }

    private static string? TryNormalizeEffort(string? effort)
    {
        try { return EffortRegistry.NormalizeLevel(effort); }
        catch (ArgumentException) { return null; }
    }
}
