<?php

/**
 * Trader Profile Narrative — turns already-computed numbers/signals into
 * first-person, varied sentences. Never computes anything itself (no DB,
 * no math beyond formatting) — every fact it writes about was already
 * produced by TraderProfileBehaviorEngine / TraderProfileIdentity /
 * AnalyticsEngine / EdgeHealthEngine.
 *
 * Phrase-variety mechanism is journal.php's own established pattern,
 * generalized into one reusable helper (confirmed exact source:
 * public/journal.php's renderBreachNarrative()/renderPassNarrative()/
 * renderAccountLesson(), all built on the same
 * `crc32($seed . implode('', array_map('strlen', $variants))) % count($variants)`
 * idiom) — not a new invention.
 */
class TraderProfileNarrative
{
    /** The exact phrase-picker mechanism from journal.php, generalized. */
    public static function pick(array $variants, string $seed): string {
        $key = abs(crc32($seed . implode('', array_map('strlen', $variants)))) % count($variants);
        return $variants[$key];
    }

    // §6.12 fix (2026-08-10) — same seeded mechanism as pick() above, but
    // for a caller (nextTradeReminders()) that needs to return MULTIPLE
    // lines at once (Before Your Next Trade deliberately renders 2-4
    // reminder lines together, a real, established design — see that
    // function's own history), not select exactly one. A plain pick()
    // would have meant dropping lines the UI is built to show. This
    // rotates the array by a seeded offset instead — every line is still
    // returned, in cyclic order, but WHICH one leads (index 0) now
    // genuinely varies with the seed, the same real variety pick() gives
    // everywhere else, rather than the seed being threaded through and
    // silently ignored.
    public static function pickRotate(array $items, string $seed): array {
        $n = count($items);
        if ($n <= 1) return $items;
        $offset = abs(crc32($seed . implode('', array_map('strlen', $items)))) % $n;
        return array_merge(array_slice($items, $offset), array_slice($items, 0, $offset));
    }

    public static function fmtPct($v, $decimals = 1): string {
        return $v === null ? '-' : number_format((float)$v, $decimals) . '%';
    }

    public static function fmtNum($v, $decimals = 2): string {
        return $v === null ? '-' : number_format((float)$v, $decimals);
    }

    // ── Greeting ──────────────────────────────────────────────────────
    // A welcome, not a summary — the difference the user asked for is real:
    // "welcome back" opens with the trader, a report opens with the data.
    public static function greetingSubtext(array $identity, string $periodNoun, string $seed): string {
        if (empty($identity['ready'])) {
            return self::pick([
                "Good to see you back. You're still early in building the kind of track record this page reads from, and that's completely fine, it just means there's less to say with confidence yet.",
                "Welcome back. Not enough has closed yet this {$periodNoun} for us to say much for certain. Come back as your history grows and this will start to fill in.",
            ], $seed . 'greet');
        }
        $strengthLabel = strtolower($identity['strength']['label']);
        return self::pick([
            "Welcome back. You've been leaning on your {$strengthLabel} lately, and that's worth noticing before we look at anything else.",
            "Good to see you again. Your {$strengthLabel} has quietly been doing a lot of the work this {$periodNoun}. Let's take a look at what else is going on.",
        ], $seed . 'greet');
    }

    // ── Strength / Blind Spot / Holding Back cards ──────────────────────
    const CHARACTER_STRENGTH_COPY = [
        'patience' => ['headline' => 'You wait.', 'body' => 'You rarely chase trades or force entries. Your patience protects your account more than you probably realize.'],
        'discipline' => ['headline' => 'You follow through.', 'body' => 'When your plan says wait, you wait. When it says act, you act. That follow-through is showing up clearly in your recent trades.'],
        'consistency' => ['headline' => 'You repeat what works.', 'body' => "Your risk and your results land in a tight, predictable range. That kind of consistency is rarer than it sounds."],
        'emotional_control' => ['headline' => 'You stay level.', 'body' => "A loss doesn't seem to knock you off your plan the way it does for a lot of traders. That composure is doing real work."],
        'trust_in_strategy' => ['headline' => 'You trust your plan.', 'body' => "You're staying inside your own strategy's numbers more than most traders manage. That discipline is the foundation everything else gets built on."],
    ];

    public static function strengthCard(array $strength): array {
        $c = self::CHARACTER_STRENGTH_COPY[$strength['key']] ?? ['headline' => 'You show up.', 'body' => 'Your consistency is real.'];
        return ['headline' => $c['headline'], 'body' => $c['body']];
    }

    public static function blindSpotCard(array $blindSpot, string $seed): array {
        if ($blindSpot['type'] === 'risk_after_loss') {
            $s = $blindSpot['signal'];
            // §6.2 fix (2026-08-10) — this used to always tell the risk%
            // story, even when the flag actually fired purely from a
            // classification-rate jump (a real, distinct, sometimes
            // larger finding that was previously never mentioned
            // anywhere on the page) — and even when
            // relative_risk_increase_pct was itself negative, which
            // still read as "risk rises"/"up from" despite the numbers
            // showing the opposite. Branches on the real trigger(s)
            // TraderProfileIdentity::build() already determined.
            $trigger = $blindSpot['trigger'] ?? 'risk';
            if ($trigger === 'c_rate') {
                $headline = self::pick(["After a losing streak, your discipline slips.", "A loss changes how you trade the next one."], $seed . 'bs');
                $body = self::pick([
                    "After a loss, " . self::fmtPct($s['post_loss_c_rate_pct']) . " of your trades carry a real discipline flag (tilt, revenge, or oversized risk), up from " . self::fmtPct($s['baseline_c_rate_pct']) . " when you're not in a losing streak. That's where most of your drawdowns start.",
                    "On a normal trade, " . self::fmtPct($s['baseline_c_rate_pct']) . " of your trades carry a discipline flag. Right after a loss, that jumps to " . self::fmtPct($s['post_loss_c_rate_pct']) . ". Your position size barely moves. Your decision-making does.",
                ], $seed . 'bs2');
            } else {
                $headline = self::pick(["After a losing streak, your risk rises.", "Your size goes up right after you lose."], $seed . 'bs');
                $body = self::pick([
                    "After a loss, your average risk climbs to " . self::fmtPct($s['post_loss_avg_risk_pct']) . ", up from " . self::fmtPct($s['baseline_avg_risk_pct']) . " when you're not in a losing streak. That's where most of your drawdowns start.",
                    "You risk " . self::fmtPct($s['baseline_avg_risk_pct']) . " on a normal trade, but " . self::fmtPct($s['post_loss_avg_risk_pct']) . " right after a loss. That gap is worth closing.",
                ], $seed . 'bs2');
                // 'both' — the risk% story is still the lead (it's what
                // most directly ties to sizing decisions), but the real,
                // additional discipline-rate finding is never silently
                // dropped just because risk% also cleared its own bar.
                if ($trigger === 'both') {
                    $body .= ' On top of that, ' . self::fmtPct($s['post_loss_c_rate_pct']) . ' of those post-loss trades carry a real discipline flag too, up from ' . self::fmtPct($s['baseline_c_rate_pct']) . ' normally.';
                }
            }
            return ['headline' => $headline, 'body' => $body];
        }
        $labels = [
            'patience' => ['h' => "You still chase some entries.", 'b' => "Your patience isn't fully there yet, some entries are happening before your own setup criteria are met."],
            'discipline' => ['h' => "Your follow-through is still building.", 'b' => "Execution quality has room to grow, plans aren't always carried through the way they're written."],
            'consistency' => ['h' => "Your results swing more than they need to.", 'b' => "Risk and outcomes vary more trade to trade than a repeatable process usually produces."],
            'emotional_control' => ['h' => "Pressure still gets to you sometimes.", 'b' => "A meaningful share of your trades still carry the signs of an emotional decision, not a planned one."],
            'trust_in_strategy' => ['h' => "You drift from your own numbers.", 'b' => "You're stepping outside your strategy's declared risk more often than the plan calls for."],
        ];
        $l = $labels[$blindSpot['key']] ?? ['h' => 'Still developing.', 'b' => 'This is an area with room to grow.'];
        return ['headline' => $l['h'], 'body' => $l['b']];
    }

    public static function holdingBackCard(array $holdingBack, string $seed): array {
        if ($holdingBack['type'] === 'winners_losers') {
            $s = $holdingBack['signal'];
            $headline = self::pick(["You cut winners short.", "You take profit early, and let losses run."], $seed . 'hb');
            $capturePct = round($s['avg_winner_capture_ratio'] * 100);
            $realizationPct = round($s['avg_loser_realization_ratio'] * 100);
            $body = self::pick([
                "On your winning trades, you're capturing about {$capturePct}% of your planned target on average. On losers, you're realizing about {$realizationPct}% of your planned stop. Your winners are smaller than your plan; your losses aren't.",
                "You plan a target and take about {$capturePct}% of it on average before closing winners, but losses run to roughly {$realizationPct}% of your planned stop. That asymmetry is working against you.",
            ], $seed . 'hb2');
            return ['headline' => $headline, 'body' => $body];
        }
        $labels = [
            'patience' => ['h' => "Waiting is still the hard part.", 'b' => "Entries before your setup is fully there are still showing up in your history."],
            'discipline' => ['h' => "Follow-through needs work.", 'b' => "The gap between what your plan says and what you actually do is still real."],
            'consistency' => ['h' => "Your process still varies.", 'b' => "Sizing and outcomes aren't landing in as tight a range as they could."],
            'emotional_control' => ['h' => "Composure under pressure is still developing.", 'b' => "A real share of trades still show signs of an emotional call rather than a planned one."],
            'trust_in_strategy' => ['h' => "You're still finding your own numbers.", 'b' => "Sticking to your strategy's declared risk is still a work in progress."],
        ];
        $l = $labels[$holdingBack['key']] ?? ['h' => 'Still developing.', 'b' => 'This is an area with room to grow.'];
        return ['headline' => $l['h'], 'body' => $l['b']];
    }

    private static function LABELS(): array {
        return TraderProfileIdentity::LABELS;
    }

    // ── Emotional Pattern rungs ───────────────────────────────────────
    // Composed from two independent lookups (position noun x tier
    // descriptor) rather than one hand-picked combinatorial table — a
    // trader's risk/classification can jump to any severity at any
    // streak position (confirmed via real end-to-end testing against a
    // live account: one real trader's risk spiked to the 'breaking' tier
    // on the very FIRST post-loss trade), so a sparse table with only
    // the "expected" combinations filled in left real combinations
    // falling through to a placeholder. This composition covers every
    // (position, tier) pair the engine can ever produce.
    const POSITION_NOUN = [0 => 'Calm and focused', 1 => 'First loss', 2 => 'Second loss', 3 => 'Third loss', '4+' => 'Big loss', 'recovery' => 'Recovery'];

    const TIER_DESCRIPTOR = [
        'baseline' => ['label' => '', 'sub' => 'Your default state: no active losing streak.'],
        // §6.1 fix (2026-08-10) — a real, honest state for a genuine
        // loss-streak position whose risk data just isn't available to
        // compare (never the same thing as 'baseline', which means no
        // streak is active at all).
        'insufficient_risk_data' => ['label' => 'not enough risk data here', 'sub' => "There's a real losing streak at this point, but not enough recorded risk data to say how your sizing changed."],
        'steady' => ['label' => 'still in control', 'sub' => 'Your risk and decision-making barely move.'],
        'shifting' => ['label' => 'a shift is showing', 'sub' => 'Something starts to change here.'],
        'escalating' => ['label' => 'risk is climbing', 'sub' => 'This is where drawdowns usually start building.'],
        'breaking' => ['label' => 'discipline is breaking', 'sub' => 'The size and the decisions stop looking like your normal trading.'],
        'reset' => ['label' => 'back to normal', 'sub' => 'After the streak ends, you return close to your usual baseline.'],
        'still_elevated' => ['label' => 'not fully reset yet', 'sub' => "Even after the streak breaks, your risk hasn't fully come back down."],
    ];

    public static function rungCopy(array $rung): array {
        $noun = self::POSITION_NOUN[$rung['position']] ?? ('Trade ' . $rung['position']);
        $descriptor = self::TIER_DESCRIPTOR[$rung['tier']] ?? ['label' => '', 'sub' => 'Real data, still being characterized.'];
        $label = $descriptor['label'] === '' ? $noun : "{$noun}: {$descriptor['label']}";
        return ['label' => $label, 'sub' => $descriptor['sub']];
    }

    // ── When You Trade Best ─────────────────────────────────────────────
    public static function bestConditionLine(array $c): string {
        return $c['label'] . ': win rate ' . self::fmtPct($c['win_rate'], 0) . ' (' . ($c['delta_pp'] > 0 ? '+' : '') . number_format($c['delta_pp'], 0) . 'pp vs. your ' . self::fmtPct($c['baseline_win_rate'], 0) . ' baseline, ' . $c['n'] . ' trades)';
    }

    /** Type-aware preposition so "London session" and "Risking over 3%" both read as real sentences, not a label bolted onto one fixed template. */
    public static function worstConditionSentence(array $c): string {
        $type = explode(':', $c['key'])[0] ?? '';
        $clause = match ($type) {
            'session' => 'during the ' . $c['label'],           // "during the London session"
            // §6.10 fix (2026-08-10) — the frequency label itself already
            // ends in "that day" ("1 trade that day"/"2 trades that
            // day"/"3+ trades that day"), so prepending "on days with"
            // doubled up the day reference ("on days with 1 trade that
            // day"). Stripping the label's own trailing "that day" before
            // building the clause reads naturally for all three labels.
            'freq' => 'on days with ' . lcfirst(preg_replace('/\s+that day$/', '', $c['label'])),
            'risk' => 'when ' . lcfirst($c['label']),           // "when risking over 3%"
            default => 'with ' . lcfirst($c['label']),
        };
        return "Your worst results happen {$clause}. Worth avoiding when possible.";
    }

    // ── Growth Timeline ──────────────────────────────────────────────────
    const METRIC_NOUN = ['adherence' => 'strategy adherence', 'c_rate' => 'undisciplined trades', 'patience' => 'checklist patience', 'risk' => 'average risk per trade'];

    public static function growthTimelineLine(array $entry, string $seed): string {
        switch ($entry['metric']) {
            case 'c_rate':
                return $entry['direction'] === 'improved'
                    ? self::pick(["You cut back on tilt/revenge-style trades, down to " . self::fmtPct($entry['to'], 0) . " of your trades, from " . self::fmtPct($entry['from'], 0) . ".", "Fewer undisciplined trades this month: " . self::fmtPct($entry['to'], 0) . ", down from " . self::fmtPct($entry['from'], 0) . "."], $seed)
                    : self::pick(["Undisciplined trades crept up, " . self::fmtPct($entry['to'], 0) . " of trades, up from " . self::fmtPct($entry['from'], 0) . ".", "More tilt/revenge-style trades showed up this month: " . self::fmtPct($entry['to'], 0) . ", up from " . self::fmtPct($entry['from'], 0) . "."], $seed);
            case 'patience':
                return $entry['direction'] === 'improved'
                    ? "You started following more of your own checklist before entering, up to " . self::fmtPct($entry['to'], 0) . "."
                    : "You started skipping more of your own checklist before entering, down to " . self::fmtPct($entry['to'], 0) . ".";
            case 'adherence':
                return $entry['direction'] === 'improved'
                    ? "You started respecting your strategy's own risk numbers again, adherence up to " . self::fmtPct($entry['to'], 0) . "."
                    : "You drifted further from your strategy's declared risk, adherence down to " . self::fmtPct($entry['to'], 0) . ".";
            case 'risk':
            default:
                return $entry['direction'] === 'improved'
                    ? "Your average risk per trade came back down to " . self::fmtNum($entry['to']) . "%."
                    : "Your average risk per trade started climbing, up to " . self::fmtNum($entry['to']) . "%.";
        }
    }

    // ── Before Your Next Trade ──────────────────────────────────────────
    public static function nextTradeReminders(?array $nextFocus, string $seed): array {
        if (!$nextFocus) {
            return ["Keep journaling every trade. The more history you build, the more specific this gets."];
        }
        if ($nextFocus['type'] === 'risk_after_loss') {
            // §6.2 fix (2026-08-10) — same trigger-aware branching as
            // blindSpotCard(): "Don't increase your size" is real advice
            // only when the risk% signal is actually part of why this
            // was flagged. When the flag fired purely from a
            // classification-rate jump, the size-focused reminders were
            // pointing at the wrong lever entirely.
            $trigger = $nextFocus['trigger'] ?? 'risk';
            if ($trigger === 'c_rate') {
                return self::pickRotate([
                    "After a loss, slow down and run your full checklist. That's where this shows up, not your size.",
                    "If you've just lost, the risk isn't a bigger position, it's skipping a step you'd normally take.",
                    "A losing streak is a signal to double down on process, not to trade faster to make it back.",
                ], $seed);
            }
            return self::pickRotate([
                "Don't increase your size after a loss. That's exactly where this shows up.",
                "If you've just lost, your first instinct on the next trade is probably the one to slow down, not speed up.",
                "A losing streak is a signal to protect your account, not to make it back faster.",
            ], $seed);
        }
        if ($nextFocus['type'] === 'winners_losers') {
            return self::pickRotate([
                "Let your winners run to your actual plan. You're closing them early more than you realize.",
                "Your stop is already sized for the trade. Give your target the same respect.",
                "The plan you made before the trade was probably the right one, trust it on the way out too.",
            ], $seed);
        }
        $key = $nextFocus['key'] ?? null;
        $byKey = [
            'patience' => ["Wait for your full setup, not most of it, all of it.", "If your checklist isn't complete, the trade can wait."],
            'discipline' => ["Follow the plan you already wrote, not the one that feels right in the moment.", "The plan was made with a clear head, trust that version of you."],
            'consistency' => ["Size this trade the same way you sized your last few. Don't let one setup feel like an exception.", "Repeatable beats optimal, do it the same way again."],
            'emotional_control' => ["Take a breath before you click in. Pressure is exactly when this habit shows up.", "If this feels urgent, that's usually the sign to slow down."],
            'trust_in_strategy' => ["Stay inside your strategy's own risk number on this one.", "Your strategy's numbers exist for a reason, this is the trade to respect them on."],
        ];
        return self::pickRotate($byKey[$key] ?? ["Focus on the process on this next trade, not the outcome."], $seed);
    }

    // ── A Letter to You ──────────────────────────────────────────────────
    // Written like TraderAlign itself is speaking — quiet, honest, first
    // person — after actually watching the trader's decisions unfold, not
    // like a report summarizing them. Every line here references a fact
    // already shown elsewhere on the page ($ctx only ever carries values
    // the caller already rendered in another section) — this function
    // never introduces a new claim, never flatters, never invents.
    public static function letterToYou(array $ctx, string $seed): array {
        $lines = [];

        // §6.11 fix (2026-08-10) — variant 2 names the real trade count
        // ("I see the {$ctx['total_trades']} decisions behind it"), which
        // read as a real fact for any real n, but for total_trades === 0
        // rendered the nonsensical "I see the 0 decisions behind it" —
        // that variant is now only offered as a candidate when there's a
        // real, nonzero count to name; the count-free variant 1 always
        // stays available regardless.
        $openingLines = ["I've been watching your trades this {$ctx['period_noun']}, not just where they landed, but the decisions behind them."];
        if (!empty($ctx['total_trades'])) {
            $openingLines[] = "I don't just see your P&L. I see the {$ctx['total_trades']} decisions behind it this {$ctx['period_noun']}, and that's what this page is actually built from.";
        }
        $lines[] = self::pick($openingLines, $seed . 'l1');

        if ($ctx['strength_label']) {
            $lines[] = self::pick([
                "One thing keeps showing up, trade after trade: your {$ctx['strength_label']}. It's not a guess. It's there in the pattern.",
                "Your {$ctx['strength_label']} isn't a one-off. It's shown up often enough this {$ctx['period_noun']} that it's become something I can actually point to.",
            ], $seed . 'l2');
        }

        if (!empty($ctx['pattern_occurrences']) && $ctx['pattern_occurrences'] > 0) {
            $lines[] = self::pick([
                "The pattern I flagged above isn't a hunch either. I've watched it repeat {$ctx['pattern_occurrences']} separate times. That's the only reason I'm confident enough to name it.",
                "I wouldn't have mentioned the pattern above if I'd only seen it once. I've seen it {$ctx['pattern_occurrences']} times now, and that's what makes it real, not a coincidence.",
            ], $seed . 'l2b');
        }

        if ($ctx['focus_line']) {
            $lines[] = self::pick([
                "So here's the honest part: {$ctx['focus_line']}",
                "If I could ask you to hold onto just one thing from this {$ctx['period_noun']}, it's this: {$ctx['focus_line']}",
            ], $seed . 'l3');
        }

        $lines[] = self::pick([
            "I'm not writing this to flatter you or to worry you. Just to tell you what I've actually seen. Keep going.",
            "None of this is dramatic. It's just what really happened. And it's enough. Keep going.",
        ], $seed . 'l4');

        return $lines;
    }
}
