<?php
require_once __DIR__ . '/../api/analytics/engine.php';
require_once __DIR__ . '/../api/analytics/edge_health_engine.php';
require_once __DIR__ . '/../src/RenderHelpers.php';
require_once __DIR__ . '/TraderProfileBehaviorEngine.php';
require_once __DIR__ . '/TraderProfileIdentity.php';
require_once __DIR__ . '/TraderProfileNarrative.php';

/**
 * Trader Profile Service — the orchestrator behind public/trader-profile.php.
 * Calls AnalyticsEngine/EdgeHealthEngine for everything they already
 * compute correctly (all-account win rate/expectancy, Stage 1 strategy
 * adherence, Discipline/Consistency scores), calls
 * TraderProfileBehaviorEngine only for the genuinely new signals, then
 * hands everything to TraderProfileIdentity + TraderProfileNarrative to
 * turn into the one $profile array the view renders. No SQL lives in
 * this file — it's pure orchestration.
 */
class TraderProfileService
{
    const PERIODS = ['monthly', 'quarterly', 'annual'];

    /**
     * Calendar-aligned period boundaries — Monthly/Quarterly/Annual only
     * (no Weekly, per the module's own "different levels of maturity"
     * framing; build() below already restricts $period to self::PERIODS
     * before this is ever called, so the Weekly case in the shared
     * function is simply never reached from here).
     *
     * 2026-08-08 (Single Source of Truth migration Wave 5d): was this
     * method's own byte-for-byte duplicate of public/reports.php's inline
     * $start/$end switch, kept separate at the time specifically because
     * reports.php's own comment said "extract when a second caller needs
     * it" — this was that second caller. Now that reports.php itself has
     * been repointed to the same shared getReportingPeriodBounds()
     * (src/RenderHelpers.php), delegating here too closes the exact
     * latent-drift risk that duplication was flagged for — both callers
     * now share one real implementation instead of two hand-kept-in-sync
     * copies. Return shape (period/start/end/start_str/end_str/label/
     * period_noun) is unchanged, so build() below needed no changes.
     *
     * 2026-08-18 (Letter grace-window fix): gained an optional
     * $referenceDate passthrough to getReportingPeriodBounds() — needed so
     * this same, single boundary calculation can also answer "what were
     * THIS period's bounds as of some other moment" (specifically: the
     * period immediately before the one containing right now), rather than
     * a second, hand-rolled "previous period" calculation.
     */
    private static function periodBounds(string $period, ?DateTime $referenceDate = null): array {
        return getReportingPeriodBounds($period, $referenceDate);
    }

    /**
     * Everything the page needs for ONE reporting window: reused-engine
     * calls plus the new behavioral signals plus the derived Trading
     * Character bars / Identity. Extracted 2026-08-18 (Letter grace-window
     * fix) so it can be run twice in one request when needed — once for the
     * live "current period" the rest of the page always shows, and once
     * more, ONLY when the letter's own grace window (see build() below) is
     * still open right after a period has just turned over, for the
     * just-completed period the letter itself should keep describing.
     * Running this a second time is cheap and rare (only within the grace
     * window immediately after a rollover), and never touches anything
     * outside the passed-in date range — a completely separate computation
     * from whatever the main page is showing.
     */
    private static function computePeriodCore(string $userId, string $startStr, string $endStr): array {
        $userMetrics = [];
        try { $userMetrics = AnalyticsEngine::getUserMetrics($userId, ['dateFrom' => $startStr, 'dateTo' => $endStr]); }
        catch (\Throwable $e) { error_log('TraderProfileService: getUserMetrics failed: ' . $e->getMessage()); }

        $stageSummary = ['trades_in_period' => 0];
        try { $stageSummary = AnalyticsEngine::getFourStagePeriodSummary($userId, 'all', $startStr, $endStr); }
        catch (\Throwable $e) { error_log('TraderProfileService: getFourStagePeriodSummary failed: ' . $e->getMessage()); }

        $edgeFactors = ['n' => 0, 'tier' => 'none', 'consistency_score' => null, 'discipline_score' => null];
        try { $edgeFactors = EdgeHealthEngine::getPeriodFactors($userId, 'all', $startStr, $endStr); }
        catch (\Throwable $e) { error_log('TraderProfileService: getPeriodFactors failed: ' . $e->getMessage()); }

        $riskAfterLoss = TraderProfileBehaviorEngine::getRiskAfterLossSignal($userId, $startStr, $endStr);
        $winnersLosers = TraderProfileBehaviorEngine::getWinnersLosersBehavior($userId, $startStr, $endStr);
        $patience = TraderProfileBehaviorEngine::getPatienceSignal($userId, $startStr, $endStr);
        $classification = TraderProfileBehaviorEngine::getClassificationDistribution($userId, $startStr, $endStr);
        $bestConditionsResult = TraderProfileBehaviorEngine::getBestConditions($userId, $startStr, $endStr);
        $bestConditions = $bestConditionsResult['conditions'];

        $trustN = $stageSummary['stage1']['strategy_adherence_m'] ?? 0;
        $characters = [
            'patience' => [
                'label' => 'Patience', 'value' => $patience['avg_ticked_pct'] ?? null, 'n' => $patience['n'] ?? 0,
                'tier' => TraderProfileBehaviorEngine::tierForN($patience['n'] ?? 0),
            ],
            'discipline' => [
                'label' => 'Discipline', 'value' => $edgeFactors['discipline_score'], 'n' => $edgeFactors['n'],
                'tier' => $edgeFactors['tier'],
            ],
            'consistency' => [
                'label' => 'Consistency', 'value' => $edgeFactors['consistency_score'], 'n' => $edgeFactors['n'],
                'tier' => $edgeFactors['tier'],
            ],
            'emotional_control' => [
                'label' => 'Emotional Control',
                'value' => $classification['c_rate_pct'] !== null ? round(100 - $classification['c_rate_pct'], 1) : null,
                'n' => $classification['total'], 'tier' => TraderProfileBehaviorEngine::tierForN($classification['total']),
            ],
            'trust_in_strategy' => [
                'label' => 'Trust in Your Strategy', 'value' => $stageSummary['stage1']['strategy_adherence_pct'] ?? null,
                'n' => $trustN, 'tier' => TraderProfileBehaviorEngine::tierForN((int)$trustN),
            ],
        ];

        $identity = TraderProfileIdentity::build($characters, [
            'risk_after_loss' => $riskAfterLoss, 'winners_losers' => $winnersLosers,
        ]);

        return [
            'user_metrics' => $userMetrics,
            'total_trades' => (int)($userMetrics['total_trades'] ?? 0),
            'characters' => $characters,
            'identity' => $identity,
            'risk_after_loss' => $riskAfterLoss,
            'winners_losers' => $winnersLosers,
            'patience' => $patience,
            'classification' => $classification,
            'best_conditions' => $bestConditions,
            'best_conditions_well_sampled' => $bestConditionsResult['well_sampled'],
        ];
    }

    /**
     * "A Letter to You" grace window — 2026-08-18.
     *
     * A letter about a just-completed period is worth reading for a while
     * after that period ends, this is a deliberate, SEPARATE window from
     * the live, continuously-updating stats/bars/findings around it (same
     * idea as e.g. a monthly recap staying visible after the month turns
     * over, rather than instantly being replaced by a brand-new, still
     * near-empty period's own not-yet-meaningful letter). Scoped
     * deliberately narrow to the letter only, per the explicit ask, every
     * OTHER section of the page keeps showing the live current period as
     * normal, unaffected by any of this.
     *
     * Mechanism, generic, no hardcoded calendar date: compute the period
     * immediately before the one containing "now" (by asking
     * getReportingPeriodBounds() for $period's own bounds as of one day
     * before the current period's own start, which always lands inside the
     * prior period for month/quarter/year-aligned boundaries), then check
     * whether "now" still falls inside [that prior period's real end, that
     * end + this period type's own grace duration]. While it does, the
     * letter is built from the PRIOR period's own data instead of the
     * live/current one.
     *
     * Grace durations, per the explicit business rule: Monthly 24 hours;
     * Quarterly ~5-7 days (6 days used as the stated range's own midpoint,
     * a documented judgment call, not independently dictated); Annual 7
     * days.
     */
    const LETTER_GRACE_HOURS = [
        'monthly' => 24,
        'quarterly' => 24 * 6,
        'annual' => 24 * 7,
    ];

    public static function build(string $userId, string $period, string $displayName): array {
        if (!in_array($period, self::PERIODS, true)) $period = 'monthly';
        $bounds = self::periodBounds($period);
        $startStr = $bounds['start_str']; $endStr = $bounds['end_str'];
        $seed = $userId . $period;

        // ── Current period (drives the whole page, exactly as before) ────
        $core = self::computePeriodCore($userId, $startStr, $endStr);
        $userMetrics = $core['user_metrics'];
        $characters = $core['characters'];
        $identity = $core['identity'];
        $riskAfterLoss = $core['risk_after_loss'];
        $winnersLosers = $core['winners_losers'];
        $patience = $core['patience'];
        $classification = $core['classification'];
        $bestConditions = $core['best_conditions'];
        $bestConditionsResult = ['well_sampled' => $core['best_conditions_well_sampled']];

        $edgeAllTime = null;
        try { $edgeAllTime = EdgeHealthEngine::getScore($userId); }
        catch (\Throwable $e) { error_log('TraderProfileService: getScore failed: ' . $e->getMessage()); }

        // emotionalPattern/growthTimeline are already all-time (no date
        // range passed) — genuinely the same regardless of which period
        // (current or grace-window-prior) the letter ends up describing,
        // so these are never recomputed a second time below.
        $emotionalPattern = TraderProfileBehaviorEngine::getEmotionalPattern($userId);
        $growthTimeline = TraderProfileBehaviorEngine::getGrowthTimeline($userId);

        // ── "A Letter to You" grace window ────────────────────────────────
        $now = new DateTime();
        $prevReferenceDate = (clone $bounds['start'])->modify('-1 day');
        $prevBounds = self::periodBounds($period, $prevReferenceDate);
        $graceHours = self::LETTER_GRACE_HOURS[$period] ?? 24;
        $graceDeadline = (clone $prevBounds['end'])->modify("+{$graceHours} hours");
        // Only meaningful once the prior period has actually produced a
        // real, distinct "end" before right now — always true in practice
        // (every period has a real predecessor), kept as an explicit guard
        // rather than assumed.
        $letterInGraceWindow = ($prevBounds['end'] < $now) && ($now <= $graceDeadline);

        if ($letterInGraceWindow) {
            // Re-run the same period-scoped computation, but for the
            // just-ended period instead of the live current one — a
            // separate, isolated pass; nothing computed here touches
            // $characters/$identity/etc. above, which still describe the
            // live current period for the rest of the page.
            $letterCore = self::computePeriodCore($userId, $prevBounds['start_str'], $prevBounds['end_str']);
            $letterPeriodNoun = $prevBounds['period_noun'];
            $letterTotalTrades = $letterCore['total_trades'];
            $letterIdentity = $letterCore['identity'];
        } else {
            $letterPeriodNoun = $bounds['period_noun'];
            $letterTotalTrades = (int)($userMetrics['total_trades'] ?? 0);
            $letterIdentity = $identity;
        }

        // ── Narrative ────────────────────────────────────────────────────
        $narrative = [
            'greeting_subtext' => TraderProfileNarrative::greetingSubtext($identity, $bounds['period_noun'], $seed),
            'strength_card' => $identity['strength'] ? TraderProfileNarrative::strengthCard($identity['strength']) : null,
            'blind_spot_card' => $identity['blind_spot'] ? TraderProfileNarrative::blindSpotCard($identity['blind_spot'], $seed) : null,
            'holding_back_card' => $identity['holding_back'] ? TraderProfileNarrative::holdingBackCard($identity['holding_back'], $seed) : null,
            'next_trade_reminders' => TraderProfileNarrative::nextTradeReminders($identity['next_focus'], $seed),
        ];

        $emotionalRungs = [];
        if (!empty($emotionalPattern['available'])) {
            foreach ($emotionalPattern['rungs'] as $rung) {
                $copy = TraderProfileNarrative::rungCopy($rung);
                $emotionalRungs[] = array_merge($rung, $copy);
            }
        }

        $bestConditionLines = [];
        foreach ($bestConditions as $c) {
            $bestConditionLines[] = array_merge($c, ['line' => TraderProfileNarrative::bestConditionLine($c)]);
        }

        $growthEntries = [];
        if (!empty($growthTimeline['available'])) {
            foreach ($growthTimeline['entries'] as $e) {
                $growthEntries[] = array_merge($e, ['line' => TraderProfileNarrative::growthTimelineLine($e, $seed)]);
            }
        }

        // seed intentionally still keyed by the page's own $period (not
        // whichever period the letter ends up describing) — this is a
        // phrase-variety seed, not a data scope, and keeping it stable
        // means a trader re-visiting during the grace window sees the same
        // letter rather than a new random variant each time.
        $letter = TraderProfileNarrative::letterToYou([
            'period_noun' => $letterPeriodNoun,
            'total_trades' => $letterTotalTrades,
            'strength_label' => $letterIdentity['strength'] ? strtolower($letterIdentity['strength']['label']) : null,
            'focus_line' => $letterIdentity['next_focus'] ? (TraderProfileNarrative::nextTradeReminders($letterIdentity['next_focus'], $seed . 'letter')[0] ?? null) : null,
            // Only ever the SAME real occurrence count already shown in the
            // Emotional Pattern section above — never a second, independent claim.
            'pattern_occurrences' => !empty($emotionalPattern['available']) ? $emotionalPattern['occurrences'] : null,
        ], $seed);

        return [
            'period' => $period, 'period_label' => $bounds['label'], 'period_noun' => $bounds['period_noun'],
            'start' => $bounds['start'], 'end' => $bounds['end'],
            'display_name' => $displayName,
            'total_trades' => (int)($userMetrics['total_trades'] ?? 0),
            'user_metrics' => $userMetrics,
            'identity' => $identity,
            'characters' => $characters,
            'narrative' => $narrative,
            'emotional_pattern' => $emotionalPattern,
            'emotional_rungs' => $emotionalRungs,
            'best_conditions' => $bestConditionLines,
            'best_conditions_well_sampled' => $bestConditionsResult['well_sampled'],
            'growth_timeline' => $growthTimeline,
            'growth_entries' => $growthEntries,
            'risk_after_loss' => $riskAfterLoss,
            'winners_losers' => $winnersLosers,
            'patience' => $patience,
            'classification' => $classification,
            'letter' => $letter,
            'edge_all_time' => $edgeAllTime,
            // Lets the view show an honest "this letter is about {period},
            // available until {date}" note during the grace window, rather
            // than silently mixing a different period's letter into the
            // page with no explanation.
            'letter_period_label' => $letterInGraceWindow ? $prevBounds['label'] : $bounds['label'],
            'letter_in_grace_window' => $letterInGraceWindow,
            'letter_grace_deadline' => $letterInGraceWindow ? $graceDeadline : null,
        ];
    }
}
