<?php
/**
 * Colex AI - Shared Helpers
 *
 * Date-range math, period stats, greeting, context cards, and suggested
 * prompts logic — shared between public/colex.php (initial page render)
 * and api/colex/stats.php (the JSON endpoint the frontend calls on load
 * and whenever the period/date-range selector changes).
 *
 * Keeping this in one place means the numbers a visitor sees on first paint
 * and the numbers returned by the AJAX refresh can never drift apart.
 */

require_once __DIR__ . '/../api/analytics/engine.php';
require_once __DIR__ . '/RenderHelpers.php';

if (!function_exists('calculateDateRange')) {
    function calculateDateRange($period, $customFrom = null, $customTo = null) {
        $now = new DateTime();
        $from = clone $now;
        $to = clone $now;

        switch ($period) {
            case 'current_week':
                $from->modify('monday this week');
                $from->setTime(0, 0, 0);
                $to->setTime(23, 59, 59);
                $display = $from->format('M j') . ' - ' . $to->format('M j, Y');
                break;

            case 'current_month':
                $from->modify('first day of this month');
                $from->setTime(0, 0, 0);
                $to->setTime(23, 59, 59);
                $display = $from->format('M j') . ' - ' . $to->format('M j, Y');
                break;

            case 'current_quarter':
                $quarterStart = ceil($now->format('n') / 3);
                $from->setDate($now->format('Y'), (($quarterStart - 1) * 3) + 1, 1);
                $from->setTime(0, 0, 0);
                $to->setTime(23, 59, 59);
                $display = $from->format('M j') . ' - ' . $to->format('M j, Y');
                break;

            case 'current_year':
                $from->setDate($now->format('Y'), 1, 1);
                $from->setTime(0, 0, 0);
                $to->setTime(23, 59, 59);
                $display = $from->format('M j') . ' - ' . $to->format('M j, Y');
                break;

            case 'previous_week':
                $from->modify('monday last week');
                $from->setTime(0, 0, 0);
                $to->modify('sunday last week');
                $to->setTime(23, 59, 59);
                $display = $from->format('M j') . ' - ' . $to->format('M j, Y');
                break;

            case 'previous_month':
                $from->modify('first day of last month');
                $from->setTime(0, 0, 0);
                $to->modify('last day of last month');
                $to->setTime(23, 59, 59);
                $display = $from->format('M j') . ' - ' . $to->format('M j, Y');
                break;

            case 'previous_quarter':
                $currentQuarter = ceil($now->format('n') / 3);
                $previousQuarterStart = ($currentQuarter - 2) * 3;
                $month = $previousQuarterStart + 1;
                $year = $now->format('Y');
                if ($previousQuarterStart < 0) {
                    $month = $previousQuarterStart + 13;
                    $year = $now->format('Y') - 1;
                }
                $from->setDate($year, $month, 1);
                $from->setTime(0, 0, 0);
                $to = clone $from;
                $to->modify('+3 months');
                $to->modify('-1 day');
                $to->setTime(23, 59, 59);
                $display = $from->format('M j') . ' - ' . $to->format('M j, Y');
                break;

            case 'previous_year':
                $from->setDate($now->format('Y') - 1, 1, 1);
                $from->setTime(0, 0, 0);
                $to->setDate($now->format('Y') - 1, 12, 31);
                $to->setTime(23, 59, 59);
                $display = $from->format('M j, Y') . ' - ' . $to->format('M j, Y');
                break;

            case 'custom':
                if ($customFrom) {
                    $from = new DateTime($customFrom);
                } else {
                    $from->modify('-7 days');
                }
                $from->setTime(0, 0, 0);

                if ($customTo) {
                    $to = new DateTime($customTo);
                }
                $to->setTime(23, 59, 59);
                $display = $from->format('M j, Y') . ' - ' . $to->format('M j, Y');
                break;

            default:
                $from->modify('monday this week');
                $from->setTime(0, 0, 0);
                $to->setTime(23, 59, 59);
                $display = $from->format('M j') . ' - ' . $to->format('M j, Y');
                break;
        }

        return [
            'from' => $from->format('Y-m-d H:i:s'),
            'to' => $to->format('Y-m-d H:i:s'),
            'display' => $display
        ];
    }
}

if (!function_exists('colexPeriodPosition')) {
    /**
     * Natural, context-aware description of where the trader currently
     * sits within the selected reporting period — the shared foundation
     * every Colex response's opening/period references build on, instead
     * of each response independently inserting a raw "This period
     * ({$display})" or a bare "this period" into a sentence. Requested
     * directly (2026-08-10) after Kevin's own live testing: switching the
     * period selector should make Colex sound like it knows where the
     * trader is in their trading week/month/quarter/year, not just swap a
     * date range into a template. Deterministic and rule-based (no LLM),
     * but the phrasing is designed around how a human trading coach
     * actually talks about "where we are."
     *
     * $period is the raw selector value ("current_week", "previous_month",
     * "custom", ...); $dateRange is calculateDateRange()'s own return
     * shape (from/to/display) for that same period — both already
     * computed by every caller, nothing new to fetch.
     *
     * Returns:
     *   is_custom     bool   no natural period name exists (a user-picked range)
     *   is_complete   bool   the period has already fully elapsed (previous_*,
     *                        or a custom range that ends before today)
     *   is_first_day  bool   today is the period's own first calendar day
     *                        (only meaningful for an ongoing, non-custom period)
     *   noun          ?string 'week'/'month'/'quarter'/'year', or null for custom
     *   intro         ?string a standalone lead sentence for the first-day case,
     *                        e.g. "Today is the first day of your trading week."
     *   lead          ?string a lead-in clause for the facts sentence, e.g.
     *                        "So far this week" / "Last month" / "Between Aug 1
     *                        and Aug 15" — null when `intro` already sets the scene
     *   mid_ref       string  a short natural phrase for swapping into the
     *                        middle of an existing sentence in place of the old
     *                        generic "this period", e.g. "this week" / "last
     *                        quarter" / "in that date range"
     */
    function colexPeriodPosition($period, $dateRange) {
        $nounMap = [
            'current_week' => 'week', 'previous_week' => 'week',
            'current_month' => 'month', 'previous_month' => 'month',
            'current_quarter' => 'quarter', 'previous_quarter' => 'quarter',
            'current_year' => 'year', 'previous_year' => 'year',
        ];
        $isCustom = !isset($nounMap[$period]);
        $noun = $nounMap[$period] ?? null;
        $todayStr = date('Y-m-d');
        $fromStr = substr($dateRange['from'] ?? '', 0, 10);
        $toStr = substr($dateRange['to'] ?? '', 0, 10);

        if ($isCustom) {
            $isComplete = $toStr !== '' && $toStr < $todayStr;
            $fromDisp = $fromStr !== '' ? date('M j', strtotime($fromStr)) : '';
            $toDisp = $toStr !== '' ? date('M j, Y', strtotime($toStr)) : '';
            $sameDay = ($fromStr !== '' && $fromStr === $toStr);
            $lead = $sameDay ? "On {$fromDisp}" : "Between {$fromDisp} and {$toDisp}";
            return [
                'is_custom' => true, 'is_complete' => $isComplete, 'is_first_day' => false, 'noun' => null,
                'month_name' => null, 'intro' => null, 'lead' => $lead,
                'mid_ref' => $sameDay ? "on {$fromDisp}" : "in that date range",
            ];
        }

        $isComplete = str_starts_with($period, 'previous_');
        $isFirstDay = (!$isComplete && $todayStr === $fromStr);
        // Only meaningful for the month bucket ("August is underway...") —
        // computed from the period's own start date, not "now", so a
        // previous_month view correctly names July while viewed in August.
        $monthName = ($noun === 'month') ? date('F', strtotime($fromStr ?: 'now')) : null;

        if ($isComplete) {
            return [
                'is_custom' => false, 'is_complete' => true, 'is_first_day' => false, 'noun' => $noun,
                'month_name' => $monthName, 'intro' => null, 'lead' => "Last {$noun}", 'mid_ref' => "last {$noun}",
            ];
        }
        if ($isFirstDay) {
            return [
                'is_custom' => false, 'is_complete' => false, 'is_first_day' => true, 'noun' => $noun,
                'month_name' => $monthName,
                'intro' => "Today is the first day of your trading {$noun}.", 'lead' => null,
                'mid_ref' => "this {$noun}",
            ];
        }
        return [
            'is_custom' => false, 'is_complete' => false, 'is_first_day' => false, 'noun' => $noun,
            'month_name' => $monthName, 'intro' => null, 'lead' => "So far this {$noun}", 'mid_ref' => "this {$noun}",
        ];
    }
}

if (!function_exists('colexPickVariant')) {
    /**
     * Same deterministic-variety idiom already established elsewhere in
     * this codebase (api/analytics/progressive_insights_engine.php's
     * ProgressiveInsightsEngine::pickVariant()) — picks one of several
     * equivalent phrasings from a real, changed input (not randomness),
     * so a repeated view of the SAME real situation stays stable within
     * a session, but a genuinely different day/trade-count/situation can
     * surface different, equally-natural wording.
     */
    function colexPickVariant(array $variants, string $seed): string {
        if (count($variants) === 1) return $variants[0];
        $idx = abs(crc32($seed)) % count($variants);
        return $variants[$idx];
    }
}

if (!function_exists('colexSignedMoney')) {
    /**
     * Formats a dollar amount with the sign OUTSIDE the "$" — "-$450.00",
     * never the "$-450.00" that a naive `'$' . number_format($negative,
     * 2)` concatenation produces (number_format() puts its own minus
     * sign first, so gluing "$" in front of that lands the currency
     * symbol before the sign). Found this exact bug in several chat.php
     * generators during the 2026-08-10 exhaustive audit — centralized
     * here so it can't recur at a future call site. Pass $withPlus=true
     * to also prefix a real gain with "+" (for a delta/comparison
     * figure, e.g. "+$120.00"); left false (default) for a plain
     * magnitude display where a bare positive number needs no sign.
     */
    function colexSignedMoney($amount, $withPlus = false) {
        $amount = (float)$amount;
        $abs = number_format(abs($amount), 2);
        if ($amount < 0) return "-\${$abs}";
        if ($withPlus && $amount > 0) return "+\${$abs}";
        return "\${$abs}";
    }
}

if (!function_exists('colexOpeningTradeCount')) {
    /**
     * Builds the natural "you've taken N trades..." opening clause from a
     * colexPeriodPosition() result — reused by every generator that opens
     * by reporting how many trades happened this period. Handles zero/one/
     * many trades and first-day/ongoing/complete/custom phrasing. Returns
     * a complete sentence (ending in ".") for the zero-trade case; for a
     * nonzero count, returns an unterminated clause ending in "...so far"
     * or "...trades" so the caller appends ", with a X% win rate. ..."
     * directly (matches the exact style of the approved benchmark
     * examples — one flowing sentence, not two choppy ones).
     *
     * Requested directly (2026-08-10), same day as the original benchmark:
     * don't repeat the identical sentence shape across Week/Month/Quarter/
     * Year — "So far this week...", "So far this month...", "So far this
     * quarter..." back to back reads templated even though each one is
     * individually natural. Several real phrasing variants exist per
     * bucket now, picked via colexPickVariant() from a seed built from the
     * real inputs (bucket + trade count + today's date) — the wording
     * changes when the real situation changes, not on every page load.
     */
    function colexOpeningTradeCount($pos, $totalTrades) {
        $noun = $pos['noun'] ?? 'period';
        $monthName = $pos['month_name'] ?? 'this month';
        $seed = ($pos['noun'] ?? 'custom') . '|' . ($pos['is_first_day'] ? 'first' : ($pos['is_complete'] ? 'complete' : 'ongoing')) . '|' . $totalTrades . '|' . date('Y-m-d');

        if ($totalTrades === 0) {
            if ($pos['is_first_day']) {
                return "Today is the first day of your trading {$noun}, and you haven't logged any trades yet.";
            }
            if ($pos['is_complete']) {
                return "You didn't log any trades {$pos['mid_ref']}.";
            }
            if ($pos['is_custom']) {
                return "{$pos['lead']}, you haven't logged any trades.";
            }
            return "You haven't taken any trades yet {$pos['mid_ref']}.";
        }

        $tradeWord = $totalTrades === 1 ? 'trade' : 'trades';

        if ($pos['is_first_day']) {
            $variants = [
                "{$pos['intro']} You've taken {$totalTrades} {$tradeWord} so far",
                "Your trading {$noun} just opened, and you've already logged {$totalTrades} {$tradeWord}",
                "The {$noun} is just getting started, {$totalTrades} {$tradeWord} in so far",
            ];
            return colexPickVariant($variants, $seed);
        }
        if ($pos['is_complete']) {
            $variants = [
                "{$pos['lead']}, you took {$totalTrades} {$tradeWord}",
                "Looking back at last {$noun}, you logged {$totalTrades} {$tradeWord}",
            ];
            return colexPickVariant($variants, $seed);
        }
        if ($pos['is_custom']) {
            // A custom range's lead is date-based with no "so far" baked
            // in, so it's still added here — it's the only place that
            // "still ongoing" information appears for a custom view.
            return "{$pos['lead']}, you've taken {$totalTrades} {$tradeWord} so far";
        }

        // Ongoing named period (current_* not-first-day) — real, distinct
        // phrasing per noun rather than one template with the noun
        // swapped in, per the explicit "don't just change the word 'week'
        // to 'month' and call that timeframe-aware" instruction.
        $variantsByNoun = [
            'week' => [
                "So far this week, you've taken {$totalTrades} {$tradeWord}",
                "You're {$totalTrades} {$tradeWord} into the week so far",
                "This week is shaping up with {$totalTrades} {$tradeWord} so far",
            ],
            'month' => [
                "So far this month, you've taken {$totalTrades} {$tradeWord}",
                "{$monthName} is underway with {$totalTrades} {$tradeWord} so far",
                "You're {$totalTrades} {$tradeWord} into {$monthName} so far",
            ],
            'quarter' => [
                "So far this quarter, you've taken {$totalTrades} {$tradeWord}",
                "Your quarter is currently sitting at {$totalTrades} {$tradeWord}",
                "{$totalTrades} {$tradeWord} into the quarter so far",
            ],
            'year' => [
                "So far this year, you've taken {$totalTrades} {$tradeWord}",
                "Across the year so far, you've taken {$totalTrades} {$tradeWord}",
                "Your year currently stands at {$totalTrades} {$tradeWord}",
            ],
        ];
        $variants = $variantsByNoun[$noun] ?? ["{$pos['lead']}, you've taken {$totalTrades} {$tradeWord}"];
        return colexPickVariant($variants, $seed);
    }
}

if (!function_exists('getPeriodStats')) {
    function getPeriodStats($db, $userId, $dateFrom, $dateTo) {
        $stats = [
            'total_trades' => 0,
            'active_accounts' => 0,
            'pnl_percentage' => 0,
            'pnl_direction' => 'neutral',
            'win_rate' => 0,
            'alert_count' => 0,
            'discipline_score' => 0,
            'drawdown_percentage' => 0,
            'revenge_trades' => 0
        ];

        // Rewritten (2026-08-08) to delegate to AnalyticsEngine::getUserMetrics()
        // rather than maintain its own independent SQL — the prior version
        // had four real, confirmed bugs
        // (docs/architecture/SINGLE_SOURCE_OF_TRUTH_AUDIT.md §16 Consolidated
        // #5), all closed by delegation rather than patched in place (a fifth,
        // still-independent implementation would only be able to drift again):
        // (1) no status='closed' filter — open/draft trades counted toward
        // total_trades/win_rate; (2) no is_validation filter — practice/
        // validation trades leaked into real stats; (3) a direct P&L Rule
        // violation — checked broker_pnl before challenge_pnl for EVERY
        // account regardless of type, instead of the account-type-aware
        // cascade the rest of the app uses; (4) a real double-count — summing
        // broker_pnl and challenge_pnl independently and adding them together
        // double-counts any prop-firm trade that has both populated (the
        // documented normal case once challenge_pnl is backfilled).
        // getUserMetrics() already gets all four right.
        try {
            // 2026-08-12 workflow-isolation audit: neither getUserMetrics()
            // (default validationScope='exclude') nor the alert_count query
            // below were workflow-aware — a validating user with no
            // main-workflow trade history got a permanently-empty Colex
            // stats widget instead of one reflecting their real, current
            // (validation) trades. Scoped to whichever workflow is actually
            // active, same pattern as every other trade query in this app.
            $colexStatsValidationScope = ValidationService::getWorkflowState($userId, $db) === 'validation' ? 'only' : 'exclude';
            $metrics = AnalyticsEngine::getUserMetrics($userId, [
                'dateFrom' => $dateFrom,
                'dateTo' => $dateTo,
                'validationScope' => $colexStatsValidationScope,
            ]);

            $stats['total_trades'] = (int)($metrics['total_trades'] ?? 0);
            $stats['win_rate'] = round(((float)($metrics['win_rate'] ?? 0)) * 100, 1);

            $accounts = AnalyticsEngine::getUserAccounts($userId, true);
            $stats['active_accounts'] = count($accounts);

            // Requested directly (2026-08-10): read these two straight from
            // AnalyticsEngine::getUserMetrics()'s own already-correct
            // fields instead of re-deriving them locally — this file
            // previously computed "P&L as % of CURRENT active-account
            // balance" (pnl_percentage) and "worst single trade as % of
            // that same balance" (drawdown_percentage, despite the name —
            // not a real drawdown), independent formulas that could
            // disagree with the app's real return_pct (net_pnl / STARTING
            // balance — the same denominator every other page's "return"
            // figure uses) and real current_drawdown_pct (the actual
            // peak-to-trough equity drawdown every other page means by
            // "drawdown"). Both getUserAccounts() calls already scope to
            // active accounts only (activeOnly=true, the default on both
            // sides), so there was never a genuine account-scope
            // difference justifying a second computation — only the
            // definition did, and getUserMetrics()'s is the one every
            // other page already treats as correct.
            $stats['pnl_percentage'] = round(((float)($metrics['return_pct'] ?? 0)) * 100, 2);
            $stats['pnl_direction'] = $stats['pnl_percentage'] > 0 ? 'positive' : ($stats['pnl_percentage'] < 0 ? 'negative' : 'neutral');
            $stats['drawdown_percentage'] = round(abs((float)($metrics['current_drawdown_pct'] ?? 0)) * 100, 2);

            // Revenge trades — real trade_classification counts, given the
            // same status='closed' + is_validation discipline (now
            // workflow-aware, see $colexStatsValidationScope above) as the
            // trade count above so this can never disagree with it.
            $alertValidationCond = $colexStatsValidationScope === 'only' ? 'is_validation = 1' : '(is_validation = 0 OR is_validation IS NULL)';
            $alertData = $db->raw("
                SELECT COUNT(*) as alert_count
                FROM trades
                WHERE user_id = ?
                AND captured_at BETWEEN ? AND ?
                AND status = 'closed'
                AND {$alertValidationCond}
                AND trade_classification = 'revenge_trade'
            ", [$userId, $dateFrom, $dateTo]);

            if (!empty($alertData)) {
                $stats['alert_count'] = (int)$alertData[0]['alert_count'];
                $stats['revenge_trades'] = $stats['alert_count'];
            }

        } catch (Exception $e) {
            error_log("Error fetching period stats: " . $e->getMessage());
        }

        return $stats;
    }
}

if (!function_exists('generateGreeting')) {
    // $displayName/$timezone come from the same user_profiles row every
    // other page reads (display_name — set via onboarding and/or Settings,
    // resolved through RenderHelpers.php's resolveDisplayName(); timezone —
    // set via Settings) — never a Colex-specific identity/clock. Both
    // default to the same fallbacks used elsewhere (a bare "Trader" for no
    // display name, RenderHelpers.php's own APP_TIMEZONE fallback for no
    // configured timezone) so an old caller that hasn't been updated yet
    // still works.
    function generateGreeting($stats, $period = '', $dateRange = [], $displayName = 'Trader', $timezone = null) {
        $timeGreeting = greetingWord($timezone);

        // Period-aware phrasing (2026-08-10, revised same day after live
        // feedback) — was a hardcoded "this period" regardless of which
        // selector (Current Week/Month/Quarter/Year/Custom) was actually
        // active, the same exposes-the-mechanic-not-the-meaning issue
        // already fixed throughout the chat responses. Reuses
        // colexPeriodPosition()'s real mid-sentence reference ("this
        // week"/"last month"/"in that date range"/etc.) instead of a
        // second, independent phrase.
        $pos = colexPeriodPosition($period, $dateRange);
        $midRef = $pos['mid_ref'] ?? 'this period';

        // "active account(s)" — a literal singular/plural marker left in
        // the string, exactly the database/report-generated tell this
        // whole rebuild exists to remove elsewhere. Real pluralization
        // instead, and the account/trade counts folded into one flowing
        // sentence ("You have 99 active accounts and have executed 6
        // trades so far this week") rather than two choppy ones grafted
        // together with the period only mentioned in the second.
        $accounts = (int)($stats['active_accounts'] ?? 0);
        $trades = (int)($stats['total_trades'] ?? 0);
        $accountsWord = $accounts === 1 ? 'active account' : 'active accounts';
        $tradesWord = $trades === 1 ? 'trade' : 'trades';
        $seed = $accounts . '|' . $trades . '|' . $midRef;

        $intro = '';
        if (!empty($pos['is_first_day'])) {
            $intro = ($pos['intro'] ?? '') . ' ';
            $body = "You have {$accounts} {$accountsWord} and have taken {$trades} {$tradesWord} today.";
        } elseif (!empty($pos['is_complete'])) {
            $variants = [
                "You have {$accounts} {$accountsWord} and took {$trades} {$tradesWord} {$midRef}.",
                "Across your {$accounts} {$accountsWord}, {$trades} {$tradesWord} closed {$midRef}.",
            ];
            $body = colexPickVariant($variants, $seed);
        } elseif (!empty($pos['is_custom'])) {
            $body = "You have {$accounts} {$accountsWord} and have taken {$trades} {$tradesWord} {$midRef}.";
        } else {
            $variants = [
                "You have {$accounts} {$accountsWord} and have executed {$trades} {$tradesWord} so far {$midRef}.",
                "You have {$accounts} {$accountsWord}, {$trades} {$tradesWord} taken {$midRef} so far.",
            ];
            $body = colexPickVariant($variants, $seed);
        }

        // Period already established by $body above — restating "this
        // week"/"this quarter" a second sentence later in a row reads
        // templated, so the P&L clause stays plain here (matches the
        // "vary naturally, don't repeat the same structure" principle).
        $pnlText = '';
        if ($stats['pnl_percentage'] != 0) {
            $direction = $stats['pnl_percentage'] > 0 ? 'up' : 'down';
            $pct = abs($stats['pnl_percentage']);
            $variants = [
                " You're {$direction} {$pct}%.",
                " That's {$direction} {$pct}% in P&L.",
            ];
            $pnlText = colexPickVariant($variants, $stats['total_trades'] . '|' . $stats['pnl_percentage'] . '|' . $midRef);
        }

        return [
            'title' => $timeGreeting . ', ' . $displayName,
            'subtitle' => $intro . $body . $pnlText
        ];
    }
}

if (!function_exists('generateContextCards')) {
    function generateContextCards($stats) {
        $cards = [];

        $cards[] = [
            'label' => 'Total Trades',
            'value' => number_format($stats['total_trades']),
            'color_class' => ''
        ];

        $cards[] = [
            'label' => 'Active Accounts',
            'value' => $stats['active_accounts'],
            'color_class' => ''
        ];

        $cards[] = [
            'label' => 'Period P&L',
            'value' => ($stats['pnl_percentage'] >= 0 ? '+' : '') . $stats['pnl_percentage'] . '%',
            'color_class' => $stats['pnl_direction']
        ];

        $cards[] = [
            'label' => 'Behavior Alerts',
            'value' => $stats['alert_count'],
            'color_class' => $stats['alert_count'] > 0 ? 'negative' : ''
        ];

        return $cards;
    }
}

if (!function_exists('colexTimeframeBucket')) {
    /**
     * Maps a period selector + resolved date range to one of the four
     * coaching "depth" buckets the suggested-prompt bank is organized
     * around: week (immediate/tactical), month (pattern recognition),
     * quarter (deeper performance analysis), year (career-level
     * reflection). A custom range has no natural bucket of its own — its
     * depth is inferred from how many real days it spans, since a 3-day
     * custom range calls for the same immediacy as a week, while a
     * 200-day one calls for year-level reflection.
     */
    function colexTimeframeBucket($period, $dateRange) {
        $map = [
            'current_week' => 'week', 'previous_week' => 'week',
            'current_month' => 'month', 'previous_month' => 'month',
            'current_quarter' => 'quarter', 'previous_quarter' => 'quarter',
            'current_year' => 'year', 'previous_year' => 'year',
        ];
        if (isset($map[$period])) return $map[$period];

        $from = strtotime(substr($dateRange['from'] ?? '', 0, 10));
        $to = strtotime(substr($dateRange['to'] ?? '', 0, 10));
        $days = ($from && $to) ? max(1, round(($to - $from) / 86400) + 1) : 7;
        if ($days <= 10) return 'week';
        if ($days <= 45) return 'month';
        if ($days <= 120) return 'quarter';
        return 'year';
    }
}

if (!function_exists('colexTimeframeFloor')) {
    /**
     * Per-timeframe floor — what counts as "too few to draw a real
     * conclusion from" scales with how much trading volume that
     * timeframe naturally implies. Judgment calls, not derived from any
     * statistical test — chosen to sit in the same rough range as this
     * app's other established n-aware gates (e.g. Reports'/Trading
     * Archive's 3/5/10/20-trade floors). Extracted (2026-08-10) so every
     * caller that needs "is this sample big enough to say something"
     * reads the same numbers — colexClassifyTraderSituation() below and
     * generateComparisonResponse()'s (api/colex/chat.php) small-sample
     * caveat both use this, instead of each keeping its own copy that
     * could quietly drift apart.
     */
    function colexTimeframeFloor($bucket) {
        $floors = ['week' => 3, 'month' => 5, 'quarter' => 10, 'year' => 20];
        return $floors[$bucket] ?? 5;
    }
}

if (!function_exists('colexClassifyTraderSituation')) {
    /**
     * Classifies where the trader stands this period into one bucket the
     * suggested-prompt bank is keyed on. Sample-size adequacy is checked
     * FIRST and overrides every performance signal below it — a trader
     * with 2 trades this quarter shouldn't be told their performance is
     * "strong" or "poor," the honest answer is there isn't enough
     * evidence yet (same n-aware philosophy already established
     * throughout this codebase). Built entirely on getPeriodStats()'s own
     * already-SSOT-sourced fields (win_rate, pnl_direction,
     * drawdown_percentage, revenge_trades — NOT discipline_score, which
     * is never actually computed, always 0).
     */
    function colexClassifyTraderSituation($stats, $bucket) {
        $n = (int)($stats['total_trades'] ?? 0);
        if ($n === 0) return 'no_trades';

        if ($n < colexTimeframeFloor($bucket)) return 'very_small_sample';

        $winRate = (float)($stats['win_rate'] ?? 0);
        $pnlDir = $stats['pnl_direction'] ?? 'neutral';
        $revengeTrades = (int)($stats['revenge_trades'] ?? 0);
        $drawdownPct = (float)($stats['drawdown_percentage'] ?? 0);

        if ($revengeTrades >= 2 || $drawdownPct >= 10) return 'repeated_mistakes';
        if ($winRate >= 50 && $pnlDir === 'negative') return 'high_wr_poor_pnl';
        if ($winRate < 40 && $pnlDir === 'positive') return 'low_wr_good_pnl';
        if ($winRate >= 50 && $pnlDir === 'positive') return 'strong';
        if ($winRate < 40 && $pnlDir === 'negative') return 'poor';
        return 'mixed';
    }
}

if (!function_exists('colexSuggestedPromptBank')) {
    /**
     * The real prompt inventory colexSuggestedPromptsFor() selects from —
     * organized [timeframe bucket][trader situation] => candidate prompt
     * strings, each bucket's wording pitched at that timeframe's own
     * natural coaching depth (week = immediate/tactical, month = pattern
     * recognition, quarter = deeper performance analysis, year =
     * career-level reflection) rather than the same sentence with the
     * noun swapped. Several real options per cell so two visits to the
     * same real situation don't always show the identical set (picked via
     * colexPickVariant() at selection time, not here).
     */
    function colexSuggestedPromptBank() {
        static $bank = null;
        if ($bank !== null) return $bank;

        $bank = [
            'week' => [
                'no_trades' => [
                    'How can I prepare for this week?',
                    'What should I focus on before my first trade?',
                    'Review my strategy before I start trading.',
                ],
                'very_small_sample' => [
                    "It's early in the week. What should I watch for?",
                    'What should I keep in mind with only a few trades logged so far?',
                    'Is there anything worth adjusting before more trades come in?',
                ],
                'repeated_mistakes' => [
                    'How can I stop repeating this mistake?',
                    'Which rule am I breaking most often?',
                    "What's triggering these repeated errors?",
                ],
                'high_wr_poor_pnl' => [
                    "Why am I winning often but not making money this week?",
                    "What's happening with my risk:reward this week?",
                    'Am I cutting winners short?',
                ],
                'low_wr_good_pnl' => [
                    'Why does this keep working despite the low win rate?',
                    'Am I protecting this edge well enough this week?',
                    'Should I be more selective with entries?',
                ],
                'strong' => [
                    'How can I maintain this consistency?',
                    "What's working best for me right now?",
                    'What should I avoid changing?',
                ],
                'poor' => [
                    'What is hurting my performance this week?',
                    "Show me where I'm going wrong.",
                    'What should I change before my next trade?',
                ],
                'mixed' => [
                    'How is my week trending overall?',
                    'What should I focus on for the rest of the week?',
                    "Where's the biggest opportunity to improve right now?",
                ],
            ],
            'month' => [
                'no_trades' => [
                    'How can I prepare for this month?',
                    'What should I focus on before trading picks up?',
                    'Review my strategy before this month gets going.',
                ],
                'very_small_sample' => [
                    "It's early in the month. What should I be watching for?",
                    'What patterns should I start tracking this month?',
                    "Too early to call a trend. What should I focus on instead?",
                ],
                'repeated_mistakes' => [
                    'What mistake keeps showing up this month?',
                    'Which rule am I breaking most often this month?',
                    'Is there a pattern behind these repeated errors?',
                ],
                'high_wr_poor_pnl' => [
                    "Why am I winning often but not making money this month?",
                    "What's the pattern behind my risk:reward this month?",
                    'Am I letting losers run too far?',
                ],
                'low_wr_good_pnl' => [
                    "What's making this edge work despite the low win rate?",
                    'Am I protecting this pattern well enough?',
                    'Is this payoff structure sustainable?',
                ],
                'strong' => [
                    "What's driving this month's consistency?",
                    'Is this performance sustainable?',
                    'What pattern is behind this strong month?',
                ],
                'poor' => [
                    'What pattern is hurting my performance this month?',
                    "What's the biggest issue this month?",
                    'Where is this month going wrong?',
                ],
                'mixed' => [
                    'What pattern stands out this month?',
                    'How is this month trending overall?',
                    'What should I watch for the rest of the month?',
                ],
            ],
            'quarter' => [
                'no_trades' => [
                    'What should I focus on heading into this quarter?',
                    'Review my strategy before this quarter gets underway.',
                    'What would set this quarter up well?',
                ],
                'very_small_sample' => [
                    "It's early in the quarter. What's worth tracking?",
                    "Too early for a real read on the quarter. What should I focus on instead?",
                ],
                'repeated_mistakes' => [
                    "What's the recurring issue this quarter?",
                    'Which rule am I breaking most often this quarter?',
                    'Is there a deeper pattern behind these mistakes?',
                ],
                'high_wr_poor_pnl' => [
                    "What's driving the gap between my win rate and P&L this quarter?",
                    'Is my risk:reward structurally off this quarter?',
                ],
                'low_wr_good_pnl' => [
                    "What's made this edge work this quarter despite the win rate?",
                    'Is this payoff structure something I can rely on?',
                ],
                'strong' => [
                    "What's been the biggest strength this quarter?",
                    "What's changed for the better this quarter?",
                    'Is this level of performance sustainable?',
                ],
                'poor' => [
                    'What has changed in my trading this quarter?',
                    "What's the real driver behind this quarter's results?",
                    'Where did this quarter go wrong?',
                ],
                'mixed' => [
                    'What has changed in my trading this quarter?',
                    'What stands out most about this quarter?',
                ],
            ],
            'year' => [
                'no_trades' => [
                    'What would you like to focus on this year?',
                    'Review my strategy before the year gets underway.',
                ],
                'very_small_sample' => [
                    "It's early in the year. What's worth focusing on?",
                    'Too early for a real career-level read. What matters most right now?',
                ],
                'repeated_mistakes' => [
                    'What mistake has defined this year so far?',
                    'Which rule am I breaking most often this year?',
                ],
                'high_wr_poor_pnl' => [
                    "What's the structural gap between my win rate and P&L this year?",
                    'Has my risk:reward been a recurring issue this year?',
                ],
                'low_wr_good_pnl' => [
                    "What's made this edge work all year despite the win rate?",
                    'Is this payoff structure the foundation of my edge?',
                ],
                'strong' => [
                    'What has been my biggest performance driver this year?',
                    "What's made this year work?",
                    'Is this level of performance repeatable next year?',
                ],
                'poor' => [
                    "What has been the biggest drag on this year's results?",
                    'What needs to change going into next year?',
                ],
                'mixed' => [
                    'What has been my biggest performance driver this year?',
                    'How has this year trended overall?',
                ],
            ],
        ];
        return $bank;
    }
}

if (!function_exists('generateSuggestedPrompts')) {
    /**
     * Requested directly (2026-08-10): the clickable prompts under Colex's
     * summary should read like "the next useful conversation to have,"
     * not a fixed list of generic questions bolted to the page. Selection
     * considers the selected timeframe, trade count, win rate, P&L,
     * drawdown, and repeated-mistake signals via
     * colexClassifyTraderSituation() (sample-size adequacy gates first —
     * a thin sample never gets diagnosed as "good" or "bad," per the
     * explicit instruction not to pretend there's enough evidence when
     * there isn't), then picks a small (max 3), varied set from
     * colexSuggestedPromptBank()'s matching cell via colexPickVariant()
     * — real rotation from a seed built off the real inputs, not
     * randomness, so the same real situation stays stable within a
     * session but genuinely changes when the real numbers do.
     *
     * $period/$dateRange are optional (default to the always-real
     * current_week shape) purely for call-site safety — both real call
     * sites (api/colex/stats.php, public/colex.php) always pass them.
     */
    function generateSuggestedPrompts($stats, $period = 'current_week', $dateRange = null) {
        $dateRange = $dateRange ?? ['from' => date('Y-m-d 00:00:00'), 'to' => date('Y-m-d 23:59:59')];
        $bucket = colexTimeframeBucket($period, $dateRange);
        $situation = colexClassifyTraderSituation($stats, $bucket);

        $taskTypeBySituation = [
            'no_trades' => 'general',
            'very_small_sample' => 'performance',
            'repeated_mistakes' => 'journal',
            'high_wr_poor_pnl' => 'performance',
            'low_wr_good_pnl' => 'performance',
            'strong' => 'performance',
            'poor' => 'journal',
            'mixed' => 'performance',
        ];
        $taskType = $taskTypeBySituation[$situation] ?? 'general';

        $bank = colexSuggestedPromptBank();
        $candidates = $bank[$bucket][$situation] ?? $bank[$bucket]['mixed'] ?? [];
        if (empty($candidates)) {
            // Defensive fallback only — every real bucket/situation
            // combination above is populated, so this should never
            // actually fire against real data.
            $candidates = ['Analyze my trading performance'];
        }

        // Seeded rotation: changes only when the real inputs change (a
        // new day, a new trade closing, a different situation) — not on
        // every page load.
        $seed = $bucket . '|' . $situation . '|' . (int)($stats['total_trades'] ?? 0) . '|' . date('Y-m-d');
        $count = count($candidates);
        $take = min(3, $count);
        if ($count > $take) {
            $offset = abs(crc32($seed)) % $count;
            $candidates = array_slice(array_merge(array_slice($candidates, $offset), array_slice($candidates, 0, $offset)), 0, $take);
        }

        $prompts = [];
        foreach ($candidates as $text) {
            $prompts[] = ['text' => $text, 'task_type' => $taskType];
        }
        return $prompts;
    }
}