<?php
/**
 * StrategyMilestoneReview — the 50-trade Strategy Review card.
 *
 * A strategy-level, milestone-based assessment ("what is happening with
 * the edge I am actually trading"), deliberately separate from Reports'
 * existing Weekly/Monthly/Quarterly/Annual sections ("how did I perform
 * during this period"). Appears once a strategy reaches 50 completed
 * trades and updates at every subsequent 50-trade milestone (50, 100,
 * 150, ...) — never scoped to a reporting period, never scoped to which
 * account/account-type tab is active. Shown on BOTH the Validation
 * Tracker (public/journal-validation.php, while a strategy is still
 * is_validation=1) and Reports (public/reports.php, once it has
 * graduated into the main workflow) — the same computation and the same
 * card, so a trader never sees a different verdict on the two pages.
 *
 * Reuses, never re-derives:
 *   - AnalyticsEngine::getValidationStrategyMetrics() for every raw
 *     number (win rate, expectancy, profit factor, avg R, rule
 *     adherence) — the same canonical calculation layer every other page
 *     already reads from.
 *   - ValidationService::evaluateEdge() for the base
 *     positive-expectancy-and-profit-factor signal — the exact same
 *     classification the Validation Tracker's own graduation CTA already
 *     gates on, so "does this look like a real edge" can never disagree
 *     between the two pages.
 *   - trades.trade_classification (sp_classify_trade's own real output —
 *     revenge_trade/tilt_warning/loss_high_risk/etc.) for behavioural
 *     evidence — never a second, independently-invented behaviour
 *     detector.
 *   - The strategy's own declared strategy_versions fields
 *     (backtested_win_rate, backtested_avg_rr, max_trades_per_day,
 *     session_preference) for the main-workflow "declared edge vs.
 *     actual results" comparison — never a fabricated target.
 *
 * This class adds exactly one new judgment layer on top of those:
 * classifying the milestone into one of four honest tiers (encouraging /
 * inconclusive / concerning / inconsistent-with-declared-edge) and
 * turning the numbers into a short, dynamically-generated narrative.
 * Never claims 50 (or 100, or 150) trades is proof either way — every
 * narrative explicitly names the tier's own real uncertainty.
 */
require_once __DIR__ . '/../api/analytics/engine.php';
require_once __DIR__ . '/ValidationService.php';

class StrategyMilestoneReview
{
    const MILESTONE_STEP = 50;

    // Real, evidence-only thresholds — each one gated on an already-real,
    // already-computed signal (trade_classification rates, a declared
    // strategy_versions field, or a plain coefficient-of-variation on
    // risk_pct). None of these invent a new metric; they only decide when
    // an existing one is worth mentioning to the trader.
    const REVENGE_RATE_FLAG = 0.08;      // >8% of trades revenge_trade
    const TILT_RATE_FLAG = 0.10;         // >10% of trades tilt_warning
    const OVERRISK_LOSS_RATE_FLAG = 0.10; // >10% of trades loss_high_risk
    const RISK_CV_FLAG = 0.55;           // coefficient of variation on risk_pct
    const SESSION_ADHERENCE_FLAG_PCT = 60.0; // below this % = flagged
    const OVERTRADING_RATIO_FLAG = 1.3;  // actual/day vs. declared max, ratio
    const POOR_RR_AVG_R_FLAG = 0.5;      // avg realized R below this
    const WIN_RATE_GAP_FLAG_PP = 15.0;   // percentage points below declared
    const RR_GAP_FLAG_RATIO = 0.4;       // avg R below this fraction of declared RR

    /**
     * Whichever strategies (of the given list, each
     * ['id','name','current_version_id']) have reached >=50 completed
     * trades. $isValidationScope: true when called from the Validation
     * Tracker (strategy is still is_validation=1); false from Reports
     * (strategy has graduated, is_validation=0 — reads via
     * getValidationStrategyMetrics()'s 'exclude' scope, same as
     * strategy.php's own post-graduation prefill).
     */
    public static function buildForStrategies($userId, $db, array $strategies, string $displayName, bool $isValidationScope): array
    {
        $reviews = [];
        foreach ($strategies as $strategy) {
            if (empty($strategy['current_version_id'])) continue;
            $review = self::build($userId, $db, $strategy, $displayName, $isValidationScope);
            if ($review !== null) $reviews[] = $review;
        }
        return $reviews;
    }

    /**
     * One strategy's review, or null if it hasn't reached 50 trades yet.
     */
    public static function build($userId, $db, array $strategy, string $displayName, bool $isValidationScope): ?array
    {
        $versionId = $strategy['current_version_id'];
        $metrics = AnalyticsEngine::getValidationStrategyMetrics($userId, $versionId, null, null, $isValidationScope ? 'only' : 'exclude');
        $tradeCount = (int)($metrics['total_trades'] ?? 0);
        if ($tradeCount < self::MILESTONE_STEP) return null;

        $milestone = intdiv($tradeCount, self::MILESTONE_STEP) * self::MILESTONE_STEP;
        $edgeEval = ValidationService::evaluateEdge($metrics);

        // Origin: did this strategy ever pass through the Validation
        // Workflow? True unconditionally while still is_validation=1
        // (the Tracker case); for a graduated (Reports) strategy, only
        // real provenance counts — never guessed from anything else.
        $graduationRecord = $isValidationScope ? null : ValidationService::findGraduationRecordForStrategy($userId, $strategy['id'], $db);
        $isValidationOrigin = $isValidationScope || $graduationRecord !== null;

        // strategy_versions' own declared fields — read once, here, so
        // both the declared-edge comparison and the evidence gatherer
        // (session/overtrading checks) share one fetch.
        $versionRow = $db->raw(
            "SELECT backtested_win_rate, backtested_avg_rr, max_risk_per_trade_pct, session_preference, max_trades_per_day, instruments_traded
             FROM strategy_versions WHERE id = :vid AND user_id = :uid LIMIT 1",
            [':vid' => $versionId, ':uid' => $userId]
        );
        $versionRow = $versionRow[0] ?? [];

        // A real, documented data-quality caveat (CLAUDE.md Known
        // Gotchas): strategy-validation.php's own wizard writes TIMEFRAMES
        // into session_preference, not real trading sessions — so a
        // session-adherence check against that column would be checking
        // the wrong thing for any strategy that ever passed through that
        // wizard. Session adherence is only ever evaluated for a
        // genuinely main-workflow-declared strategy.
        $sessionPreferenceReliable = !$isValidationOrigin;

        $declaredEdge = null;
        if (!$isValidationOrigin) {
            $declaredWinRate = $versionRow['backtested_win_rate'] !== null ? (float)$versionRow['backtested_win_rate'] : null;
            $declaredAvgRr = $versionRow['backtested_avg_rr'] !== null ? (float)$versionRow['backtested_avg_rr'] : null;
            if ($declaredWinRate !== null || $declaredAvgRr !== null) {
                $declaredEdge = ['win_rate' => $declaredWinRate, 'avg_rr' => $declaredAvgRr];
            }
        }

        $evidence = self::gatherBehavioralEvidence($userId, $db, $versionId, $isValidationScope, $versionRow, $sessionPreferenceReliable);

        $tier = self::classify($metrics, $edgeEval, $declaredEdge, $evidence);

        $narrative = self::buildNarrative($displayName, $isValidationOrigin, $tier, $metrics, $edgeEval, $evidence, $declaredEdge, $milestone, $tradeCount, $strategy['name']);

        return [
            'strategy_id' => $strategy['id'],
            'strategy_name' => $strategy['name'],
            'version_id' => $versionId,
            'trade_count' => $tradeCount,
            'milestone' => $milestone,
            'is_validation_origin' => $isValidationOrigin,
            'tier' => $tier,
            'metrics' => $metrics,
            'edge_eval' => $edgeEval,
            'evidence' => $evidence,
            'declared_edge' => $declaredEdge,
            'narrative' => $narrative,
        ];
    }

    /**
     * Real, bounded evidence set — every item independently computed
     * from already-stored columns, every item optional (only present
     * in the return array when it actually clears its own threshold).
     * Nothing here is fabricated; an evidence item simply doesn't appear
     * when the underlying data doesn't support it.
     */
    private static function gatherBehavioralEvidence($userId, $db, string $versionId, bool $isValidationScope, array $versionRow, bool $sessionPreferenceReliable): array
    {
        $scopeSql = validationScopeSql($isValidationScope ? 'validation' : 'main', 't');

        $rows = $db->raw(
            "SELECT
                COUNT(*) AS n,
                SUM(CASE WHEN t.trade_classification = 'revenge_trade' THEN 1 ELSE 0 END) AS revenge_n,
                SUM(CASE WHEN t.trade_classification = 'tilt_warning' THEN 1 ELSE 0 END) AS tilt_n,
                SUM(CASE WHEN t.trade_classification = 'loss_high_risk' THEN 1 ELSE 0 END) AS overrisk_loss_n,
                AVG(NULLIF(t.risk_pct, 0)) AS avg_risk_pct,
                STDDEV_POP(NULLIF(t.risk_pct, 0)) AS sd_risk_pct,
                MIN(t.captured_at) AS first_captured,
                MAX(t.captured_at) AS last_captured
             FROM trades t
             WHERE t.user_id = :uid AND t.strategy_version_id = :vid
               AND t.status = 'closed' AND t.deleted_at IS NULL AND {$scopeSql}",
            [':uid' => $userId, ':vid' => $versionId]
        );
        $row = $rows[0] ?? [];
        $n = (int)($row['n'] ?? 0);

        $evidence = [];
        if ($n > 0) {
            $revengeRate = (int)($row['revenge_n'] ?? 0) / $n;
            $tiltRate = (int)($row['tilt_n'] ?? 0) / $n;
            $overriskLossRate = (int)($row['overrisk_loss_n'] ?? 0) / $n;

            if ($revengeRate >= self::REVENGE_RATE_FLAG) {
                $evidence['revenge_trading'] = ['rate' => round($revengeRate * 100, 1), 'count' => (int)$row['revenge_n']];
            }
            if ($tiltRate >= self::TILT_RATE_FLAG) {
                $evidence['tilt_trading'] = ['rate' => round($tiltRate * 100, 1), 'count' => (int)$row['tilt_n']];
            }
            if ($overriskLossRate >= self::OVERRISK_LOSS_RATE_FLAG) {
                $evidence['oversized_risk_losses'] = ['rate' => round($overriskLossRate * 100, 1), 'count' => (int)$row['overrisk_loss_n']];
            }

            $avgRisk = $row['avg_risk_pct'] !== null ? (float)$row['avg_risk_pct'] : null;
            $sdRisk = $row['sd_risk_pct'] !== null ? (float)$row['sd_risk_pct'] : null;
            if ($avgRisk !== null && $avgRisk > 0 && $sdRisk !== null) {
                $cv = $sdRisk / $avgRisk;
                if ($cv >= self::RISK_CV_FLAG) {
                    $evidence['inconsistent_risk'] = ['cv' => round($cv, 2), 'avg_risk_pct' => round($avgRisk, 2)];
                }
            }

            // Overtrading — only checkable when the strategy itself
            // declares a real daily cap (strategy_versions.max_trades_per_day).
            $maxPerDay = $versionRow['max_trades_per_day'] !== null ? (int)$versionRow['max_trades_per_day'] : null;
            if ($maxPerDay !== null && $maxPerDay > 0 && !empty($row['first_captured']) && !empty($row['last_captured'])) {
                $days = max(1, (strtotime($row['last_captured']) - strtotime($row['first_captured'])) / 86400);
                $actualPerDay = $n / $days;
                if ($actualPerDay >= $maxPerDay * self::OVERTRADING_RATIO_FLAG) {
                    $evidence['overtrading'] = ['actual_per_day' => round($actualPerDay, 1), 'declared_max_per_day' => $maxPerDay];
                }
            }

            // Session adherence — only for a genuinely main-workflow
            // declared strategy (see build()'s own comment on why).
            if ($sessionPreferenceReliable) {
                $declaredSessions = json_decode($versionRow['session_preference'] ?? '[]', true) ?: [];
                if (!empty($declaredSessions)) {
                    $sessionRows = $db->raw(
                        "SELECT t.session, COUNT(*) AS c FROM trades t
                         WHERE t.user_id = :uid AND t.strategy_version_id = :vid
                           AND t.status = 'closed' AND t.deleted_at IS NULL AND {$scopeSql}
                         GROUP BY t.session",
                        [':uid' => $userId, ':vid' => $versionId]
                    );
                    $declaredLower = array_map('strtolower', $declaredSessions);
                    $inSession = 0;
                    $totalWithSession = 0;
                    foreach ($sessionRows as $sr) {
                        $c = (int)$sr['c'];
                        if (empty($sr['session'])) continue;
                        $totalWithSession += $c;
                        if (in_array(strtolower($sr['session']), $declaredLower, true)) $inSession += $c;
                    }
                    if ($totalWithSession > 0) {
                        $sessionAdherencePct = round(($inSession / $totalWithSession) * 100, 1);
                        if ($sessionAdherencePct < self::SESSION_ADHERENCE_FLAG_PCT) {
                            $evidence['session_adherence'] = ['pct' => $sessionAdherencePct, 'declared' => $declaredSessions];
                        }
                    }
                }
            }
        }

        return $evidence;
    }

    /**
     * Four honest tiers, never a binary proof/disproof. Priority order:
     * a declared-edge mismatch (main-workflow strategies only) is
     * reported as its own tier regardless of raw profitability, since
     * "profitable but not what you said you'd do" is a genuinely
     * different, more specific finding than a plain concerning result.
     */
    private static function classify(array $metrics, array $edgeEval, ?array $declaredEdge, array $evidence): string
    {
        if ($declaredEdge !== null) {
            $actualWinRatePct = ($metrics['win_rate'] ?? 0) * 100;
            if ($declaredEdge['win_rate'] !== null) {
                $gap = $actualWinRatePct - $declaredEdge['win_rate'];
                if ($gap <= -self::WIN_RATE_GAP_FLAG_PP) return 'inconsistent';
            }
            if ($declaredEdge['avg_rr'] !== null && $declaredEdge['avg_rr'] > 0 && $metrics['avg_r_multiple'] !== null) {
                if ($metrics['avg_r_multiple'] < $declaredEdge['avg_rr'] * self::RR_GAP_FLAG_RATIO) return 'inconsistent';
            }
        }

        $expectancy = (float)($metrics['expectancy'] ?? 0);
        $profitFactor = $metrics['profit_factor'] ?? null;
        $behaviorClean = !isset($evidence['revenge_trading']) && !isset($evidence['tilt_trading']);

        // Severe process failure overrides a merely-positive dollar
        // expectancy — a real bug found live: a strategy at 14% win rate,
        // 40% revenge-trading, and 8% checklist adherence still read as
        // +2.70 expectancy (a few outsized wins carrying an otherwise
        // broken process) and was classifying as "Inconclusive" instead
        // of "Concerning". A positive number produced by an undisciplined
        // process is not encouraging news, and a trader reading this card
        // should never see it colored as if it were. Any one of these
        // clears a real severity bar (well above the ordinary evidence-
        // flag thresholds above, which only need to be worth MENTIONING,
        // not worth overriding the whole verdict) before it overrides.
        $winRate = (float)($metrics['win_rate'] ?? 0);
        $ruleAdherence = $metrics['rule_adherence_pct'] ?? null;
        $severeRevenge = isset($evidence['revenge_trading']) && $evidence['revenge_trading']['rate'] >= 25.0;
        $severeTilt = isset($evidence['tilt_trading']) && $evidence['tilt_trading']['rate'] >= 25.0;
        $severeOverrisk = isset($evidence['oversized_risk_losses']) && $evidence['oversized_risk_losses']['rate'] >= 25.0;
        $severeChecklistNeglect = $ruleAdherence !== null && $ruleAdherence < 25.0;
        $severeLowWinRate = $winRate < 0.20;
        if ($severeRevenge || $severeTilt || $severeOverrisk || $severeChecklistNeglect || $severeLowWinRate) {
            return 'concerning';
        }

        if ($expectancy <= 0 || ($profitFactor !== null && $profitFactor < 0.9)) {
            return 'concerning';
        }
        if ($expectancy > 0 && ($profitFactor === null || $profitFactor >= 1.3) && $behaviorClean) {
            return 'encouraging';
        }
        return 'inconclusive';
    }

    /**
     * Plain-language paragraphs. Two real templates (validation-origin,
     * matching the "This strategy came from your validation workflow..."
     * shape; main-workflow-origin, matching the "When you defined this
     * strategy, you identified..." declared-vs-actual shape) — chosen by
     * $isValidationOrigin, never guessed from anything else.
     */
    private static function buildNarrative(string $displayName, bool $isValidationOrigin, string $tier, array $metrics, array $edgeEval, array $evidence, ?array $declaredEdge, int $milestone, int $tradeCount, string $strategyName): array
    {
        $p = [];
        $winRatePct = round(($metrics['win_rate'] ?? 0) * 100, 1);
        $expectancy = (float)($metrics['expectancy'] ?? 0);
        $pf = $metrics['profit_factor'] ?? null;
        $sampleNote = $tradeCount === $milestone
            ? "your first {$milestone} trades"
            : "{$tradeCount} trades so far (your last milestone was {$milestone})";

        if ($isValidationOrigin) {
            $p[] = "Hi {$displayName},";

            $tierPhrase = match ($tier) {
                'encouraging' => "This strategy came from your Validation Workflow, where you tested it over {$sampleNote}. So far, the results are encouraging: your data shows a real, positive edge (expectancy of " . self::fmtSigned($expectancy) . " per trade" . ($pf !== null ? ", profit factor of " . number_format($pf, 2) : "") . ").",
                'inconclusive' => "This strategy came from your Validation Workflow, where you tested it over {$sampleNote}. So far, the results remain inconclusive: " . self::inconclusiveReason($expectancy, $pf, $evidence) . ".",
                'concerning' => "This strategy came from your Validation Workflow, where you tested it over {$sampleNote}. So far, the results have not demonstrated a strong enough edge: " . self::concerningReason($expectancy, $pf) . ".",
                'inconsistent' => "This strategy came from your Validation Workflow, where you tested it over {$sampleNote}. Its win rate and expectancy have moved enough that the results are genuinely mixed at this point.",
                default => "This strategy came from your Validation Workflow, where you tested it over {$sampleNote}.",
            };
            $p[] = $tierPhrase;
            $p[] = "Your current sample is still limited, so this should not be presented as a final verdict.";

            $behaviorSentence = self::behaviorSentence($evidence);
            if ($behaviorSentence !== null) {
                $p[] = "Your trading behaviour also shows " . $behaviorSentence . ".";
            } else {
                $p[] = "Your trading behaviour has stayed clean across these trades, with no meaningful revenge-trading, tilt, or oversized-risk pattern detected.";
            }

            if ($tier === 'encouraging') {
                $p[] = "Keep doing what's working: your risk discipline and execution have supported this result so far. Let the sample keep growing before treating it as fully proven.";
            } else {
                $improvement = self::improvementList($evidence);
                $improvementPhrase = $improvement !== '' ? " while deliberately working on {$improvement}" : '';
                $p[] = "If you believe the edge can be improved, you can continue trading and validating it{$improvementPhrase}. Otherwise, consider reviewing the strategy, seeking mentorship, or moving on to a stronger edge.";
            }
        } else {
            $p[] = "Hello {$displayName},";

            if ($declaredEdge !== null && $declaredEdge['win_rate'] !== null) {
                $rrPart = $declaredEdge['avg_rr'] !== null ? " with an intended reward:risk of 1:" . number_format($declaredEdge['avg_rr'], 2) : '';
                $p[] = "When you defined \"{$strategyName}\", you identified an expected win rate of " . number_format($declaredEdge['win_rate'], 1) . "%{$rrPart}. After {$sampleNote}, your actual win rate is {$winRatePct}%" . ($metrics['avg_r_multiple'] !== null ? " and your average realized reward is " . number_format($metrics['avg_r_multiple'], 2) . "R" : "") . ".";
            } else {
                $p[] = "\"{$strategyName}\" was created directly in your main workflow, without a declared win rate or reward:risk target on record. After {$sampleNote}, your actual win rate is {$winRatePct}%" . (self::fmtExpectancyClause($expectancy)) . ".";
            }

            $doesReflect = $tier !== 'inconsistent' && $tier !== 'concerning';
            $specificEvidence = self::mainWorkflowEvidenceSentence($tier, $evidence, $declaredEdge, $metrics);
            $p[] = "Your current trading behaviour " . ($doesReflect ? "reflects the edge you originally defined" : "does not fully reflect the edge you originally defined") . ". {$specificEvidence}";

            $focus = self::improvementList($evidence, true);
            $focusPhrase = $focus !== '' ? $focus : ($tier === 'encouraging' ? "keeping your risk sizing and session discipline exactly as they've been" : "reviewing your entries against your own declared setup rules before every trade");
            $p[] = "As you continue trading, focus on {$focusPhrase} so your actual execution becomes more consistent with the edge you originally defined.";
        }

        return $p;
    }

    private static function fmtSigned(float $v, int $decimals = 2): string
    {
        return ($v >= 0 ? '+' : '') . number_format($v, $decimals);
    }

    private static function fmtExpectancyClause(float $expectancy): string
    {
        return ", with an expectancy of " . self::fmtSigned($expectancy) . " per trade";
    }

    private static function inconclusiveReason(float $expectancy, $pf, array $evidence): string
    {
        if ($expectancy > 0 && $pf !== null && $pf < 1.3) {
            return "a small positive edge (profit factor " . number_format($pf, 2) . ") that hasn't yet cleared a strong enough margin to call proven";
        }
        if (!empty($evidence)) {
            return "the underlying numbers look reasonable, but some behavioural inconsistency (see below) makes the result harder to trust yet";
        }
        return "the numbers are close to breakeven, not clearly one way or the other";
    }

    private static function concerningReason(float $expectancy, $pf): string
    {
        if ($expectancy <= 0) {
            return "expectancy is currently " . self::fmtSigned($expectancy) . " per trade";
        }
        return "profit factor is currently " . number_format($pf, 2) . ", below the level needed to call this a reliable edge";
    }

    private static function behaviorSentence(array $evidence): ?string
    {
        $parts = [];
        if (isset($evidence['revenge_trading'])) $parts[] = "revenge trading on {$evidence['revenge_trading']['rate']}% of trades";
        if (isset($evidence['tilt_trading'])) $parts[] = "tilt-driven trades on {$evidence['tilt_trading']['rate']}% of trades";
        if (isset($evidence['oversized_risk_losses'])) $parts[] = "oversized risk on {$evidence['oversized_risk_losses']['rate']}% of your losing trades";
        if (isset($evidence['inconsistent_risk'])) $parts[] = "inconsistent position sizing from trade to trade";
        if (isset($evidence['overtrading'])) $parts[] = "trading more often per day than you declared ({$evidence['overtrading']['actual_per_day']} vs. a declared max of {$evidence['overtrading']['declared_max_per_day']})";
        if (isset($evidence['session_adherence'])) $parts[] = "only {$evidence['session_adherence']['pct']}% of trades taken inside your own declared sessions";
        if (empty($parts)) return null;
        if (count($parts) === 1) return $parts[0];
        $last = array_pop($parts);
        return implode(', ', $parts) . ', and ' . $last;
    }

    private static function improvementList(array $evidence, bool $asPlainList = false): string
    {
        $items = [];
        if (isset($evidence['revenge_trading'])) $items[] = 'avoiding revenge trades after a loss';
        if (isset($evidence['tilt_trading'])) $items[] = 'stepping away when you notice tilt building';
        if (isset($evidence['oversized_risk_losses'])) $items[] = 'keeping risk sized to your plan, especially on losing trades';
        if (isset($evidence['inconsistent_risk'])) $items[] = 'sizing positions more consistently trade to trade';
        if (isset($evidence['overtrading'])) $items[] = 'staying inside your own declared daily trade limit';
        if (isset($evidence['session_adherence'])) $items[] = 'trading only inside your declared sessions';
        if (empty($items)) return '';
        if (count($items) === 1) return $items[0];
        $last = array_pop($items);
        return implode(', ', $items) . ' and ' . $last;
    }

    private static function mainWorkflowEvidenceSentence(string $tier, array $evidence, ?array $declaredEdge, array $metrics): string
    {
        if ($tier === 'inconsistent' && $declaredEdge !== null) {
            $actualWinRatePct = round(($metrics['win_rate'] ?? 0) * 100, 1);
            if ($declaredEdge['win_rate'] !== null && ($actualWinRatePct - $declaredEdge['win_rate']) <= -self::WIN_RATE_GAP_FLAG_PP) {
                return "In particular, your actual win rate ({$actualWinRatePct}%) is meaningfully below the " . number_format($declaredEdge['win_rate'], 1) . "% you declared when you built this strategy.";
            }
            if ($declaredEdge['avg_rr'] !== null && $metrics['avg_r_multiple'] !== null) {
                return "In particular, your average realized reward (" . number_format($metrics['avg_r_multiple'], 2) . "R) is well short of the 1:" . number_format($declaredEdge['avg_rr'], 2) . " you originally targeted.";
            }
        }
        $behaviorSentence = self::behaviorSentence($evidence);
        if ($behaviorSentence !== null) {
            return "In particular, the data shows " . $behaviorSentence . ".";
        }
        if ($tier === 'encouraging') {
            return "No significant behavioural red flags were found in this data. Your discipline has supported the result so far.";
        }
        return "No single behavioural pattern stands out yet, though the sample is still on the smaller side.";
    }

    /**
     * Card rendering — one shared implementation for both Reports and the
     * Validation Tracker, so they can never visually or textually drift.
     * $context: 'reports' | 'tracker' — only changes the eyebrow label and
     * an optional "View full strategy" link target, nothing about the
     * actual assessment.
     */
    public static function renderCard(array $review, string $context = 'reports'): void
    {
        $tierMeta = [
            'encouraging' => ['label' => 'Encouraging', 'color' => 'var(--positive)', 'icon' => '✅'],
            'inconclusive' => ['label' => 'Inconclusive', 'color' => 'var(--foreground-dim)', 'icon' => '🔎'],
            'concerning' => ['label' => 'Concerning', 'color' => 'var(--negative)', 'icon' => '⚠️'],
            'inconsistent' => ['label' => 'Inconsistent With Declared Edge', 'color' => 'var(--warning)', 'icon' => '⚠️'],
        ];
        $meta = $tierMeta[$review['tier']] ?? $tierMeta['inconclusive'];
        $m = $review['metrics'];

        // The card's accent should still lean toward red or green even on
        // an "Inconclusive" tier, real money is either up or down for this
        // strategy so far, and a flat neutral gray regardless of that sign
        // read as if the card ignored the actual number. Encouraging,
        // Concerning, and Inconsistent already have a clear color of their
        // own; only Inconclusive falls back to the sign of expectancy
        // (the plainest positive/negative signal already computed above),
        // staying truly neutral only in the rare exact-zero case.
        if ($review['tier'] === 'inconclusive') {
            $expectancySign = (float)($m['expectancy'] ?? 0);
            if ($expectancySign > 0) {
                $meta = ['label' => 'Inconclusive', 'color' => 'var(--positive)', 'icon' => '🔎'];
            } elseif ($expectancySign < 0) {
                $meta = ['label' => 'Inconclusive', 'color' => 'var(--negative)', 'icon' => '🔎'];
            }
        }
        ?>
        <div class="smr-card" style="background:color-mix(in srgb,<?= $meta['color'] ?> 5%,var(--background-elevated));border:1px solid color-mix(in srgb,<?= $meta['color'] ?> 30%,transparent);border-left:3px solid <?= $meta['color'] ?>;border-radius:var(--radius-lg);padding:.75rem .9rem;margin-bottom:1rem;max-width:38rem">
            <div style="display:flex;align-items:center;justify-content:space-between;gap:.6rem;flex-wrap:wrap;margin-bottom:.35rem">
                <div style="font-size:.58rem;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--foreground-dim)">📐 Strategy Review · <?= (int)$review['milestone'] ?> Trades</div>
                <div style="font-size:.62rem;font-weight:700;color:<?= $meta['color'] ?>;background:color-mix(in srgb,<?= $meta['color'] ?> 14%,transparent);border:1px solid color-mix(in srgb,<?= $meta['color'] ?> 28%,transparent);border-radius:var(--radius-full);padding:.12rem .55rem"><?= $meta['icon'] ?> <?= htmlspecialchars($meta['label']) ?></div>
            </div>
            <div style="font-size:.82rem;font-weight:700;color:var(--foreground);margin-bottom:.4rem"><?= htmlspecialchars($review['strategy_name']) ?> <span style="font-weight:400;color:var(--foreground-dim);font-size:.66rem;font-family:var(--font-mono)">· <?= (int)$review['trade_count'] ?> trades total</span></div>

            <div style="font-size:.7rem;color:var(--foreground);line-height:1.5;margin-bottom:.5rem">
                <?php foreach ($review['narrative'] as $para): ?>
                <p style="margin-bottom:.4rem"><?= htmlspecialchars($para) ?></p>
                <?php endforeach; ?>
            </div>

            <div style="display:flex;flex-wrap:wrap;gap:.4rem .9rem;font-family:var(--font-mono);padding-top:.5rem;border-top:1px solid var(--border);font-size:.66rem">
                <span style="color:var(--foreground-dim)">Win Rate <b style="color:var(--foreground)"><?= round(($m['win_rate'] ?? 0) * 100, 1) ?>%</b></span>
                <span style="color:var(--foreground-dim)">Expectancy <b style="color:var(--foreground)"><?= self::fmtSigned((float)($m['expectancy'] ?? 0)) ?></b></span>
                <span style="color:var(--foreground-dim)">Profit Factor <b style="color:var(--foreground)"><?= $m['profit_factor'] !== null ? number_format($m['profit_factor'], 2) : '-' ?></b></span>
                <span style="color:var(--foreground-dim)">Avg R <b style="color:var(--foreground)"><?= $m['avg_r_multiple'] !== null ? number_format($m['avg_r_multiple'], 2) . 'R' : '-' ?></b></span>
                <span style="color:var(--foreground-dim)">Checklist <b style="color:var(--foreground)"><?= $m['rule_adherence_pct'] !== null ? $m['rule_adherence_pct'] . '%' : '-' ?></b></span>
            </div>

            <div style="font-size:.58rem;color:var(--foreground-dim);margin-top:.4rem">Not a final verdict. Updates again at the next 50-trade milestone.</div>
        </div>
        <?php
    }
}
