using System.Text.Json;
using ClaudeDo.Data;

namespace ClaudeDo.Ui.Services;

/// <summary>
/// Appends one NDJSON line per timed operation (hub invoke, bulk DB read/write) to a log file, so
/// a day of normal usage yields a file that can be sorted by duration to find real outliers.
/// Never throws — a failed write is swallowed, because a measurement must never disturb the app.
/// <see cref="Shared"/> is off by default; set <c>CLAUDEDO_OP_TIMING=1</c> to turn it back on.
/// </summary>
public sealed class OperationTiming
{
    /// <summary>Fast successful operations are dropped: the first day of data was 43% 3ms reconcile
    /// ticks, which crowd out the outliers the file exists to surface. Failed/cancelled operations
    /// are always kept regardless of duration — a 1ms cancelled call is a churn signal, not noise.</summary>
    public const int DefaultMinMs = 25;

    /// <summary>One rollover file is kept (<c>.1</c>); at ~1000 lines/h a 4 MB cap holds weeks.</summary>
    public const long DefaultMaxBytes = 4L * 1024 * 1024;

    public static string DefaultPath =>
        Path.Combine(Paths.AppDataRoot(), "logs", "operation-timing.ndjson");

    /// <summary>Kill switch for <see cref="Shared"/>: off by default, set
    /// <c>CLAUDEDO_OP_TIMING=1</c> in the environment to turn the sink back on.</summary>
    private static readonly bool DefaultEnabled =
        Environment.GetEnvironmentVariable("CLAUDEDO_OP_TIMING") == "1";

    // Settable, not just get-only: xUnit constructs real ViewModels (DetailsIslandViewModel,
    // TasksIslandViewModel, ...) that call Shared.Record directly, so without a way to redirect
    // it every test run appended its churn straight into the live app's NDJSON file at the real
    // AppData path — see Ui.Tests' TestSetup module initializer, which points this at a temp file
    // for the whole test process before any test runs.
    public static OperationTiming Shared { get; set; } = new(DefaultPath, enabled: DefaultEnabled);

    private static readonly int ProcessId = Environment.ProcessId;

    private readonly string _path;
    private readonly int _minMs;
    private readonly long _maxBytes;
    private readonly bool _enabled;
    private readonly object _writeLock = new();

    public OperationTiming(string filePath, int minMs = DefaultMinMs, long maxBytes = DefaultMaxBytes, bool enabled = true)
    {
        _path = filePath;
        _minMs = minMs;
        _maxBytes = maxBytes;
        _enabled = enabled;
    }

    public void Record(string kind, string operation, TimeSpan elapsed, bool ok)
    {
        if (!_enabled) return;

        try
        {
            var ms = (long)elapsed.TotalMilliseconds;
            if (ok && ms < _minMs) return;

            var line = JsonSerializer.Serialize(new
            {
                ts = DateTime.UtcNow,
                // Runs of the app interleave in one file; without the pid, a restart looks like
                // concurrent work by a single instance.
                pid = ProcessId,
                kind,
                op = operation,
                ms,
                ok,
            });

            lock (_writeLock)
            {
                var dir = Path.GetDirectoryName(_path);
                if (!string.IsNullOrEmpty(dir))
                    Directory.CreateDirectory(dir);
                Roll();
                File.AppendAllText(_path, line + Environment.NewLine);
            }
        }
        catch
        {
            // A measurement must never disturb the app.
        }
    }

    private void Roll()
    {
        if (!File.Exists(_path) || new FileInfo(_path).Length < _maxBytes) return;
        var rolled = _path + ".1";
        File.Delete(rolled);
        File.Move(_path, rolled);
    }
}
