using System.ComponentModel;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ModelContextProtocol.Server;

namespace ClaudeDo.Worker.External;

public sealed record RunDto(
    string Id, int RunNumber, string? SessionId, bool IsRetry,
    string? ResultMarkdown, string? StructuredOutputJson, string? ErrorMarkdown,
    int? ExitCode, int? TurnCount, int? TokensIn, int? TokensOut,
    DateTime? StartedAt, DateTime? FinishedAt);

public sealed record TaskLogResult(
    bool Available,
    IReadOnlyList<string> Entries,
    int TotalLines,
    bool Truncated);

[McpServerToolType]
public sealed class RunHistoryMcpTools
{
    private readonly TaskRunRepository _runs;
    private readonly TaskRepository _tasks;

    public RunHistoryMcpTools(TaskRunRepository runs, TaskRepository tasks)
    {
        _runs = runs;
        _tasks = tasks;
    }

    [McpServerTool, Description(
        "List all execution runs for a task — metadata, tokens, turns, result, and error per run — ordered " +
        "oldest to newest by run number, so the last entry is the most recent. Use a run's id from here with " +
        "get_run to fetch it individually.")]
    public async Task<IReadOnlyList<RunDto>> ListRuns(string taskId, CancellationToken cancellationToken)
    {
        taskId = await TaskIdResolver.ResolveAsync(_tasks, taskId, cancellationToken);
        var runs = await _runs.GetByTaskIdAsync(taskId, cancellationToken);
        return runs.Select(ToDto).ToList();
    }

    [McpServerTool, Description("Get one execution run's full detail by its run id, obtained from list_runs.")]
    public async Task<RunDto> GetRun(string runId, CancellationToken cancellationToken)
    {
        var run = await _runs.GetByIdAsync(runId, cancellationToken)
            ?? throw new InvalidOperationException($"Run {runId} not found.");
        return ToDto(run);
    }

    [McpServerTool, Description(
        "Fetch NDJSON log lines from a task's latest run — use this to check progress or debug a task without " +
        "opening the log file. Defaults to the last 50 lines. available=false means no log exists yet (queued " +
        "or just started — not an error); truncated=true when fewer entries are returned than totalLines.")]
    public async Task<TaskLogResult> GetTaskLog(
        string taskId,
        [Description("Number of trailing entries to return; ignored if offset or limit is set. Default 50.")]
        int? tail = null,
        [Description("0-based entry index to start from; overrides tail when set. Combine with limit to page through the log.")]
        int? offset = null,
        [Description("Max entries to return starting at offset. Omit to return everything from offset to the end.")]
        int? limit = null,
        CancellationToken cancellationToken = default)
    {
        taskId = await TaskIdResolver.ResolveAsync(_tasks, taskId, cancellationToken);
        var run = await _runs.GetLatestByTaskIdAsync(taskId, cancellationToken);
        if (run is null || string.IsNullOrWhiteSpace(run.LogPath) || !File.Exists(run.LogPath))
            return new TaskLogResult(false, [], 0, false);

        string allText;
        try
        {
            await using var fs = new FileStream(
                run.LogPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            using var reader = new StreamReader(fs);
            allText = await reader.ReadToEndAsync(cancellationToken);
        }
        catch (IOException)
        {
            return new TaskLogResult(false, [], 0, false);
        }

        var lines = allText.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
        var totalLines = lines.Length;

        IReadOnlyList<string> entries;
        bool truncated;

        if (offset.HasValue || limit.HasValue)
        {
            var start = Math.Max(0, offset ?? 0);
            var count = limit.HasValue ? Math.Min(limit.Value, totalLines - start) : totalLines - start;
            entries = lines.Skip(start).Take(count).ToArray();
            truncated = start > 0 || (start + count) < totalLines;
        }
        else
        {
            var take = tail ?? 50;
            if (totalLines <= take)
            {
                entries = lines;
                truncated = false;
            }
            else
            {
                entries = lines[^take..];
                truncated = true;
            }
        }

        return new TaskLogResult(true, entries, totalLines, truncated);
    }

    private static RunDto ToDto(TaskRunEntity r) => new(
        r.Id, r.RunNumber, r.SessionId, r.IsRetry,
        r.ResultMarkdown, r.StructuredOutputJson, r.ErrorMarkdown,
        r.ExitCode, r.TurnCount, r.TokensIn, r.TokensOut,
        r.StartedAt, r.FinishedAt);
}
