<?php
/**
 * Strategy Change History — TRADERALIGN STRATEGY BUSINESS RULES redesign
 * (2026-08-11). Source of truth: "TRADERALIGN STRATEGY BUSINESS RULES.docx".
 *
 * Core model this file supports: ONE strategy_versions row per strategy,
 * updated in place on every edit (api/strategy/save.php). Editing never
 * creates a new version row or a new strategy — it writes a
 * strategy_change_events row (one per save) plus one strategy_change_fields
 * row per meaningfully-changed field. This class is the single place that:
 *   - decides which fields are "meaningful" (§4 — not name/description)
 *   - diffs old vs new declaration and builds the event+field rows
 *   - reads that history back (fetch/narrate/segment trades by boundary)
 *
 * Deliberately NOT used for is_validation=1 (practice/validation) strategies
 * — per explicit instruction, that domain is untouched and left for a
 * dedicated future redesign. Callers must not invoke this for a validation
 * strategy; api/strategy/save.php enforces that at the call site.
 */

class StrategyChangeHistory
{
    /**
     * Meaningful scalar fields on strategy_versions — a change to any of
     * these produces a strategy_change_fields row. Name/description/
     * trading_style live on `strategies`, not `strategy_versions`; trading
     * style IS tracked (it's a real declared characteristic — affects
     * checklist expiry windows and style-drift analytics elsewhere in the
     * app) but is diffed separately since it's handled by a different
     * caller (api/strategy/save.php touches the `strategies` row and the
     * `strategy_versions` row in two different statements).
     */
    private const SCALAR_FIELDS = [
        'max_risk_per_trade_pct' => 'Max Risk Per Trade %',
        'backtested_avg_rr'      => 'Reward:Risk',
        'backtested_win_rate'    => 'Backtested Win Rate %',
        'max_losing_streak'      => 'Max Losing Streak',
        'max_trades_per_day'     => 'Max Trades Per Day',
        'max_trades_per_week'    => 'Max Trades Per Week',
        'move_to_be_rule'        => 'Move-to-Breakeven Rule',
    ];

    /** Array-valued fields — diffed by JSON-encoded before/after. */
    private const ARRAY_FIELDS = [
        'session_preference' => 'Session',
        'instruments_traded' => 'Instrument',
        'timeframes_traded'  => 'Timeframe',
        'setup_criteria'     => 'Setup Criteria',
        'entry_triggers'     => 'Entry Triggers',
        'sl_rules'           => 'Stop-Loss Rules',
        'tp_rules'           => 'Take-Profit Rules',
    ];

    /** Human label for a field_name value (scalar, array, checklist_*, or trading_style). */
    public static function fieldLabel(string $fieldName): string
    {
        if (isset(self::SCALAR_FIELDS[$fieldName])) return self::SCALAR_FIELDS[$fieldName];
        if (isset(self::ARRAY_FIELDS[$fieldName]))  return self::ARRAY_FIELDS[$fieldName];
        if ($fieldName === 'trading_style') return 'Trading Style';
        $checklistLabels = [
            'checklist_setup'     => 'Setup Checklist',
            'checklist_entry'     => 'Entry Confirmation',
            'checklist_risk'      => 'Risk Checklist',
            'checklist_execution' => 'Execution Checklist',
        ];
        return $checklistLabels[$fieldName] ?? ucwords(str_replace('_', ' ', $fieldName));
    }

    /**
     * Compares the strategy's OLD row state (as currently persisted) against
     * the NEW state about to be saved, and returns an array of
     * ['field' => ..., 'previous' => ..., 'new' => ...] for every field that
     * actually changed. Purely a diff — writes nothing.
     *
     * $oldStrategy/$newStrategy: ['trading_style' => ...] (only the tracked
     * subset needed). $oldVersion/$newVersion: associative arrays with the
     * SCALAR_FIELDS/ARRAY_FIELDS keys. $oldChecklist/$newChecklist: arrays
     * of ['category' => ..., 'text' => ...] rows (already-resolved display
     * text — polished_text falling back to raw_text).
     */
    public static function diff(array $oldStrategy, array $newStrategy, array $oldVersion, array $newVersion, array $oldChecklist, array $newChecklist): array
    {
        $changes = [];

        if (($oldStrategy['trading_style'] ?? null) !== ($newStrategy['trading_style'] ?? null)) {
            $changes[] = ['field' => 'trading_style', 'previous' => $oldStrategy['trading_style'] ?? null, 'new' => $newStrategy['trading_style'] ?? null];
        }

        foreach (array_keys(self::SCALAR_FIELDS) as $key) {
            $old = self::normalizeScalar($oldVersion[$key] ?? null);
            $new = self::normalizeScalar($newVersion[$key] ?? null);
            if ($old !== $new) {
                $changes[] = ['field' => $key, 'previous' => $old, 'new' => $new];
            }
        }

        foreach (array_keys(self::ARRAY_FIELDS) as $key) {
            $old = self::normalizeArray($oldVersion[$key] ?? []);
            $new = self::normalizeArray($newVersion[$key] ?? []);
            if ($old !== $new) {
                $changes[] = ['field' => $key, 'previous' => json_encode(array_values(json_decode($old, true) ?: [])), 'new' => json_encode(array_values(json_decode($new, true) ?: []))];
            }
        }

        // Checklist content, diffed per category (joined item text) rather
        // than per-item — matches the doc's own "Entry Confirmation
        // changed" framing (§5) without over-engineering per-item tracking.
        foreach (['setup', 'entry', 'risk', 'execution'] as $cat) {
            $oldText = self::checklistCategoryText($oldChecklist, $cat);
            $newText = self::checklistCategoryText($newChecklist, $cat);
            if ($oldText !== $newText) {
                $changes[] = ['field' => 'checklist_' . $cat, 'previous' => $oldText, 'new' => $newText];
            }
        }

        return $changes;
    }

    private static function normalizeScalar($v): ?string
    {
        if ($v === null || $v === '') return null;
        if (is_numeric($v)) return rtrim(rtrim(sprintf('%.6f', (float)$v), '0'), '.');
        return (string)$v;
    }

    private static function normalizeArray($v): string
    {
        $arr = is_array($v) ? $v : (json_decode((string)$v, true) ?: []);
        $arr = array_values(array_filter(array_map('strval', $arr), fn($x) => $x !== ''));
        sort($arr);
        return json_encode($arr);
    }

    private static function checklistCategoryText(array $items, string $category): string
    {
        $texts = [];
        foreach ($items as $it) {
            if (($it['category'] ?? '') !== $category) continue;
            $t = trim((string)($it['text'] ?? ''));
            if ($t !== '') $texts[] = $t;
        }
        sort($texts);
        return implode(' | ', $texts);
    }

    /**
     * Writes one strategy_change_events row + N strategy_change_fields rows
     * for a non-empty diff. No-op (returns null) if $changes is empty — a
     * save that changed nothing meaningful (e.g. only name/description)
     * does not create an event, per §4.
     */
    public static function record(PDO $pdo, string $strategyId, string $userId, array $changes): ?string
    {
        if (empty($changes)) return null;
        $eventId = ScopedDb::uuid();
        $stmt = $pdo->prepare('INSERT INTO strategy_change_events (id, strategy_id, user_id, changed_at) VALUES (:id, :sid, :uid, NOW())');
        $stmt->execute([':id' => $eventId, ':sid' => $strategyId, ':uid' => $userId]);

        $fieldStmt = $pdo->prepare('INSERT INTO strategy_change_fields (id, change_event_id, field_name, previous_value, new_value) VALUES (:id, :eid, :fn, :pv, :nv)');
        foreach ($changes as $c) {
            $fieldStmt->execute([
                ':id' => ScopedDb::uuid(), ':eid' => $eventId,
                ':fn' => $c['field'], ':pv' => $c['previous'], ':nv' => $c['new'],
            ]);
        }
        return $eventId;
    }

    /**
     * Every change event for a strategy, oldest first, each with its field
     * changes attached. Ownership is the caller's responsibility (pass a
     * strategy id already verified to belong to $userId).
     */
    public static function fetchEvents(ScopedDb $db, string $strategyId): array
    {
        $events = $db->raw(
            'SELECT id, changed_at FROM strategy_change_events WHERE strategy_id = :sid ORDER BY changed_at ASC, created_at ASC',
            [':sid' => $strategyId]
        );
        if (empty($events)) return [];

        $ids = array_column($events, 'id');
        $placeholders = implode(',', array_fill(0, count($ids), '?'));
        $fieldsStmt = $db->pdo()->prepare("SELECT change_event_id, field_name, previous_value, new_value FROM strategy_change_fields WHERE change_event_id IN ($placeholders)");
        $fieldsStmt->execute($ids);
        $byEvent = [];
        foreach ($fieldsStmt->fetchAll(PDO::FETCH_ASSOC) as $f) {
            $byEvent[$f['change_event_id']][] = $f;
        }

        foreach ($events as &$e) {
            $e['fields'] = $byEvent[$e['id']] ?? [];
        }
        unset($e);
        return $events;
    }

    /**
     * A short, data-driven narration of a strategy's change history — never
     * a hardcoded sentence, always built from the real events passed in.
     * Matches the tone of the doc's own worked examples (§20/§21).
     */
    public static function narrate(array $events, ?string $context = null): string
    {
        $n = count($events);
        if ($n === 0) return 'This strategy has not been changed since it was created.';

        $last = $events[$n - 1];
        $lastFields = $last['fields'] ?? [];
        $lastSummary = '';
        if (!empty($lastFields)) {
            $f = $lastFields[0];
            $label = self::fieldLabel($f['field_name']);
            $lastSummary = "The most recent change was {$label}, from " . self::displayValue($f['previous_value']) . ' to ' . self::displayValue($f['new_value']) . '.';
            if (count($lastFields) > 1) {
                $lastSummary = "The most recent change updated " . count($lastFields) . ' fields, including ' . $label . '.';
            }
        }

        $times = $n === 1 ? 'once' : "{$n} times";
        $contextPhrase = $context ? " {$context}" : '';
        return "This strategy has been changed {$times}.{$contextPhrase} {$lastSummary}";
    }

    /** Public — reused by Journal/Reports render code for their own narration sentences. */
    public static function displayValue(?string $v): string
    {
        if ($v === null || $v === '') return '(not set)';
        $decoded = json_decode($v, true);
        if (is_array($decoded)) return empty($decoded) ? '(none)' : implode(', ', $decoded);
        return $v;
    }

    /**
     * Splits a strategy's trades into segments bounded by its real change
     * events (§11/§30) — purely by comparing each trade's own timestamp
     * against each event's changed_at, never by strategy_version_id (which,
     * going forward, is stable per strategy and can't express boundaries).
     *
     * $trades: rows with a $timestampField datetime column (captured_at is
     * the app's own convention for "when this trade was taken under
     * whichever declaration was live").
     *
     * Returns N+1 segments (N = count($events)): segment 0 is "before the
     * first change", segment i is "after change i, before change i+1", and
     * the last segment is "after the most recent change" (or, with zero
     * events, one segment containing every trade — the strategy's whole
     * lifetime under its one, never-changed declaration).
     */
    public static function segmentTradesByChanges(array $trades, array $events, string $timestampField = 'captured_at'): array
    {
        $boundaries = array_map(fn($e) => $e['changed_at'], $events);
        $segments = array_fill(0, count($boundaries) + 1, []);

        foreach ($trades as $t) {
            $ts = $t[$timestampField] ?? null;
            $idx = count($boundaries);
            foreach ($boundaries as $i => $b) {
                if ($ts !== null && $ts < $b) { $idx = $i; break; }
            }
            $segments[$idx][] = $t;
        }
        return $segments;
    }
}
