<?php
/**
 * ════════════════════════════════════════════════════════════════════
 * Progressive Insights Engine
 * ────────────────────────────────────────────────────────────────────
 * Applies the same Progressive Intelligence philosophy already built for
 * the 4-Stage Performance Engine to every other analytical section in
 * Reports, Session, Edge (instrument/asset-class), Strategy, Risk,
 * Drawdown, Professional Metrics, Boundary Analytics, Portfolio
 * Analytics, Lifecycle Journey, and Cross-Strategy Analytics.
 *
 * This engine does NOT decide which stats are shown per tier, that
 * structural progression already exists in reports.php via $depth gates
 * (Weekly=1, Monthly=2, Quarterly=3, Annual=4) and is preserved exactly
 * as-is. This engine only adds the narrative/coaching layer on top of
 * whatever data each tier already computes and displays, answering the
 * four confirmed questions:
 *   Weekly:    "What should I improve right now?"
 *   Monthly:   "What trading behaviors are becoming habits?"
 *   Quarterly: "Am I becoming consistently profitable, and why?"
 *   Annual:    "How has my career evolved, and what's the long-term focus?"
 *
 * Every method is a pure function over already-computed data (no new
 * queries), narrative only, per the confirmed "build on what exists"
 * instruction. Each domain keeps its own distinct voice; none of these
 * are the same template with nouns swapped.
 * ════════════════════════════════════════════════════════════════════
 */

class ProgressiveInsightsEngine
{
    private static function pickVariant(array $variants, string $seed): string
    {
        if (count($variants) === 1) return $variants[0];
        $idx = abs(crc32($seed)) % count($variants);
        return $variants[$idx];
    }

    // ────────────────────────────────────────────────────────────────
    // Behavioral Analytics, Classification Distribution
    // ────────────────────────────────────────────────────────────────
    public static function behavioral(string $period, array $classificationRows, int $totalTrades): string
    {
        if ($totalTrades === 0 || empty($classificationRows)) {
            return "No classified trades this period to draw a behavioral picture from yet.";
        }
        usort($classificationRows, fn($a, $b) => $b['count'] <=> $a['count']);
        $top = $classificationRows[0];
        $topLabel = ucwords(str_replace('_', ' ', $top['trade_classification'] ?? 'unclassified'));
        $topPct = round(($top['count'] / max(1, $totalTrades)) * 100);
        $seed = 'behavioral' . $period . $top['trade_classification'] . $top['count'];
        $periodNoun = periodNoun($period);

        // n-aware gate: calling something a "behavioral signature" or
        // "pattern" needs more than one data point, regardless of period —
        // a single trade in an annual report is still just one trade.
        // Thresholds are a judgment call in the same spirit as Trade
        // Highlights' 3-trade gate and Edge Health's 100-trade gate, not
        // independently dictated by any single source.
        if ($totalTrades === 1) {
            return "Your only closed trade this {$periodNoun} was classified \"{$topLabel}\". That's one trade, not a pattern, nothing to conclude yet from a sample of one. Keep journaling and a real picture will start to form.";
        }
        if ($totalTrades < 10) {
            return "With {$totalTrades} trades this {$periodNoun}, \"{$topLabel}\" was the most common classification ({$topPct}%). Worth noting, but still too small a sample to call it a settled behavioral pattern yet, keep watching as more trades come in.";
        }

        return match ($period) {
            'weekly' => self::pickVariant([
                "This week's most common trading behavior was \"{$topLabel}\" ({$topPct}% of trades). That's the single behavior worth watching for the rest of the week.",
                "\"{$topLabel}\" showed up most this week, at {$topPct}% of trades. Small, immediate correction there compounds fast.",
            ], $seed),
            'monthly' => self::pickVariant([
                "\"{$topLabel}\" has been the dominant classification this month at {$topPct}% of trades, if this repeats next month too, it's no longer a one-off, it's a habit forming.",
                "This month's behavioral signature is \"{$topLabel}\" ({$topPct}% of trades). Watching whether this share grows or shrinks next month will show whether it's improving or hardening into habit.",
            ], $seed),
            'quarterly' => self::pickVariant([
                "Across the quarter, \"{$topLabel}\" behavior accounted for {$topPct}% of classified trades, at this scale it's a real behavioral pattern shaping account outcomes, not noise. It's worth tying directly to which accounts passed vs. breached this quarter.",
                "Behaviorally, this quarter has been defined by \"{$topLabel}\" ({$topPct}% of trades). That consistency across three months of trading is exactly the kind of pattern that ends up determining account survival.",
            ], $seed),
            default => self::pickVariant([
                "Over the full year, \"{$topLabel}\" was the defining behavioral pattern at {$topPct}% of all classified trades, a durable trait, not a phase. Whether this pattern helped or hurt this year's outcomes is the clearest behavioral lesson to carry into next year.",
                "The year's behavioral record centers on \"{$topLabel}\" ({$topPct}% of trades). A full year of this pattern is strong evidence of who this trader is under pressure right now, and the best starting point for next year's discipline goals.",
            ], $seed),
        };
    }

    // ────────────────────────────────────────────────────────────────
    // Performance by Session
    // ────────────────────────────────────────────────────────────────
    public static function session(string $period, array $sessionRows): string
    {
        if (empty($sessionRows)) {
            return "No session data recorded this period yet.";
        }
        $ranked = $sessionRows;
        usort($ranked, fn($a, $b) => (float)$b['total_pnl'] <=> (float)$a['total_pnl']);
        $best = $ranked[0];
        $worst = $ranked[count($ranked) - 1];
        $seed = 'session' . $period . $best['session'] . $worst['session'];
        $periodNoun = periodNoun($period);
        $totalSessionTrades = array_sum(array_column($sessionRows, 'trades'));

        // n-aware gate, same principle as behavioral()/edge(): comparing
        // sessions (or noting a concentration) needs more than a trade or
        // two to mean anything, no matter how long the period nominally
        // covers, a quarter with 3 trades is still a 3-trade sample.
        if ($totalSessionTrades === 1) {
            return "Only one trade has closed this {$periodNoun}, in the {$best['session']} session. One trade says nothing about session performance yet, nothing to compare it against.";
        }
        if ($totalSessionTrades < 10) {
            if (count($ranked) < 2 || $best['session'] === $worst['session']) {
                return "Only {$totalSessionTrades} trade(s) closed this {$periodNoun}, all in {$best['session']}. Too few yet to say whether that's a deliberate specialization or simply the only session traded so far.";
            }
            return "With {$totalSessionTrades} trades this {$periodNoun}, {$best['session']} edged out {$worst['session']}. Still too small a sample to call this a real session pattern, keep watching as more trades land.";
        }

        if (count($ranked) < 2 || $best['session'] === $worst['session']) {
            return match ($period) {
                'weekly' => "Only one session traded this week ({$best['session']}), not enough spread yet to compare session performance.",
                'monthly' => "Trading this month stayed concentrated in {$best['session']}, worth deliberately testing another session to see if the edge holds elsewhere.",
                'quarterly' => "This quarter's activity was concentrated almost entirely in {$best['session']}. That's either a deliberate specialization or an untested blind spot, worth deciding which on purpose.",
                default => "The whole year's trading centered on {$best['session']}. That's a real specialization at this point, the strategic question is whether that focus is a strength to double down on or a limitation to diversify away from.",
            };
        }

        return match ($period) {
            'weekly' => self::pickVariant([
                "{$best['session']} was this week's strongest session; {$worst['session']} was the weakest. Small sample so far, worth tracking, not yet acting on.",
                "This week, results leaned toward {$best['session']} over {$worst['session']}. Too early to call it an edge, but worth watching next week.",
            ], $seed),
            'monthly' => self::pickVariant([
                "{$best['session']} has outperformed {$worst['session']} consistently enough this month to start looking like a real pattern, not chance.",
                "A month of data now shows {$best['session']} pulling ahead of {$worst['session']}, this is the point where session choice starts to matter as a deliberate strategy, not a coincidence.",
            ], $seed),
            'quarterly' => self::pickVariant([
                "Across the quarter, {$best['session']} has been the clear stronger session over {$worst['session']}. This is now a strategic input: session selection is shaping this quarter's profitability, not just individual trade outcomes.",
                "Three months of data confirm {$best['session']} as the more reliable session versus {$worst['session']}. Reallocating more trading time toward {$best['session']} is a concrete, low-risk improvement for next quarter.",
            ], $seed),
            default => self::pickVariant([
                "A full year confirms {$best['session']} as the strongest session, well ahead of {$worst['session']}. This is a durable edge, not a streak, it belongs in next year's trading plan as a stated rule, not an observation.",
                "The year-long record shows {$best['session']} consistently outperforming {$worst['session']}. That's mature enough evidence to formalize: session choice should be an explicit part of the declared strategy edge going forward.",
            ], $seed),
        };
    }

    // ────────────────────────────────────────────────────────────────
    // Where the Edge Is, Instrument Performance / Asset Class
    // ────────────────────────────────────────────────────────────────
    public static function edge(string $period, array $topInstruments): string
    {
        if (empty($topInstruments)) {
            return "No instrument data recorded this period yet.";
        }
        $ranked = $topInstruments;
        usort($ranked, fn($a, $b) => (float)$b['total_pnl'] <=> (float)$a['total_pnl']);
        $best = $ranked[0];
        $bestTrades = isset($best['trades']) ? (int)$best['trades'] : null;
        $seed = 'edge' . $period . $best['instrument'];
        $pnlStr = ($best['total_pnl'] >= 0 ? '+' : '') . number_format((float)$best['total_pnl'], 2);

        // Weekly stays fully factual, no edge language at all, full stop —
        // a week is never a long enough horizon for even a hedged edge
        // mention (see CLAUDE.md's 2026-07-26 edge-language note).
        if ($period === 'weekly') {
            return "The best-performing instrument this week was {$best['instrument']}, at {$pnlStr}" . ($bestTrades !== null ? " across {$bestTrades} trade(s)" : "") . ". That's a factual result, not a claim of edge, an edge claim needs a longer time horizon (see Quarterly or Annual reports).";
        }

        // Monthly: a *light*, hedged edge mention is allowed, distinct
        // from Weekly's plain factual framing and from Quarterly/Annual's
        // confident framing below. No "Early Signal"-style badge language
        // and no "the clearest edge signal points to X" confidence, a
        // month can name an early tendency and say it's too soon to call
        // it an edge, pointing to Quarterly/Annual for a real read. Still
        // n-aware: a single trade doesn't earn even a soft mention (see
        // CLAUDE.md's 2026-07-26 revision of the edge-language scoping).
        if ($period === 'monthly') {
            if ($bestTrades === 1) {
                return "Your only trade this month was in {$best['instrument']}, at {$pnlStr}. That's one trade, nowhere near enough yet to even call it an early tendency.";
            }
            if ($bestTrades !== null && $bestTrades < 5) {
                return "{$best['instrument']} had the strongest showing this month, at {$pnlStr} across {$bestTrades} trades, not enough history yet to call it an edge, but worth watching over the quarter.";
            }
            return "{$best['instrument']} had the strongest showing this month, at {$pnlStr}. Still too early in a single month to call it a confirmed edge, but if this holds into the quarter, it's worth watching closely.";
        }

        // Quarterly/Annual: edge language is allowed, but still scaled to
        // how many trades actually back the claim, a "definitive edge"
        // rested on 1-2 trades is exactly the overconfident framing this
        // gate exists to prevent, even at a 90-day+ horizon.
        if ($bestTrades !== null && $bestTrades < 5) {
            return match ($period) {
                'quarterly' => "{$best['instrument']} led this quarter's P&L, but on only {$bestTrades} trade(s), too thin a sample to call it the quarter's edge yet, even at three months of data. Worth watching, not yet worth acting on.",
                default => "{$best['instrument']} led this year's P&L, but on only {$bestTrades} trade(s), not enough volume on that instrument specifically to call it a real edge, even over a full year. Worth watching, not yet worth acting on.",
            };
        }

        return match ($period) {
            'quarterly' => self::pickVariant([
                "Where the edge lives this quarter is clear: {$best['instrument']}. At three months of data, this is strong enough to treat as a real strength, the strategic question is whether instrument allocation should shift to lean into it further.",
                "Quarterly results confirm {$best['instrument']} as the strongest edge in the portfolio. This is the kind of finding that should influence which instruments get prioritized next quarter, not just be logged as a curiosity.",
            ], $seed),
            default => self::pickVariant([
                "Across the full year, {$best['instrument']} has been the definitive source of edge. That's a career-level finding, it belongs explicitly in the declared strategy, not left as an informal observation.",
                "A year of data settles the question: the edge lives in {$best['instrument']}. Going into next year, the strategic move is either to concentrate further here or deliberately test whether that edge transfers to related instruments.",
            ], $seed),
        };
    }

    // ────────────────────────────────────────────────────────────────
    // Strategy Performance
    // ────────────────────────────────────────────────────────────────
    public static function strategyPerformance(string $period, array $strategyPerformance): string
    {
        if (empty($strategyPerformance)) {
            return "No strategy activity recorded this period yet.";
        }
        $top = $strategyPerformance[0];
        $topTrades = isset($top['trades']) ? (int)$top['trades'] : null;
        $seed = 'strategy' . $period . ($top['name'] ?? '');
        $periodNoun = periodNoun($period);

        // Real bug found and fixed 2026-08-18 (Reports verification pass,
        // caught live via a real "Undefined array key: version_number"
        // warning + a broken "breakout (v)" render): every branch below
        // used to append "(v{$top['version_number']})" to the strategy
        // name, but $strategyPerformance (built in reports.php) has never
        // carried a version_number column, and per the TRADERALIGN STRATEGY
        // BUSINESS RULES redesign (2026-08-11, see CLAUDE.md §13) version
        // numbers are internal bookkeeping only and must never be shown to
        // the trader anywhere in the app, reports.php's own strategy
        // displays were already correctly updated for that redesign; this
        // file's narrative sentences were the one place that update never
        // reached. Fixed by dropping the "(vN)" suffix entirely, not by
        // resurrecting a version number that was deliberately removed
        // app-wide.
        $stratName = $top['name'];

        // n-aware gate: leading the portfolio on one or two trades isn't
        // evidence a strategy "works", same principle as behavioral()/
        // edge()/session() above.
        if ($topTrades === 1) {
            return "{$stratName} has only one closed trade this {$periodNoun} so far. One trade doesn't confirm or deny a strategy, keep journaling before drawing any conclusion.";
        }
        if ($topTrades !== null && $topTrades < 10) {
            return "{$stratName} led this {$periodNoun} with {$topTrades} trades. That's still a small sample, worth watching, but not yet enough to call this strategy confirmed.";
        }

        // Narrative audit §3.1: every branch below this point describes
        // $top as having "led"/"beaten"/"proven itself the strongest" —
        // language that implies a real comparison against other
        // strategies. When exactly one strategy was active this period,
        // there was no competition to win; describe the volume plainly
        // instead of implying it outperformed anything.
        if (count($strategyPerformance) === 1) {
            return match ($period) {
                'weekly' => "{$stratName} was the only strategy traded this week, across {$topTrades} trades. No comparison to make yet, this is simply what was traded.",
                'monthly' => "{$stratName} was the only strategy traded this month, across {$topTrades} trades. Worth checking whether results are holding to its own declared edge, there's no second strategy yet to compare it against.",
                'quarterly' => "{$stratName} has been the only strategy traded this quarter, across {$topTrades} trades. That's real volume on one strategy, but it isn't a portfolio comparison, trading a second strategy alongside it is what would show whether this one is genuinely strong or simply the only one tried.",
                default => "{$stratName} was the only strategy traded this year, across {$topTrades} trades. A full year on one strategy is a real commitment worth reviewing on its own terms, but without a second strategy in the mix, there's nothing yet to say it's the strongest in the portfolio.",
            };
        }

        return match ($period) {
            'weekly' => "This week, {$stratName} led in results. Early days, one strong week doesn't confirm the strategy yet.",
            'monthly' => self::pickVariant([
                "{$stratName} has been the strongest performer this month. Worth checking whether that's holding to its own declared edge or just running hot.",
                "A month in, {$stratName} is out in front. That's enough data to start comparing actual results against what this strategy declared it should do.",
            ], $seed),
            'quarterly' => self::pickVariant([
                "{$stratName} has led the portfolio this quarter. At this scale, that's a real strategic signal, worth deciding whether to allocate more capital or trading time toward it next quarter.",
                "Quarterly results put {$stratName} ahead of every other strategy. That's the kind of finding that should shape which strategy gets prioritized going forward.",
            ], $seed),
            default => self::pickVariant([
                "Across the year, {$stratName} has proven itself the strongest strategy in the portfolio. That's a career-level result, it deserves to anchor next year's trading plan.",
                "The year's clearest strategic winner is {$stratName}. Twelve months of evidence is enough to make this the primary strategy going into next year, with everything else tested against it.",
            ], $seed),
        };
    }

    // ────────────────────────────────────────────────────────────────
    // Risk Analytics, Performance Analysis (Expectancy/Profit Factor)
    // ────────────────────────────────────────────────────────────────
    public static function risk(string $period, ?float $expectancy, ?float $profitFactor, int $overRiskCount, int $totalTrades): string
    {
        if ($expectancy === null) {
            return "Not enough closed trades yet this period to compute expectancy or profit factor reliably.";
        }
        $overRiskPct = $totalTrades > 0 ? round(($overRiskCount / $totalTrades) * 100) : 0;
        $seed = 'risk' . $period . round($expectancy, 2) . $overRiskCount;
        $periodNoun = periodNoun($period);

        // n-aware gate: expectancy/profit factor computed from a handful
        // of trades is not yet a reliable baseline, however confident the
        // raw number looks, same principle as the functions above.
        if ($totalTrades === 1) {
            return "Your only closed trade this {$periodNoun} produced " . ($expectancy >= 0 ? '+' : '') . number_format($expectancy, 2) . ($overRiskCount > 0 ? ", and exceeded the declared risk limit" : "") . ". One trade isn't an expectancy, it's a single outcome. Nothing to conclude about risk-adjusted performance yet.";
        }
        if ($totalTrades < 10) {
            return "With {$totalTrades} trades this {$periodNoun}, expectancy sits at " . ($expectancy >= 0 ? '+' : '') . number_format($expectancy, 2) . " per trade" . ($overRiskCount > 0 ? " ({$overRiskCount} trade(s) over the declared risk limit)" : "") . ". Still too small a sample for this number to be reliable, treat it as an early read, not a settled baseline.";
        }

        return match ($period) {
            'monthly' => self::pickVariant([
                "This month's expectancy is " . ($expectancy >= 0 ? '+' : '') . number_format($expectancy, 2) . " per trade" . ($overRiskCount > 0 ? ", with {$overRiskCount} trade(s) ({$overRiskPct}%) exceeding the declared risk limit, that's the fastest lever to improve next month's numbers." : ", and every trade stayed within the declared risk limit, that discipline is exactly what makes expectancy reliable going forward.") . "",
                "Expectancy this month sits at " . ($expectancy >= 0 ? '+' : '') . number_format($expectancy, 2) . " per trade." . ($overRiskCount > 0 ? " {$overRiskCount} of {$totalTrades} trades ({$overRiskPct}%) broke the declared risk cap, tightening that alone would likely move the expectancy number." : " No risk-limit breaches this month, which is the foundation good expectancy is built on."),
            ], $seed),
            'quarterly' => self::pickVariant([
                "Over the quarter, expectancy has held at " . ($expectancy >= 0 ? '+' : '') . number_format($expectancy, 2) . " per trade" . ($profitFactor !== null ? " with a profit factor of " . number_format($profitFactor, 2) : "") . ". At three months of data, this is a real measure of the strategy's edge, not a short-term fluctuation, it's the number to build a business case around.",
                "This quarter's risk-adjusted picture: expectancy of " . ($expectancy >= 0 ? '+' : '') . number_format($expectancy, 2) . " per trade" . ($profitFactor !== null ? ", profit factor {$profitFactor}" : "") . ". That's enough data to treat this as the strategy's true baseline going into next quarter.",
            ], $seed),
            default => self::pickVariant([
                "The full year's expectancy of " . ($expectancy >= 0 ? '+' : '') . number_format($expectancy, 2) . " per trade" . ($profitFactor !== null ? " and profit factor of " . number_format($profitFactor, 2) : "") . " is the clearest single measure of this year's trading edge. This number, more than any individual trade, should anchor next year's risk and position-sizing decisions.",
                "A year of data puts expectancy at " . ($expectancy >= 0 ? '+' : '') . number_format($expectancy, 2) . " per trade. That's mature enough to treat as a career baseline, future strategy changes should be judged against whether they move this number up or down.",
            ], $seed),
        };
    }

    // ────────────────────────────────────────────────────────────────
    // Drawdown Analysis (Quarterly+)
    // ────────────────────────────────────────────────────────────────
    public static function drawdown(string $period, ?float $maximumDrawdown, int $drawdownFrequency, int $totalTrades = 0): string
    {
        if ($maximumDrawdown === null) {
            return "Not enough closed-trade history yet this period to chart a drawdown curve.";
        }
        $seed = 'drawdown' . $period . round($maximumDrawdown, 2) . $drawdownFrequency;
        $periodNoun = periodNoun($period);

        // n-aware gate: a "drawdown profile" claim needs an actual equity
        // curve behind it, not one or two trades, same principle as the
        // functions above.
        if ($totalTrades === 1) {
            return "Only one trade has closed this {$periodNoun}, not enough of an equity curve yet to call this a drawdown pattern. The " . number_format(abs($maximumDrawdown), 2) . " figure reflects that single trade, not a trend.";
        }
        if ($totalTrades > 0 && $totalTrades < 10) {
            return "With {$totalTrades} trades this {$periodNoun}, the deepest pull-back reached " . number_format(abs($maximumDrawdown), 2) . ". Too small a sample yet to call this the strategy's real drawdown profile, watch how it develops as more trades close.";
        }

        return match ($period) {
            'quarterly' => self::pickVariant([
                "This quarter's peak drawdown reached " . number_format(abs($maximumDrawdown), 2) . ", with {$drawdownFrequency} stretch(es) beyond the $100 warning line. That's a real test of risk tolerance under this strategy, worth confirming it stayed inside what any funded account rules would allow.",
                "Drawdown depth this quarter hit " . number_format(abs($maximumDrawdown), 2) . " at its worst point. Whether that's acceptable depends entirely on account size and rules, but three months of data is enough to know this is the strategy's real drawdown profile, not a one-off dip.",
            ], $seed),
            default => self::pickVariant([ // annual
                "Over the full year, the deepest drawdown reached " . number_format(abs($maximumDrawdown), 2) . ", occurring {$drawdownFrequency} time(s). A year-long drawdown profile like this is what actually determines whether an account or challenge survives, it's the single most important risk number to carry into next year's account sizing decisions.",
                "The year's worst drawdown was " . number_format(abs($maximumDrawdown), 2) . ". Knowing this number cold, not just the wins, is what separates a trader who scales up safely from one who gets breached scaling up too fast.",
            ], $seed),
        };
    }

    // ────────────────────────────────────────────────────────────────
    // Professional Metrics, Sharpe/Sortino (Annual only)
    // ────────────────────────────────────────────────────────────────
    public static function professionalMetrics(?float $sharpeRatio, ?float $sortinoRatio, int $totalTrades = 0): string
    {
        if ($sharpeRatio === null && $sortinoRatio === null) {
            return "Not enough return history yet this year to compute risk-adjusted performance ratios.";
        }
        $parts = [];
        if ($sharpeRatio !== null) $parts[] = "a Sharpe ratio of " . number_format($sharpeRatio, 2);
        if ($sortinoRatio !== null) $parts[] = "a Sortino ratio of " . number_format($sortinoRatio, 2);
        $joined = implode(' and ', $parts);

        // Narrative audit §3.3: every sibling narrative function in this
        // file (behavioral()/session()/edge()/strategyPerformance()/
        // risk()/drawdown()) gates its language on trade count before
        // reaching for its strongest claim, this one didn't gate at
        // all, only on whether the ratios were computable. Since
        // Annual's hard trade-count lock was removed app-wide, a
        // handful of trades this year was enough to reach the app's
        // single strongest-worded claim ("the most rigorous single
        // verdict on this year's trading quality").
        if ($totalTrades === 1) {
            return "This year's only closed trade produced {$joined}. These ratios are built for a real distribution of returns, one trade can't yet support them meaningfully, so treat this as a placeholder, not a verdict.";
        }
        if ($totalTrades > 0 && $totalTrades < 10) {
            return "This year's {$totalTrades} trades produced {$joined}. These are the same risk-adjusted return measures used to evaluate professional fund performance, but they need a much larger sample than {$totalTrades} trades to mean much yet, worth tracking as the year's trade count grows, not acting on now.";
        }
        return "This year's trading produced {$joined}, these are the same risk-adjusted return measures used to evaluate professional fund performance, and they only become meaningful with a full year of returns behind them. This is the most rigorous single verdict on this year's trading quality.";
    }

    // ────────────────────────────────────────────────────────────────
    // Boundary Analytics, All Prop Firm Accounts
    // ────────────────────────────────────────────────────────────────
    public static function boundaryAnalytics(string $period, array $boundaryAnalytics): string
    {
        if (empty($boundaryAnalytics) || (int)($boundaryAnalytics['total_challenges'] ?? 0) === 0) {
            return "No challenge accounts to analyze yet.";
        }
        $avgBreach   = $boundaryAnalytics['avg_breach_payload'];
        $avgPass     = $boundaryAnalytics['avg_excess_at_pass'];
        $breachCount = (int)($boundaryAnalytics['breach_count'] ?? 0);
        $passCount   = (int)($boundaryAnalytics['pass_count'] ?? 0);

        // n-aware phrasing per side (breach average, pass average), each
        // is its own sample, so gate them independently rather than
        // assuming "an average exists" means "the average is meaningful."
        // Same principle as the functions above: n=1 gets an honest
        // single-data-point caveat, small-n gets a hedge, larger n gets
        // the existing confident framing unchanged.
        $breachPhrase = function () use ($avgBreach, $breachCount) {
            if ($avgBreach === null) return null;
            if ($breachCount === 1) return "one breach so far, which overshot the limit by " . number_format(abs($avgBreach), 2) . ", a single data point, not yet an average worth trusting";
            if ($breachCount < 5) return "{$breachCount} breaches averaging " . number_format(abs($avgBreach), 2) . " past the limit, still a small sample, worth watching rather than treating as the settled overshoot pattern";
            return "breaches averaging " . number_format(abs($avgBreach), 2) . " past the limit";
        };
        $passPhrase = function () use ($avgPass, $passCount) {
            if ($avgPass === null) return null;
            if ($passCount === 1) return "one pass so far, clearing the target by " . number_format($avgPass, 2) . ", a single data point, not yet an average worth trusting";
            if ($passCount < 5) return "{$passCount} passes clearing the target by " . number_format($avgPass, 2) . " on average, still a small sample";
            return "passes clearing the target by " . number_format($avgPass, 2) . " on average";
        };
        $breachText = $breachPhrase();
        $passText = $passPhrase();

        // Narrative audit §3.2: "the gap between breaching versus passing"
        // is a genuinely two-sided comparison, it needs a real sample on
        // BOTH sides, not just one. The old logic used OR here, so e.g.
        // 10 real breaches and 0 passes still asserted a "gap between
        // breaching versus passing," describing a comparison when only
        // one side had any data at all. Fixed once, shared by both the
        // quarterly and annual branches below (they had the identical
        // bug) rather than patched twice.
        $hasTwoSidedGap = ($breachCount >= 5 && $passCount >= 5);
        $gapClosingLine = function () use ($hasTwoSidedGap, $breachCount, $passCount) {
            if ($hasTwoSidedGap) {
                return " That gap between how far accounts overshoot on the way to breaching versus passing is exactly the kind of pattern that should shape position sizing near account limits.";
            }
            if ($breachCount >= 5 && $passCount < 5) {
                return " There isn't enough pass history yet to compare the two margins, once more accounts pass, this section will show how they line up.";
            }
            if ($passCount >= 5 && $breachCount < 5) {
                return " There isn't enough breach history yet to compare the two margins, that's not a bad thing on its own, and once (if ever) more breaches happen, this section will show how they line up.";
            }
            return " Keep an eye on this as more challenge accounts resolve, the sample on at least one side is still small.";
        };

        return match ($period) {
            'weekly', 'monthly' => ($breachText !== null)
                ? "Across your challenge accounts, " . $breachText . "."
                : "No breaches recorded yet, nothing to average, which is exactly the outcome you want.",
            'quarterly' => ($breachText !== null || $passText !== null)
                ? "This quarter's boundary picture: " . ($breachText ?? "no breaches this quarter") . ($passText !== null ? ", while " . $passText : "") . "." . $gapClosingLine()
                : "Not enough challenge account history this quarter to establish a boundary pattern.",
            default => ($breachText !== null || $passText !== null)
                ? "The full year's challenge account record shows " . ($breachText ?? "no breaches") . ($passText !== null ? " against " . $passText : "") . "." . ($hasTwoSidedGap ? " This is a career-level read on risk discipline near account boundaries, the single clearest number to improve for a higher pass rate next year." : " Still a small sample on at least one side at the year level, worth tracking as more accounts resolve before treating this as a settled career pattern.")
                : "Not enough challenge account history this year to establish a boundary pattern.",
        };
    }

    // ────────────────────────────────────────────────────────────────
    // Portfolio Analytics, Prop Firm / Live Account Summary
    // ────────────────────────────────────────────────────────────────
    public static function portfolio(string $period, string $accountLabel, int $totalAccounts, float $combinedPnl, int $breachedOrLostCount): string
    {
        if ($totalAccounts === 0) return "No {$accountLabel} accounts yet.";
        $seed = 'portfolio' . $period . $accountLabel . round($combinedPnl, 2);
        // Names the subject plainly ("your {accountLabel} portfolio") rather
        // than describing it abstractly (the old "the {N}-account {type}
        // portfolio" phrasing read like a system reporting on itself instead
        // of a person talking to the trader about their own accounts).
        return match ($period) {
            'weekly' => "This week, your {$accountLabel} portfolio ({$totalAccounts} account(s)) combined for " . ($combinedPnl >= 0 ? '+' : '') . number_format($combinedPnl, 2) . ". Too early in the week to draw portfolio-level conclusions, check back as more trades close.",
            'monthly' => self::pickVariant([
                "This month, your {$accountLabel} portfolio ({$totalAccounts} account(s)) combined for " . ($combinedPnl >= 0 ? '+' : '') . number_format($combinedPnl, 2) . ". Month-over-month trend comparison isn't shown yet in Reports, once more monthly history builds up, this section will show whether the portfolio is trending up or down.",
            ], $seed),
            'quarterly' => self::pickVariant([
                "This quarter, your {$accountLabel} portfolio ({$totalAccounts} account(s)) combined for " . ($combinedPnl >= 0 ? '+' : '') . number_format($combinedPnl, 2) . "" . ($breachedOrLostCount > 0 ? ", with {$breachedOrLostCount} account(s) breached or closed at a loss, worth reviewing whether those share a common cause." : ", with no accounts breached or closed at a loss this quarter.") . " At this scale, portfolio-level decisions (which accounts to keep funding, which to close) start to matter more than any single trade.",
            ], $seed),
            default => self::pickVariant([
                "This year, your {$accountLabel} portfolio ({$totalAccounts} account(s)) combined for " . ($combinedPnl >= 0 ? '+' : '') . number_format($combinedPnl, 2) . "" . ($breachedOrLostCount > 0 ? ", with {$breachedOrLostCount} account(s) breached or closed at a loss over the year" : "") . ". This is the career-level scorecard for account management decisions, which accounts to scale, which to walk away from, going into next year.",
            ], $seed),
        };
    }

    // ────────────────────────────────────────────────────────────────
    // Lifecycle Journey (single selected account)
    // ────────────────────────────────────────────────────────────────
    public static function lifecycleJourney(string $period, array $lifecycleJourney): string
    {
        $lifetimeReturn = (float)($lifecycleJourney['summary']['lifetime_return_pct'] ?? 0);
        $phaseCount = count($lifecycleJourney['phases'] ?? []);
        if ($phaseCount === 0) return "No lifecycle phases recorded yet for this account.";
        return match ($period) {
            'weekly', 'monthly' => "This account has moved through {$phaseCount} phase(s) so far, for a lifetime return of " . number_format($lifetimeReturn, 2) . "%. Still early to judge the full journey, keep tracking phase-to-phase.",
            'quarterly' => "Across {$phaseCount} phase(s), this account's lifetime return sits at " . number_format($lifetimeReturn, 2) . "%. A quarter's view of the full journey, not just the current phase, is what shows whether this account is actually progressing or stalling.",
            default => "This account's full lifecycle spans {$phaseCount} phase(s) with a lifetime return of " . number_format($lifetimeReturn, 2) . "%. A year-long view of one account's complete journey, every phase, every setback, is the most concrete record of how this account's story actually played out.",
        };
    }

    // ────────────────────────────────────────────────────────────────
    // Cross-Strategy / Analytics Across Strategies
    // ────────────────────────────────────────────────────────────────
    public static function crossStrategy(string $period, array $crossStrategy): string
    {
        // Found live while verifying the §3.1 fix below: reports.php
        // deliberately pads this array with a user's *untraded* active
        // strategies (0 trades, `$untradedStrategies` merged in purely
        // so the Strategy Comparison TABLE can show what's available but
        // unused). This narrative describes trading behavior, not the
        // strategy inventory, so a 0-trade row must never count toward
        // "how many strategies were compared" or be eligible as the
        // "top" strategy, on a real account, this padding alone made
        // "Comparing all 4 strategies traded this year" render true for
        // the table but false for the sentence (only 1 of the 4 actually
        // had a trade), which is the exact §3.1 bug via a second route.
        $traded = array_values(array_filter($crossStrategy, fn($s) => (int)($s['trades'] ?? 0) > 0));
        if (empty($traded)) return "No active strategies with trades this period to compare.";
        $ranked = $traded; usort($ranked, fn($a, $b) => (float)$b['total_pnl'] <=> (float)$a['total_pnl']);
        $top = $ranked[0];
        $topTrades = isset($top['trades']) ? (int)$top['trades'] : null;
        $count = count($traded);
        $periodNoun = periodNoun($period);

        // n-aware gate: a strategy "leading the portfolio" on one or two
        // trades isn't a head-to-head result yet, same principle as
        // strategyPerformance() above (this section is the portfolio-wide
        // sibling of that per-strategy view).
        if ($topTrades === 1) {
            $strategiesWereActive = $count === 1 ? "1 strategy was active" : "{$count} strategies were active";
            return "{$strategiesWereActive} this {$periodNoun}, but {$top['name']} has only one closed trade so far. One trade can't lead a comparison yet, nothing to conclude between strategies until more trades close.";
        }
        if ($topTrades !== null && $topTrades < 10) {
            $strategiesWereActive = $count === 1 ? "1 strategy was active" : "{$count} strategies were active";
            return "{$strategiesWereActive} this {$periodNoun}; {$top['name']} is ahead on {$topTrades} trades. Still too small a sample to call this a real head-to-head result, worth watching as more trades come in.";
        }

        // Narrative audit §3.1: this function's whole premise is a
        // cross-strategy comparison. With $count === 1 there is only one
        // strategy in the portfolio this period, "led by"/"came out on
        // top" describes a competition that never happened (and reads as
        // grammatically broken: "1 strategies were active... led by X").
        if ($count === 1) {
            return "Only one strategy ({$top['name']}) was active this {$periodNoun}, with {$topTrades} trades closed. There's nothing to compare it against yet, trading a second strategy alongside it is what would make a real cross-strategy comparison possible.";
        }

        return match ($period) {
            'weekly', 'monthly' => "{$count} strategies were active this {$periodNoun}, led by {$top['name']}. Worth watching whether that lead holds as more data comes in.",
            'quarterly' => "Across {$count} active strategies this quarter, {$top['name']} led the portfolio. That's a strong enough signal at this scale to start shifting more trading time or capital toward it.",
            default => "Comparing all {$count} strategies traded this year, {$top['name']} came out on top. A full year of head-to-head strategy comparison is exactly the evidence needed to decide which strategy anchors next year's plan.",
        };
    }
}
