using System.ComponentModel;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Runner;
using Microsoft.EntityFrameworkCore;
using ModelContextProtocol.Server;

namespace ClaudeDo.Worker.External;

public sealed record TaskConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns);
public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns, string? VerifyCommand);
public sealed record TaskConfigResult(bool Found, TaskConfigDto? Config);
public sealed record ListConfigResult(bool Found, ListConfigDto? Config);
public sealed record SetListConfigResult(bool Ok, string ListId, ListConfigDto? Config);
public sealed record SetTaskConfigResult(bool Ok, string TaskId, int Number, TaskConfigDto? Config);

public sealed record EffectiveModelDto(string Value, string Source);
public sealed record EffectiveMaxTurnsDto(int Effective, string Source, int Requested, bool Clamped);
public sealed record EffectiveAgentPathDto(string? Value, string? Source);
public sealed record EffectiveSystemPromptDto(bool Set, IReadOnlyList<string> Sources);
public sealed record EffectiveRunConfigDto(
    string TaskId,
    int Number,
    EffectiveModelDto Model,
    EffectiveMaxTurnsDto MaxTurns,
    string Effort,
    string PermissionMode,
    EffectiveAgentPathDto AgentPath,
    EffectiveSystemPromptDto SystemPrompt,
    IReadOnlyList<string> SkillNames);

[McpServerToolType]
public sealed class ConfigMcpTools
{
    private readonly ListRepository _lists;
    private readonly TaskRepository _tasks;
    private readonly HubBroadcaster _broadcaster;
    private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;

    public ConfigMcpTools(
        ListRepository lists, TaskRepository tasks, HubBroadcaster broadcaster,
        IDbContextFactory<ClaudeDoDbContext> dbFactory)
    {
        _lists = lists;
        _tasks = tasks;
        _broadcaster = broadcaster;
        _dbFactory = dbFactory;
    }

    [McpServerTool, Description("Read a list's default run config, including its verify command — the fallback used by tasks in this list that don't set their own overrides. Returns { found: false, config: null } if none is set.")]
    public async Task<ListConfigResult> GetListConfig(string listId, CancellationToken cancellationToken)
    {
        var cfg = await _lists.GetConfigAsync(listId, cancellationToken);
        return cfg is null
            ? new ListConfigResult(false, null)
            : new ListConfigResult(true, new ListConfigDto(cfg.Model, cfg.SystemPrompt, cfg.AgentPath, cfg.MaxTurns, cfg.VerifyCommand));
    }

    // clearFields (not a per-field sentinel like UpdateList's "" for workingDir) because maxTurns is an
    // int? — there's no blank value to overload as "clear" for a number the way there is for a string.
    private static readonly string[] ConfigFieldNames = { "model", "systemPrompt", "agentPath", "maxTurns" };
    private static readonly string[] ListConfigFieldNames = { "model", "systemPrompt", "agentPath", "maxTurns", "verifyCommand" };

    private static HashSet<string> NormalizeClearFields(IReadOnlyList<string>? clearFields, string[] validFields)
    {
        var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var field in clearFields ?? Array.Empty<string>())
        {
            if (!validFields.Contains(field, StringComparer.OrdinalIgnoreCase))
                throw new InvalidOperationException(
                    $"Unknown clearFields entry '{field}'. Valid values: {string.Join(", ", validFields)}.");
            set.Add(field);
        }
        return set;
    }

    [McpServerTool, Description(
        "Set a list's default model/system prompt/agent path/max turns/verify command — the fallback for tasks " +
        "in this list that don't override them (verify command is list-only; tasks cannot override it). Only " +
        "the fields you pass are changed; omitted fields keep their current value. To clear a field instead, " +
        "name it in clearFields; clearing all five deletes the list's config unless it also carries settings " +
        "this tool doesn't expose (session skills, file-scope serialization, permission mode), which are " +
        "always preserved.")]
    public async Task<SetListConfigResult> SetListConfig(
        string listId, string? model = null, string? systemPrompt = null, string? agentPath = null,
        int? maxTurns = null, string? verifyCommand = null,
        [Description("Field names to explicitly clear: 'model', 'systemPrompt', 'agentPath', 'maxTurns', " +
            "'verifyCommand'. A field named here is cleared even if a value was also passed for it.")]
        IReadOnlyList<string>? clearFields = null,
        CancellationToken cancellationToken = default)
    {
        _ = await _lists.GetByIdAsync(listId, cancellationToken)
            ?? throw new InvalidOperationException($"List {listId} not found.");

        var clear = NormalizeClearFields(clearFields, ListConfigFieldNames);
        var existing = await _lists.GetConfigAsync(listId, cancellationToken);

        var m = clear.Contains("model") ? null : model.NullIfBlank() ?? existing?.Model;
        var sp = clear.Contains("systemPrompt") ? null : systemPrompt.NullIfBlank() ?? existing?.SystemPrompt;
        var ap = clear.Contains("agentPath") ? null : agentPath.NullIfBlank() ?? existing?.AgentPath;
        var mt = clear.Contains("maxTurns") ? null : maxTurns ?? existing?.MaxTurns;
        var vc = clear.Contains("verifyCommand") ? null : verifyCommand.NullIfBlank() ?? existing?.VerifyCommand;

        // Fields this tool doesn't expose but that live on the same row. They must survive every
        // write here — ListRepository.SetConfigAsync copies the entity verbatim, so anything left
        // at its default would silently reset (a TicketProjectId reset silently kills the
        // list<->ticket-project link — the tool has no ticket parameter on purpose, that link is
        // UI-only).
        var hasUnrelatedSettings = existing is not null
            && (existing.SessionSkills is not null
                || existing.PermissionMode is not null || existing.TicketProjectId is not null);

        ListConfigDto? config;
        var allCleared = m is null && sp is null && ap is null && mt is null && vc is null;
        if (allCleared && clear.Count > 0 && !hasUnrelatedSettings)
        {
            await _lists.DeleteConfigAsync(listId, cancellationToken);
            config = null;
        }
        else if (allCleared && existing is null)
        {
            config = null;
        }
        else
        {
            await _lists.SetConfigAsync(new ListConfigEntity
            {
                ListId = listId, Model = m, SystemPrompt = sp, AgentPath = ap, MaxTurns = mt,
                VerifyCommand = vc, SessionSkills = existing?.SessionSkills,
                PermissionMode = existing?.PermissionMode,
                TicketProjectId = existing?.TicketProjectId,
            }, cancellationToken);
            config = allCleared ? null : new ListConfigDto(m, sp, ap, mt, vc);
        }

        await _broadcaster.ListUpdated(listId);
        return new SetListConfigResult(true, listId, config);
    }

    [McpServerTool, Description(
        "Set per-task overrides for model/system prompt/agent path/max turns; these take precedence over the " +
        "list's default config for this one task. Only the fields you pass are changed; omitted fields keep " +
        "their current override. To clear a field instead, name it in clearFields.")]
    public async Task<SetTaskConfigResult> SetTaskConfig(
        string taskId, string? model = null, string? systemPrompt = null, string? agentPath = null,
        int? maxTurns = null,
        [Description("Field names to explicitly clear: 'model', 'systemPrompt', 'agentPath', 'maxTurns'. " +
            "A field named here is cleared even if a value was also passed for it.")]
        IReadOnlyList<string>? clearFields = null,
        CancellationToken cancellationToken = default)
    {
        taskId = await TaskIdResolver.ResolveAsync(_tasks, taskId, cancellationToken);
        var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
            ?? throw new InvalidOperationException($"Task {taskId} not found.");

        var clear = NormalizeClearFields(clearFields, ConfigFieldNames);

        var m = clear.Contains("model") ? null : model.NullIfBlank() ?? task.Model;
        var sp = clear.Contains("systemPrompt") ? null : systemPrompt.NullIfBlank() ?? task.SystemPrompt;
        var ap = clear.Contains("agentPath") ? null : agentPath.NullIfBlank() ?? task.AgentPath;
        var mt = clear.Contains("maxTurns") ? null : maxTurns ?? task.MaxTurns;

        // SessionSkills and PermissionMode aren't exposed here but share the write — UpdateAgentSettings
        // sets every column, so passing the current value is what keeps them from being cleared.
        await _tasks.UpdateAgentSettingsAsync(
            taskId, m, sp, ap, mt, task.SessionSkills, task.PermissionMode, cancellationToken);
        await _broadcaster.TaskUpdated(taskId);
        return new SetTaskConfigResult(true, taskId, task.Number, new TaskConfigDto(m, sp, ap, mt));
    }

    [McpServerTool, Description("Read this task's per-task overrides (model/system prompt/agent path/max turns), which take precedence over the list's default config. Returns { found: false, config: null } if none is set.")]
    public async Task<TaskConfigResult> GetTaskConfig(string taskId, CancellationToken cancellationToken)
    {
        taskId = await TaskIdResolver.ResolveAsync(_tasks, taskId, cancellationToken);
        var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
            ?? throw new InvalidOperationException($"Task {taskId} not found.");
        if (task.Model is null && task.SystemPrompt is null && task.AgentPath is null && task.MaxTurns is null)
            return new TaskConfigResult(false, null);
        return new TaskConfigResult(true, new TaskConfigDto(task.Model, task.SystemPrompt, task.AgentPath, task.MaxTurns));
    }

    [McpServerTool, Description(
        "Report the config a task will ACTUALLY run with — model, max turns, effort, permission mode, agent " +
        "path, whether a system prompt is set, and skill names — each tagged with its source " +
        "(task/list/preset/global). Use this over get_task_config/get_app_settings when you need resolved " +
        "values, not raw overrides. maxTurns also reports the raw requested value and whether it was clamped " +
        "to the global ceiling.")]
    public async Task<EffectiveRunConfigDto> GetEffectiveRunConfig(string taskId, CancellationToken cancellationToken)
    {
        taskId = await TaskIdResolver.ResolveAsync(_tasks, taskId, cancellationToken);
        var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
            ?? throw new InvalidOperationException($"Task {taskId} not found.");
        var listConfig = await _lists.GetConfigAsync(task.ListId, cancellationToken);

        AppSettingsEntity global;
        using (var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken))
            global = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);

        var systemFile = PromptFiles.ReadOrDefault(PromptKind.System);
        var isImprovementChild = task.ParentTaskId is not null && task.CreatedBy == task.ParentTaskId;
        var improvementPrompt = isImprovementChild ? PromptFiles.ReadOrDefault(PromptKind.ImprovementChild) : null;

        var effective = EffectiveRunConfigResolver.Resolve(task, listConfig, global, systemFile, improvementPrompt);

        var requestedSkills = TaskRunner.UnionSkillNames(task.SessionSkills, listConfig?.SessionSkills, global.SessionSkills);
        var skillNames = requestedSkills;
        if (requestedSkills.Count > 0)
        {
            List<SessionSkillEntity> installed;
            using (var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken))
                installed = (await new SessionSkillRepository(ctx).ListAsync(cancellationToken)).ToList();
            var installedNames = installed.Select(s => s.Name).ToHashSet(StringComparer.Ordinal);
            skillNames = TaskRunner.FilterToInstalled(requestedSkills, installedNames);
        }

        return new EffectiveRunConfigDto(
            taskId,
            task.Number,
            new EffectiveModelDto(effective.Model, effective.ModelSource),
            new EffectiveMaxTurnsDto(effective.MaxTurns, effective.MaxTurnsSource, effective.RequestedMaxTurns, effective.MaxTurnsClamped),
            effective.Effort,
            effective.PermissionMode,
            new EffectiveAgentPathDto(effective.AgentPath, effective.AgentPathSource),
            new EffectiveSystemPromptDto(effective.SystemPromptSet, effective.SystemPromptSources),
            skillNames);
    }
}
