using System.Text;
using System.Text.RegularExpressions;
using ClaudeDo.Worker.Git;

namespace ClaudeDo.Worker.Findings;

/// <summary>One durable finding, as handed in by a caller. Git and DB lookups happen above this layer.</summary>
public sealed record FindingInput(
    string Slug,
    string Title,
    string Body,
    string Scope,
    string SourceTaskId,
    string VerifiedAgainst);

public sealed record SaveFindingOutcome(
    string Slug,
    string Path,
    bool Created,
    int TotalFindings,
    bool NearCapacity);

/// <summary>
/// Owns &lt;working-dir&gt;/.claudedo/ — one markdown file per finding plus a rebuilt INDEX.md.
/// Pure filesystem: the caller supplies the head commit and source task id.
/// INDEX.md is always regenerated from the files on disk, never patched, so findings the user
/// deleted or renamed by hand disappear from the index on the next write.
/// </summary>
public sealed class FindingsStore
{
    /// <summary>INDEX.md is read by every run; past this many entries it stops paying for itself.</summary>
    public const int WarnThreshold = 80;

    private static readonly Regex SlugPattern = new("^[a-z0-9]+(-[a-z0-9]+)*$", RegexOptions.Compiled);
    private readonly SemaphoreSlim _indexLock = new(1, 1);

    public static string StoreDir(string workingDir) => Path.Combine(workingDir, ".claudedo");
    public static string TrapsDir(string workingDir) => Path.Combine(StoreDir(workingDir), "traps");
    public static string IndexPath(string workingDir) => Path.Combine(StoreDir(workingDir), "INDEX.md");

    public async Task<SaveFindingOutcome> SaveAsync(
        string workingDir, FindingInput input, CancellationToken ct, bool tracked = false)
    {
        if (string.IsNullOrWhiteSpace(input.Slug) || input.Slug.Length > 60 || !SlugPattern.IsMatch(input.Slug))
            throw new ArgumentException(
                $"Slug '{input.Slug}' is invalid. Use lowercase kebab-case, max 60 chars (e.g. 'conpty-arg-quoting').",
                nameof(input));
        if (string.IsNullOrWhiteSpace(input.Title))
            throw new ArgumentException("Title must not be empty.", nameof(input));
        if (string.IsNullOrWhiteSpace(input.Body))
            throw new ArgumentException("Body must not be empty.", nameof(input));

        var trapsDir = TrapsDir(workingDir);
        Directory.CreateDirectory(trapsDir);

        if (!tracked)
            await GitExcludeWriter.AppendLineAsync(workingDir, "/.claudedo/", ct);

        var path = Path.Combine(trapsDir, input.Slug + ".md");
        var created = !File.Exists(path);

        await File.WriteAllTextAsync(path, Render(input), ct);

        int total;
        await _indexLock.WaitAsync(ct);
        try
        {
            total = await RebuildIndexAsync(workingDir, ct);
        }
        finally
        {
            _indexLock.Release();
        }

        return new SaveFindingOutcome(input.Slug, path, created, total, total >= WarnThreshold);
    }

    private static string Render(FindingInput input)
    {
        var sb = new StringBuilder();
        sb.Append("---\n");
        sb.Append($"slug: {input.Slug}\n");
        sb.Append($"scope: {input.Scope}\n");
        sb.Append($"source-task: {input.SourceTaskId}\n");
        sb.Append($"verified-against: {input.VerifiedAgainst}\n");
        sb.Append("---\n\n");
        sb.Append($"# {input.Title.Trim()}\n\n");
        sb.Append(input.Body.Trim());
        sb.Append('\n');
        return sb.ToString();
    }

    private static async Task<int> RebuildIndexAsync(string workingDir, CancellationToken ct)
    {
        var trapsDir = TrapsDir(workingDir);
        var slugs = Directory.Exists(trapsDir)
            ? Directory.EnumerateFiles(trapsDir, "*.md")
                .Select(Path.GetFileNameWithoutExtension)
                .Where(s => !string.IsNullOrEmpty(s))
                .Select(s => s!)
                .OrderBy(s => s, StringComparer.Ordinal)
                .ToList()
            : new List<string>();

        var sb = new StringBuilder();
        sb.Append("# Findings\n\n");
        sb.Append("Traps and invariants for this project. Read this index first, then open only the\n");
        sb.Append("findings that touch your task. Generated by ClaudeDo — edits to entries belong in\n");
        sb.Append("`traps/`, this file is rewritten on every save.\n\n");

        if (slugs.Count == 0)
        {
            sb.Append("_No findings yet._\n");
        }
        else
        {
            foreach (var slug in slugs)
            {
                var title = await ReadTitleAsync(Path.Combine(trapsDir, slug + ".md"), slug, ct);
                sb.Append($"- [{slug}](traps/{slug}.md) — {title}\n");
            }
        }

        await File.WriteAllTextAsync(IndexPath(workingDir), sb.ToString(), ct);
        return slugs.Count;
    }

    private static async Task<string> ReadTitleAsync(string path, string fallback, CancellationToken ct)
    {
        foreach (var line in await File.ReadAllLinesAsync(path, ct))
        {
            if (line.StartsWith("# ", StringComparison.Ordinal))
                return line[2..].Trim();
        }
        return fallback;
    }
}
