namespace Predictalytics.Application.Services;

/// <summary>A master eligible for the copy portfolio.</summary>
public sealed record PortfolioCandidate(int TraderId, decimal Score, string Category);

/// <summary>A master selected into the diversified portfolio, with the reason it was picked.</summary>
public sealed record PortfolioPick(int TraderId, decimal Score, string Category, string Reason);

/// <summary>Output of the diversifier: the picks plus who was dropped and why.</summary>
public sealed record CopyPortfolioResult(
    IReadOnlyList<PortfolioPick> Picks,
    IReadOnlyList<int> DroppedForCorrelation,
    IReadOnlyList<int> DroppedForCategoryCap);

/// <summary>
/// Builds a diversified copy portfolio (#4): instead of blindly taking the top-N single scores
/// (which can be three correlated weather bots), greedily pick high-scoring masters while enforcing
/// a per-category cap and a max pairwise correlation, so the copied set actually spreads risk.
/// Pure — the similarity lookup is injected — and fully unit-tested.
/// </summary>
public static class CopyPortfolioBuilder
{
    public static CopyPortfolioResult Build(
        IReadOnlyList<PortfolioCandidate> candidates,
        Func<int, int, double> similarity,
        int size = 8,
        int maxPerCategory = 2,
        double maxSimilarity = 0.6)
    {
        var picks = new List<PortfolioPick>();
        var droppedCorr = new List<int>();
        var droppedCat = new List<int>();
        var perCategory = new Dictionary<string, int>();

        foreach (var c in candidates.OrderByDescending(c => c.Score))
        {
            if (picks.Count >= size) break;

            var catCount = perCategory.GetValueOrDefault(c.Category);
            if (catCount >= maxPerCategory)
            {
                droppedCat.Add(c.TraderId);
                continue;
            }

            // Reject if too correlated with anyone already picked.
            var clash = picks.FirstOrDefault(p => similarity(c.TraderId, p.TraderId) >= maxSimilarity);
            if (clash is not null)
            {
                droppedCorr.Add(c.TraderId);
                continue;
            }

            var reason = catCount == 0
                ? $"Score {c.Score:F0} · erste {c.Category}-Position"
                : $"Score {c.Score:F0} · diversifiziert in {c.Category}";
            picks.Add(new PortfolioPick(c.TraderId, c.Score, c.Category, reason));
            perCategory[c.Category] = catCount + 1;
        }

        return new CopyPortfolioResult(picks, droppedCorr, droppedCat);
    }
}
