using System.Collections.Concurrent;
using System.Text.Json;
using ClaudeDo.Data;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Usage.Interfaces;

namespace ClaudeDo.Worker.Usage;

public sealed class TranscriptUsageReader : ITranscriptUsageReader
{
    private readonly string _projectsRoot;
    private readonly ConcurrentDictionary<string, FileCacheEntry> _cache = new();

    // cfg is unused since the model/scope aggregation moved to TokenTracker, but the parameter
    // stays so the DI registration and every existing call site keep working.
    public TranscriptUsageReader(WorkerConfig cfg, string? projectsRoot = null)
    {
        _projectsRoot = projectsRoot ?? Paths.Expand("~/.claude/projects");
    }

    public Task<SessionUsageTotals?> ReadSessionTotalsAsync(string sessionId, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(sessionId) || !Directory.Exists(_projectsRoot))
            return Task.FromResult<SessionUsageTotals?>(null);

        var file = new DirectoryInfo(_projectsRoot)
            .EnumerateFiles($"{sessionId}.jsonl", SearchOption.AllDirectories)
            .FirstOrDefault();
        if (file is null) return Task.FromResult<SessionUsageTotals?>(null);

        var seenKeys = new HashSet<string>();
        long input = 0, output = 0, cacheRead = 0, cacheCreation = 0;
        foreach (var record in GetOrReadFile(file))
        {
            ct.ThrowIfCancellationRequested();
            if (!seenKeys.Add(record.DedupeKey)) continue;

            input += record.InputTokens;
            output += record.OutputTokens;
            cacheRead += record.CacheReadTokens;
            cacheCreation += record.CacheCreationTokens;
        }

        return Task.FromResult<SessionUsageTotals?>(
            new SessionUsageTotals(input, output, cacheRead, cacheCreation));
    }

    private List<UsageMessageRecord> GetOrReadFile(FileInfo info)
    {
        if (_cache.TryGetValue(info.FullName, out var cached) &&
            cached.Length == info.Length && cached.LastWriteUtc == info.LastWriteTimeUtc)
        {
            return cached.Records;
        }

        var records = ReadFile(info.FullName);
        _cache[info.FullName] = new FileCacheEntry(info.Length, info.LastWriteTimeUtc, records);
        return records;
    }

    private List<UsageMessageRecord> ReadFile(string file)
    {
        var records = new List<UsageMessageRecord>();

        foreach (var line in File.ReadLines(file))
        {
            if (string.IsNullOrWhiteSpace(line)) continue;

            JsonDocument doc;
            try { doc = JsonDocument.Parse(line); }
            catch (JsonException) { continue; }

            using (doc)
            {
                var root = doc.RootElement;
                if (root.ValueKind != JsonValueKind.Object) continue;
                if (!root.TryGetProperty("type", out var typeEl) || typeEl.GetString() != "assistant") continue;
                if (!root.TryGetProperty("timestamp", out var tsEl) ||
                    !DateTimeOffset.TryParse(tsEl.GetString(), out var ts)) continue;
                if (!root.TryGetProperty("message", out var msg) || msg.ValueKind != JsonValueKind.Object) continue;
                if (!msg.TryGetProperty("model", out var modelEl) || modelEl.ValueKind != JsonValueKind.String) continue;

                // The timestamp and model are still parsed above: they gate out non-assistant
                // lines and <synthetic> models, which is right for the session totals too.
                var model = modelEl.GetString()!;
                if (model == "<synthetic>") continue;

                long input = 0, output = 0, cacheRead = 0, cacheCreation = 0;
                if (msg.TryGetProperty("usage", out var usage) && usage.ValueKind == JsonValueKind.Object)
                {
                    input = GetLong(usage, "input_tokens");
                    output = GetLong(usage, "output_tokens");
                    cacheRead = GetLong(usage, "cache_read_input_tokens");
                    cacheCreation = GetLong(usage, "cache_creation_input_tokens");
                }

                var dedupeKey = TryGetString(root, "requestId")
                    ?? TryGetString(msg, "id")
                    ?? Guid.NewGuid().ToString();

                records.Add(new UsageMessageRecord(input, output, cacheRead, cacheCreation, dedupeKey));
            }
        }

        return records;
    }

    private static string? TryGetString(JsonElement obj, string prop) =>
        obj.TryGetProperty(prop, out var el) && el.ValueKind == JsonValueKind.String ? el.GetString() : null;

    private static long GetLong(JsonElement obj, string prop) =>
        obj.TryGetProperty(prop, out var el) && el.TryGetInt64(out var v) ? v : 0;

    private sealed record FileCacheEntry(long Length, DateTime LastWriteUtc, List<UsageMessageRecord> Records);

    private sealed record UsageMessageRecord(
        long InputTokens, long OutputTokens, long CacheReadTokens, long CacheCreationTokens,
        string DedupeKey);
}
