using System.Text.Json;
using Predictalytics.Domain.Entities;

namespace Predictalytics.Application.Services;

/// <summary>One fingerprint dimension that has drifted between a baseline and the current snapshot.</summary>
public sealed record FingerprintDriftDimension(string Dimension, string Detail, decimal Magnitude);

/// <summary>Result of comparing a baseline fingerprint snapshot to the current one.</summary>
public sealed record FingerprintDriftResult(
    bool HasDrifted,
    DateTime BaselineAt,
    DateTime CurrentAt,
    IReadOnlyList<FingerprintDriftDimension> Dimensions);

/// <summary>
/// Pure comparison of two <see cref="TraderFingerprintSnapshot"/> rows. Encodes the "is this master
/// still the same trader?" rules for strategy-drift detection (#3). No DB access — fully unit-tested.
/// </summary>
public static class FingerprintDriftCalculator
{
    // Thresholds (documented, tunable). A dimension is flagged only past these.
    public const decimal ScoreDropPoints = 15m;          // copytrading score fell by >= this many points
    public const decimal ConcentrationShift = 0.25m;     // Herfindahl moved by >= this (0..1 scale)
    public const decimal SizingRatio = 2.0m;             // P50 position size grew/shrank by >= this factor
    public const decimal CategoryMixTvd = 0.35m;         // total-variation distance of category mix
    public const double TraitJaccardDistance = 0.5;      // trait-set Jaccard distance

    public static FingerprintDriftResult Compare(TraderFingerprintSnapshot baseline, TraderFingerprintSnapshot current)
    {
        var dims = new List<FingerprintDriftDimension>();

        // 1. Copytrading score — only a DROP matters for protecting copiers.
        var scoreDelta = current.CopytradingScore - baseline.CopytradingScore;
        if (-scoreDelta >= ScoreDropPoints)
            dims.Add(new("score", $"Copytrading-Score {baseline.CopytradingScore:F0} → {current.CopytradingScore:F0}", Math.Abs(scoreDelta)));

        // 2. Category concentration — specialist ↔ generalist shift in either direction.
        var concDelta = Math.Abs(current.CategoryConcentration - baseline.CategoryConcentration);
        if (concDelta >= ConcentrationShift)
            dims.Add(new("concentration", $"Kategorie-Konzentration {baseline.CategoryConcentration:F2} → {current.CategoryConcentration:F2}", concDelta));

        // 3. Position sizing — a large jump in typical bet size.
        if (baseline.P50PositionSize > 0 && current.P50PositionSize > 0)
        {
            var ratio = current.P50PositionSize / baseline.P50PositionSize;
            if (ratio >= SizingRatio || ratio <= 1m / SizingRatio)
                dims.Add(new("sizing", $"Typische Positionsgröße ${baseline.P50PositionSize:N0} → ${current.P50PositionSize:N0}", ratio >= 1m ? ratio : 1m / ratio));
        }

        // 4. Conviction edge — a sign flip means their big bets stopped outperforming.
        var bConv = baseline.ConvictionEdgePct;
        var cConv = current.ConvictionEdgePct;
        if (bConv is > 0m && cConv is < 0m)
            dims.Add(new("conviction", $"Conviction-Edge {bConv:F1}% → {cConv:F1}% (Vorzeichenwechsel)", Math.Abs((bConv ?? 0) - (cConv ?? 0))));

        // 5. Category mix — total-variation distance between the two volume-share distributions.
        var tvd = CategoryMixTvd_(baseline.CategoryMixJson, current.CategoryMixJson);
        if (tvd >= CategoryMixTvd)
            dims.Add(new("category_mix", $"Kategorie-Mix verschoben (TVD {tvd:F2})", tvd));

        // 6. Trait set — Jaccard distance between the two trait sets.
        var (jaccard, added, removed) = TraitSetDistance(baseline.TraitSetJson, current.TraitSetJson);
        if (jaccard >= (decimal)TraitJaccardDistance)
        {
            var detail = $"Trait-Set geändert (+[{string.Join(", ", added)}] −[{string.Join(", ", removed)}])";
            dims.Add(new("trait_set", detail, jaccard));
        }

        return new FingerprintDriftResult(dims.Count > 0, baseline.CapturedAt, current.CapturedAt, dims);
    }

    private static decimal CategoryMixTvd_(string? baselineJson, string? currentJson)
    {
        var a = Parse(baselineJson);
        var b = Parse(currentJson);
        if (a.Count == 0 || b.Count == 0) return 0m;

        decimal sum = 0m;
        foreach (var key in a.Keys.Union(b.Keys))
            sum += Math.Abs(a.GetValueOrDefault(key) - b.GetValueOrDefault(key));
        return sum / 2m; // total-variation distance of two probability distributions

        static Dictionary<string, decimal> Parse(string? json)
        {
            if (string.IsNullOrWhiteSpace(json)) return new();
            try { return JsonSerializer.Deserialize<Dictionary<string, decimal>>(json) ?? new(); }
            catch { return new(); }
        }
    }

    private static (decimal Jaccard, List<string> Added, List<string> Removed) TraitSetDistance(string? baselineJson, string? currentJson)
    {
        var a = ParseSet(baselineJson);
        var b = ParseSet(currentJson);
        var union = new HashSet<string>(a); union.UnionWith(b);
        if (union.Count == 0) return (0m, new(), new());

        var intersection = new HashSet<string>(a); intersection.IntersectWith(b);
        var jaccard = 1m - (decimal)intersection.Count / union.Count;

        var added = b.Except(a).OrderBy(x => x).ToList();
        var removed = a.Except(b).OrderBy(x => x).ToList();
        return (jaccard, added, removed);

        static HashSet<string> ParseSet(string? json)
        {
            if (string.IsNullOrWhiteSpace(json)) return new();
            try { return new HashSet<string>(JsonSerializer.Deserialize<List<string>>(json) ?? new()); }
            catch { return new(); }
        }
    }
}
