<?php

/**
 * Trader Profile Identity — pure, no DB access. Takes the five already-
 * computed Character-bar scores (see TraderProfileService) plus the
 * behavioral signals from TraderProfileBehaviorEngine, and returns which
 * archetype, strength, blind spot, and "what's holding you back" apply.
 *
 * Never asserts a label the underlying numbers don't support — every
 * returned field traces back to one specific score or signal, which the
 * view renders as that finding's evidence.
 */
class TraderProfileIdentity
{
    // Minimum tier (see TraderProfileBehaviorEngine::tierForN) for a
    // Character score to be eligible as a Strength/Blind Spot candidate.
    // 'small_sample' = n >= 10 — a deliberately lighter bar than the
    // all-time Edge Health card's, since this operates on one Monthly/
    // Quarterly/Annual period rather than a trader's whole history.
    const ELIGIBLE_MIN_TIER_RANK = 3; // small_sample

    const TIER_RANK = ['none' => 0, 'single_trade' => 1, 'very_small_sample' => 2, 'small_sample' => 3, 'developing' => 4, 'confident' => 5];

    // §6.4 — the minimum Character-bar score (0-100) for "Your Greatest
    // Strength" to actually claim one. Reuses EdgeHealthEngine::
    // scoreToStars()'s own existing 3-star cutoff (>=55), this
    // codebase's established definition of "a real positive grade," so
    // this isn't a newly-invented number.
    const MIN_STRENGTH_VALUE = 55.0;

    // strength key => weak key => [name, line]. 'default' covers any weak
    // key not explicitly listed under that strength. Every line is written
    // to be immediately followed by real evidence on the page, not to
    // stand alone as a verdict.
    const ARCHETYPES = [
        'patience' => [
            'emotional_control' => ['name' => 'The Patient Builder', 'line' => "You wait for your setups. You plan with confidence. Your challenge is holding that plan when pressure rises."],
            'trust_in_strategy' => ['name' => 'The Independent Thinker', 'line' => "You've got the patience to wait for your own read on the market. The part you're still working on is trusting your strategy's numbers when your gut disagrees."],
            'default' => ['name' => 'The Patient Builder', 'line' => "You wait for your setups more than most traders manage. That patience is doing real work for your account."],
        ],
        'discipline' => [
            'emotional_control' => ['name' => 'The Steady Hand', 'line' => "Your execution has been getting sharper as periods go on. Where it slips is under pressure, after things start going wrong."],
            'consistency' => ['name' => 'The Disciplined Grinder', 'line' => "You're doing the right things more often lately. The next step is doing them the same way every time, not just most of the time."],
            'default' => ['name' => 'The Steady Hand', 'line' => "Your execution has genuinely improved over this period. That's not luck, that's a real trend in your own numbers."],
        ],
        'consistency' => [
            'patience' => ['name' => 'The Momentum Trader', 'line' => "Your sizing and your results land in a tight, repeatable range. Where it breaks down is waiting for the setup in the first place."],
            'emotional_control' => ['name' => 'The Streaky Competitor', 'line' => "When you're on, your numbers are remarkably steady. The swings show up around losses, not around wins."],
            'default' => ['name' => 'The Steady Operator', 'line' => "Your risk and your results are landing in a tight, repeatable range, and that consistency is worth protecting."],
        ],
        'emotional_control' => [
            'patience' => ['name' => 'The Calm Operator', 'line' => "You stay level when things go wrong. What's still developing is waiting for the right moment before you get in."],
            'trust_in_strategy' => ['name' => 'The Free Spirit', 'line' => "Losses don't rattle you. That's rare and it's real. Where you drift is off your own strategy's declared numbers."],
            'default' => ['name' => 'The Calm Operator', 'line' => "You stay level when a trade goes against you. That composure is the foundation everything else gets built on."],
        ],
        'trust_in_strategy' => [
            'patience' => ['name' => 'The Eager Executor', 'line' => "When you trade, you trade your plan. What's still developing is waiting for it to actually set up."],
            'discipline' => ['name' => 'The Rule Follower', 'line' => "You stay inside your strategy's own numbers more than most. The trend in your execution quality is what's still catching up."],
            'default' => ['name' => 'The Rule Follower', 'line' => "You stay closer to your strategy's declared risk and R:R than most traders manage, and that discipline shows up in the numbers."],
        ],
    ];

    const LABELS = [
        'patience' => 'Patience',
        'discipline' => 'Discipline',
        'consistency' => 'Consistency',
        'emotional_control' => 'Emotional Control',
        'trust_in_strategy' => 'Trust in Your Strategy',
    ];

    /**
     * @param array $characters key => ['value'=>float|null, 'tier'=>string]
     * @param array $signals ['risk_after_loss'=>..., 'winners_losers'=>...]
     */
    public static function build(array $characters, array $signals): array {
        $eligible = [];
        foreach ($characters as $key => $c) {
            if ($c['value'] === null) continue;
            if ((self::TIER_RANK[$c['tier']] ?? 0) < self::ELIGIBLE_MIN_TIER_RANK) continue;
            $eligible[$key] = $c;
        }

        if (count($eligible) < 3) {
            return [
                'ready' => false,
                'archetype' => ['name' => 'The Developing Trader', 'line' => "You're still early in building the kind of track record we can read real patterns from. Keep journaling: this page gets sharper with every trade you close."],
                'strength' => null, 'strength_below_floor' => false, 'blind_spot' => null, 'holding_back' => null, 'next_focus' => null,
            ];
        }

        uasort($eligible, fn($a, $b) => $b['value'] <=> $a['value']);
        $keys = array_keys($eligible);
        $strengthKey = $keys[0];
        $weakestKey = end($keys);
        // §6.4 fix (2026-08-10) — eligibility here only ever checked
        // SAMPLE SIZE (tier >= small_sample), never the score's actual
        // VALUE. The highest of five mediocre scores (e.g. a genuinely
        // weak trader's least-bad trait at 31/100) got the exact same
        // unconditionally enthusiastic "Your X protects your account
        // more than you probably realize" copy as a genuine 90+ score —
        // the real number was visible only inside a collapsed evidence
        // drawer. MIN_STRENGTH_VALUE (55) reuses this codebase's own
        // existing definition of "a real positive grade," not an
        // invented cutoff — it's the exact same threshold
        // EdgeHealthEngine::scoreToStars() already uses for its 3-star
        // band. Below it, there is no real standout — $strength stays
        // null (rendered as an honest "nothing stands out yet" state,
        // distinct from the separate "not enough trades at all" state)
        // and the archetype (which is built entirely around "your
        // greatest strength is X") falls back to the same generic
        // "still finding its shape" copy it already uses when no
        // archetype table match exists at all.
        $hasRealStrength = $eligible[$strengthKey]['value'] >= self::MIN_STRENGTH_VALUE;
        $strength = $hasRealStrength
            ? ['key' => $strengthKey, 'label' => self::LABELS[$strengthKey], 'value' => $eligible[$strengthKey]['value']]
            : null;

        // Flags on EITHER a real jump in position size OR a real jump in
        // undisciplined-trade rate after a loss — found via real end-to-end
        // testing that a trader can show a dramatic classification shift
        // (e.g. 0% -> 58% C-grade after a loss) with only a modest risk%
        // increase, which a risk%-only threshold would have missed entirely.
        $ral = $signals['risk_after_loss'] ?? [];
        // §6.2 fix (2026-08-10) — this flag is a real, intentional OR of
        // two independent signals (a risk% jump vs. a discipline/
        // classification-rate jump after a loss — found via live testing
        // that a dramatic c-rate shift, e.g. 0%->58%, can occur with only
        // a modest risk% increase a risk%-only threshold would have
        // missed). Every downstream sentence used to assume it was ALWAYS
        // the risk% story, even when only the c-rate signal actually
        // tripped, and even though relative_risk_increase_pct can itself
        // be negative while c_rate_delta_pp alone clears the flag — in
        // that case the old wording said risk "climbs"/"up from" using
        // numbers that show the opposite direction. $riskTrigger/
        // $cRateTrigger are computed once here and threaded through as
        // 'trigger' so every consumer (blindSpotCard(), nextTradeReminders(),
        // the evidence drawer) renders the finding that's actually real.
        $riskTrigger = ($ral['relative_risk_increase_pct'] ?? 0) >= 30;
        $cRateTrigger = ($ral['c_rate_delta_pp'] ?? 0) >= 25;
        $riskAfterLossFlag = !empty($ral['available']) && ($riskTrigger || $cRateTrigger);
        $winnersLosersFlag = !empty($signals['winners_losers']['available']) && !empty($signals['winners_losers']['cuts_winners_short']);

        if ($riskAfterLossFlag) {
            $trigger = ($riskTrigger && $cRateTrigger) ? 'both' : ($riskTrigger ? 'risk' : 'c_rate');
            $blindSpot = ['type' => 'risk_after_loss', 'signal' => $signals['risk_after_loss'], 'trigger' => $trigger];
        } else {
            $blindSpot = ['type' => 'character', 'key' => $weakestKey, 'label' => self::LABELS[$weakestKey], 'value' => $eligible[$weakestKey]['value']];
        }

        // §6.6 fix (2026-08-10) — this used to always reach for the
        // SECOND-weakest character (index n-2) whenever Blind Spot wasn't
        // already occupying that exact key, regardless of whether Blind
        // Spot was actually a character trait at all. When Blind Spot is
        // 'risk_after_loss' (not a character trait — see the flag logic
        // above), the true weakest character (index n-1, $weakestKey) was
        // never consumed by anything, yet Holding Back still skipped past
        // it straight to the second-weakest — permanently excluding the
        // genuinely lowest-scoring character from ever appearing anywhere
        // on this page (Strength/Blind Spot/Holding Back). Now: only skip
        // to the second-weakest when Blind Spot genuinely already claimed
        // the true weakest (type==='character'); otherwise Holding Back
        // uses the true weakest directly, since nothing else claimed it.
        $holdingBack = null;
        if ($winnersLosersFlag) {
            $holdingBack = ['type' => 'winners_losers', 'signal' => $signals['winners_losers']];
        } elseif ($blindSpot['type'] === 'character' && count($keys) >= 2) {
            $secondWeakestKey = $keys[count($keys) - 2];
            $holdingBack = ['type' => 'character', 'key' => $secondWeakestKey, 'label' => self::LABELS[$secondWeakestKey], 'value' => $eligible[$secondWeakestKey]['value']];
        } elseif ($blindSpot['type'] !== 'character') {
            $holdingBack = ['type' => 'character', 'key' => $weakestKey, 'label' => self::LABELS[$weakestKey], 'value' => $eligible[$weakestKey]['value']];
        }

        // "Before Your Next Trade" is one focus, not a grab-bag — prioritize
        // a specific detected behavior over a generic low character score,
        // and risk-after-loss (higher-stakes: streak risk escalation) over
        // cut-winners-short (opportunity cost, lower-stakes) when both fire.
        if ($riskAfterLossFlag) {
            $nextFocus = $blindSpot;
        } elseif ($winnersLosersFlag) {
            $nextFocus = $holdingBack;
        } else {
            $nextFocus = $blindSpot;
        }

        $weakKeyForArchetype = $blindSpot['type'] === 'character' ? $blindSpot['key'] : null;
        // §6.4 — every ARCHETYPES entry is built around "your greatest
        // strength is X"; when there IS no real strength ($hasRealStrength
        // false), naming an archetype off the merely-highest-ranked
        // (but not actually strong) trait would carry the same false
        // confidence this fix removes from the Strength card itself —
        // falls back to the same generic "still finding its shape" copy
        // already used when no archetype table match exists at all.
        $archTable = $hasRealStrength ? (self::ARCHETYPES[$strengthKey] ?? null) : null;
        $archetype = $archTable
            ? ($archTable[$weakKeyForArchetype] ?? $archTable['default'])
            : ['name' => 'The Developing Trader', 'line' => "Your trading is still finding its shape. Keep journaling and a clearer picture will form."];

        return [
            'ready' => true,
            'archetype' => $archetype,
            'strength' => $strength,
            // §6.4 — distinguishes "no strength shown because there
            // genuinely aren't 3 eligible characters yet" (the pre-
            // existing 'ready' => false case above) from "there ARE
            // enough real, well-sampled characters, but none of them
            // actually cleared the bar for a real strength" — the view
            // renders a different, honest message for each.
            'strength_below_floor' => !$hasRealStrength,
            'blind_spot' => $blindSpot,
            'holding_back' => $holdingBack,
            'next_focus' => $nextFocus,
        ];
    }
}
