<?php
require_once __DIR__ . '/../../config/config.php';
require_once __DIR__ . '/../../src/ScopedDb.php';
require_once __DIR__ . '/../../src/Auth.php';
require_once __DIR__ . '/../../src/RenderHelpers.php';
require_once __DIR__ . '/../../src/PropFirmRuleEngine.php';
require_once __DIR__ . '/../../src/ValidationService.php';

header('Content-Type: application/json');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['error' => 'Method not allowed']);
    exit;
}

if (session_status() === PHP_SESSION_NONE) {
    session_name(SESSION_NAME);
    session_start();
}

try {
    $dsn = "mysql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME . ";charset=utf8mb4";
    $pdo = new PDO($dsn, DB_USER, DB_PASS, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);
} catch (PDOException $e) {
    http_response_code(500);
    echo json_encode(['error' => 'Database connection failed']);
    exit;
}

$auth = new Auth($pdo);
$userId = $auth->requireAuth();
$db = new ScopedDb($pdo, $userId);

$input = json_decode(file_get_contents('php://input'), true);

if (!$input || empty($input['trade_lifecycle_id'])) {
    http_response_code(400);
    echo json_encode(['error' => 'Missing trade_lifecycle_id']);
    exit;
}

// Accept 'completed' as well as 'pending_calculator': when a trade breaches,
// the calculator's first save already flips this row to 'completed', then the
// breach popup fires a SECOND save (with the override reason) for the same
// trade_lifecycle_id. That second call must not be rejected here.
$stmt = $pdo->prepare("SELECT id FROM checklist_results WHERE trade_lifecycle_id = :tid AND user_id = :uid AND status IN ('pending_calculator', 'completed')");
$stmt->execute([':tid' => $input['trade_lifecycle_id'], ':uid' => $userId]);
if (!$stmt->fetch()) {
    http_response_code(400);
    echo json_encode(['error' => 'No pending checklist found. Tick the checklist first.']);
    exit;
}

// ──── Idempotent re-save guard ────
// If a trade row already exists for this trade_lifecycle_id (i.e. this is the
// popup's follow-up save after a breach), do NOT run the insert/balance/breach
// pipeline again — that would create a duplicate trade row and subtract the
// loss from current_balance a second time. Instead, just amend the existing
// row with whatever the popup collected and return.
$existingStmt = $pdo->prepare(
    "SELECT id FROM trades WHERE trade_lifecycle_id = :tid AND user_id = :uid AND deleted_at IS NULL LIMIT 1"
);
$existingStmt->execute([':tid' => $input['trade_lifecycle_id'], ':uid' => $userId]);
$existingTrade = $existingStmt->fetch();

if ($existingTrade) {
    $existingTradeId = $existingTrade['id'];

    // Only amend fields that exist on the trades table already (avoid guessing
    // at a column name for the popup's "reason" text — see note below).
    $updateFields = [];
    $updateParams = [':id' => $existingTradeId, ':uid' => $userId];

    if (array_key_exists('warnings_fired', $input)) {
        $updateFields[] = 'warnings_fired = :warnings_fired';
        $updateParams[':warnings_fired'] = json_encode($input['warnings_fired'] ?? []);
    }
    if (array_key_exists('blocker_fired', $input)) {
        $updateFields[] = 'blocker_fired = :blocker_fired';
        $updateParams[':blocker_fired'] = !empty($input['blocker_fired']) ? 1 : 0;
    }
    if (array_key_exists('user_overrode_blocker', $input)) {
        $updateFields[] = 'user_overrode_blocker = :user_overrode_blocker';
        $updateParams[':user_overrode_blocker'] = !empty($input['user_overrode_blocker']) ? 1 : 0;
    }
    if (array_key_exists('breach_override_reason', $input)) {
        $updateFields[] = 'breach_override_reason = :breach_override_reason';
        $updateParams[':breach_override_reason'] = $input['breach_override_reason'] ?? null;
    }
    if (array_key_exists('account_status_at_capture', $input)) {
        $updateFields[] = 'account_status_at_capture = :account_status_at_capture';
        $updateParams[':account_status_at_capture'] = $input['account_status_at_capture'] ?? 'breached';
    }

    if (!empty($updateFields)) {
        $updateSql = "UPDATE trades SET " . implode(', ', $updateFields) . " WHERE id = :id AND user_id = :uid";
        $updateStmt = $pdo->prepare($updateSql);
        $updateStmt->execute($updateParams);
    }

    echo json_encode([
        'success'  => true,
        'trade_id' => $existingTradeId,
        'message'  => 'Trade already saved; updated with the latest details.',
    ]);
    exit;
}

$draftCount = $db->count('trades', "status = 'draft' AND deleted_at IS NULL");
if ($draftCount >= 40) {
    http_response_code(400);
    echo json_encode(['error' => 'Maximum drafts reached. Clear some drafts first.']);
    exit;
}

$accountId = $input['account_id'] ?? null;
$accountType = null;
if ($accountId) {
    // 2026-08-19 security fix (workflow isolation): a trade must never be
    // created against an account that belongs to the OTHER workflow context
    // than the one the trader is currently in — this is the same
    // trading_accounts.workflow_context rule already enforced on account
    // creation/deposit/withdrawal/close (api/accounts/*.php), the account
    // picker (src/AccountPicker.php), and Trading Center's own account
    // queries, but this endpoint — the actual trade-creation write path —
    // had never been given the same check, confirmed via a full-repo grep
    // during a pre-launch security audit. selectById() alone only checks
    // user_id, not workflow_context, so it's replaced here with an explicit
    // workflow-scoped lookup.
    $draftWorkflowState = ValidationService::getWorkflowState($userId, $db);
    $acctRows = $db->raw(
        "SELECT * FROM trading_accounts WHERE id = :id AND user_id = :uid AND workflow_context = :wf AND deleted_at IS NULL LIMIT 1",
        [':id' => $accountId, ':uid' => $userId, ':wf' => $draftWorkflowState]
    );
    $acct = $acctRows[0] ?? null;
    if (!$acct) { http_response_code(403); echo json_encode(['error' => 'Account not found']); exit; }
    $accountType = $acct['account_type'] ?? null;
}

$strategyVersionId = $input['strategy_version_id'] ?? null;
$edgeSnapshot = null;
// Validation Workflow — the trade's own is_validation flag is derived
// HERE, server-side, from its attached strategy's is_validation flag, at
// the single moment a trade is created. Never trusted from the client
// (the calculator/JS never sends is_validation at all), never
// independently re-derived anywhere else — every downstream page
// (Dashboard/Journal/Drafts/Validation Tracker/Reports/AnalyticsEngine)
// only ever READS this stored flag, it never recomputes it.
//
// Real, pre-existing bug fixed here (found 2026-08-12, via a live user
// report — a real validation trade was invisible to Validation Tracker
// while visible in main-workflow Reports): this file previously never set
// trades.is_validation at all, so every trade defaulted to the column's
// DB default (0) regardless of its strategy — a validation-strategy trade
// was therefore indistinguishable from a main-workflow trade to every
// is_validation-scoped query. Confirmed live (DB-wide, before this fix):
// 5 real trades across 3 users, dating back to 2026-07-17 (long before
// the Validation Workflow feature's own session), were attached to a real
// is_validation=1 strategy but themselves carried is_validation=0 — see
// CLAUDE.md's Validation Workflow section for the full incident writeup
// and the one-time data correction applied to those 5 rows.
$isValidationTrade = 0;
if ($strategyVersionId) {
    $sv = $db->selectById('strategy_versions', $strategyVersionId);
    if (!$sv) { http_response_code(403); echo json_encode(['error' => 'Strategy version not found']); exit; }

    if (!empty($sv['strategy_id'])) {
        $parentStrategy = $db->selectById('strategies', $sv['strategy_id']);
        $isValidationTrade = (!empty($parentStrategy) && (int)($parentStrategy['is_validation'] ?? 0) === 1) ? 1 : 0;
    }

    // Capture strategy edge snapshot - complete mirror of strategy version at trade time
    $edgeSnapshot = json_encode([
        'max_risk_per_trade_pct' => $sv['max_risk_per_trade_pct'] ?? null,
        'backtested_avg_rr' => $sv['backtested_avg_rr'] ?? null,
        'instruments_traded' => $sv['instruments_traded'] ?? null,
        'session_preference' => $sv['session_preference'] ?? null,
        'backtested_win_rate' => $sv['backtested_win_rate'] ?? null,
        'max_losing_streak' => $sv['max_losing_streak'] ?? null,
        'max_trades_per_day' => $sv['max_trades_per_day'] ?? null,
        'max_trades_per_week' => $sv['max_trades_per_week'] ?? null,
        'avg_trades_per_week' => $sv['avg_trades_per_week'] ?? null,
        'setup_criteria' => $sv['setup_criteria'] ?? null,
        'entry_triggers' => $sv['entry_triggers'] ?? null,
        'sl_rules' => $sv['sl_rules'] ?? null,
        'tp_rules' => $sv['tp_rules'] ?? null,
        'partial_close_rules' => $sv['partial_close_rules'] ?? null,
        'move_to_be_rule' => $sv['move_to_be_rule'] ?? null,
        'strategy_version_id' => $strategyVersionId,
        'captured_at' => date('Y-m-d H:i:s')
    ]);
}

// ──── Determine trade_context based on account lifecycle_state ────
// Default to 'pre_breach' for normal active trades
$tradeContext = 'pre_breach';
$lifecycleState = null;

if ($accountId) {
    // Query the account's lifecycle_state from challenge_accounts
    $lifecycleStmt = $pdo->prepare(
        "SELECT lifecycle_state 
         FROM challenge_accounts 
         WHERE account_id = :aid 
         ORDER BY created_at DESC 
         LIMIT 1"
    );
    $lifecycleStmt->execute([':aid' => $accountId]);
    $lifecycleRow = $lifecycleStmt->fetch();
    
    if ($lifecycleRow) {
        $lifecycleState = $lifecycleRow['lifecycle_state'];
        
        // Set trade_context based on lifecycle_state
        if ($lifecycleState === 'breached') {
            $tradeContext = 'post_breach';
        } elseif (in_array($lifecycleState, ['phase_1_complete', 'phase_2_complete'])) {
            $tradeContext = 'post_completion';
        } elseif (in_array($lifecycleState, ['phase_1_active', 'phase_2_active', 'funded_active'])) {
            $tradeContext = 'pre_breach';
        }
        // For any other states, keep default 'pre_breach'
    }
}

// ──── No split/breach computation here — the calculator NEVER determines ────
// ──── breach or pass status on its own. ─────────────────────────────────────
// challenge_pnl / excess_pnl are computed exactly once, at Draft Outcome
// confirmation (dashboard.php's resolve_trade_id handler), from the trader's
// ACTUAL confirmed P&L — never from this trade's projection. They start NULL
// here and stay NULL until that confirmation happens.
//
// $tradeContext above already reflects the account's LAST-CONFIRMED
// lifecycle_state (read from challenge_accounts a few lines up) — that's the
// correct and only signal this file uses. It is exactly analogous to how the
// calculator reads last-confirmed current_balance rather than guessing it:
// the account only becomes 'breached' or 'phase_x_complete' once the trader
// explicitly confirms that outcome in Draft Outcome, and only THEN does the
// next calculator load (and next draft.php save) see it reflected here.
$challengePnl = null;
$excessPnl = null;

$tradeGroupId = ScopedDb::uuid();
$capturedAt = date('Y-m-d H:i:s');
$tradeLifecycleId = $input['trade_lifecycle_id'];
$mt5CommentHash = ScopedDb::mt5CommentHash($tradeLifecycleId, $accountId ?? '', $capturedAt);

$riskPerPosition = isset($input['risk_per_position']) ? (float)$input['risk_per_position'] : 0;
$maxPotentialProfit = isset($input['max_potential_profit']) ? (float)$input['max_potential_profit'] : 0;
$riskRewardTp1 = isset($input['risk_reward_tp1']) ? (float)$input['risk_reward_tp1'] : null;
$entryPrice = isset($input['entry_price']) ? (float)$input['entry_price'] : 0;
$slPrice = isset($input['sl_price']) ? (float)$input['sl_price'] : 0;
$tpPrice = isset($input['tp1_price']) ? (float)$input['tp1_price'] : (isset($input['tp_price']) ? (float)$input['tp_price'] : 0);
$tp2Price  = (isset($input['tp2_price'])  && $input['tp2_price']  !== null && $input['tp2_price']  !== '') ? (float)$input['tp2_price']  : null;
$tp3Price  = (isset($input['tp3_price'])  && $input['tp3_price']  !== null && $input['tp3_price']  !== '') ? (float)$input['tp3_price']  : null;
$tp4Price  = (isset($input['tp4_price'])  && $input['tp4_price']  !== null && $input['tp4_price']  !== '') ? (float)$input['tp4_price']  : null;
$tp5Price  = (isset($input['tp5_price'])  && $input['tp5_price']  !== null && $input['tp5_price']  !== '') ? (float)$input['tp5_price']  : null;
$tp6Price  = (isset($input['tp6_price'])  && $input['tp6_price']  !== null && $input['tp6_price']  !== '') ? (float)$input['tp6_price']  : null;
$tp7Price  = (isset($input['tp7_price'])  && $input['tp7_price']  !== null && $input['tp7_price']  !== '') ? (float)$input['tp7_price']  : null;
$tp8Price  = (isset($input['tp8_price'])  && $input['tp8_price']  !== null && $input['tp8_price']  !== '') ? (float)$input['tp8_price']  : null;
$tp9Price  = (isset($input['tp9_price'])  && $input['tp9_price']  !== null && $input['tp9_price']  !== '') ? (float)$input['tp9_price']  : null;
$tp10Price = (isset($input['tp10_price']) && $input['tp10_price'] !== null && $input['tp10_price'] !== '') ? (float)$input['tp10_price'] : null;
$riskPct = isset($input['risk_pct']) ? (float)$input['risk_pct'] : null;
$lotsPerPosition = isset($input['lots_per_position']) ? (float)$input['lots_per_position'] : null;
$recommendedLots = isset($input['recommended_lots']) ? (float)$input['recommended_lots'] : null;
$accountSizeAtEntry = isset($input['account_size_at_entry']) ? (float)$input['account_size_at_entry'] : null;
$balanceAtEntry = isset($input['balance_at_entry']) ? (float)$input['balance_at_entry'] : null;

// ── Drawdown-budget snapshot fields — computed server-side from
// PropFirmRuleEngine at save time, never trusted from the client. Per
// docs/architecture/SINGLE_SOURCE_OF_TRUTH_AUDIT.md §15 (Consolidated
// #3) and the MIGRATION_PLAN's Wave 2: calculator-source.html previously
// computed account_status_label/drawdown_at_entry_pct/
// breathing_room_used_pct/breathing_room_remaining/trades_remaining_buffer
// client-side from window._getPFContext()'s URL-query-string-sourced
// rule percentages (?max_dd_pct=, cached once per page load,
// window._pfContextCache) combined with the trader's own editable
// Current Balance form input — a structurally different, staleness-
// prone derivation path than the canonical phase-aware engine, capable
// of silently carrying pre-promotion rule percentages into a trade saved
// moments after a real phase promotion.
//
// Real, previously-undocumented data gap found while implementing this
// fix: only drawdown_at_entry_pct was ever actually captured by this
// endpoint (confirmed live: 310 of 370 real trades have it populated) —
// account_status_label/breathing_room_used_pct/breathing_room_remaining/
// trades_remaining_buffer were silently discarded on every single trade
// ever saved (0 of 370 populated), despite the client always sending
// them and despite drafts.php's own buildEntryFacts() having real,
// dormant-until-now display logic keyed on trades_remaining_buffer
// ("roughly N more trades of this size could be taken before reaching
// the account's drawdown limit") that could never have rendered for any
// real trade. This fix closes both the staleness risk and this capture
// gap at once.
//
// All five are computed here, from PropFirmRuleEngine's real
// overall_drawdown figures for this account, evaluated against THIS
// trade's own risk_per_position — never from $input. Personal/live
// accounts and prop-firm accounts with no resolvable challenge metrics
// keep all five null (no drawdown-budget concept applies), matching the
// same account_status_label semantics calculator-source.html's own
// tcStatus/accountStatus2 use (Safe under 25% of budget consumed,
// Caution 25-50%, Danger 50%+).
$accountStatusLabel = null;
$drawdownAtEntryPct = null;
$breathingRoomUsedPct = null;
$breathingRoomRemaining = null;
$tradesRemainingBuffer = null;

if ($accountType === 'prop_firm' && $accountId) {
    $pfr = PropFirmRuleEngine::compute($accountId);
    if (!isset($pfr['error'])) {
        $limitAmount = (float)$pfr['overall_drawdown']['limit_amount'];
        $currentAmount = (float)$pfr['overall_drawdown']['current_amount'];
        $remainingAmount = (float)$pfr['overall_drawdown']['remaining_amount'];

        if ($limitAmount > 0) {
            $budgetUsedPct = round(($currentAmount / $limitAmount) * 100, 2);
            $drawdownAtEntryPct = $budgetUsedPct;
            $breathingRoomUsedPct = $budgetUsedPct;
            $accountStatusLabel = $budgetUsedPct >= 50 ? 'Danger' : ($budgetUsedPct >= 25 ? 'Caution' : 'Safe');
        }
        $breathingRoomRemaining = round($remainingAmount, 2);

        $riskPerPositionForBuffer = isset($input['risk_per_position']) ? (float)$input['risk_per_position'] : 0;
        if ($riskPerPositionForBuffer > 0) {
            $tradesRemainingBuffer = round($remainingAmount / $riskPerPositionForBuffer, 2);
        }
    }
}

$phaseProfitTargetPct = isset($input['phase_profit_target_pct']) ? (float)$input['phase_profit_target_pct'] : null;
$phaseMaxDdPct = isset($input['phase_max_dd_pct']) ? (float)$input['phase_max_dd_pct'] : null;
$phaseDailyLossPct = isset($input['phase_daily_loss_pct']) ? (float)$input['phase_daily_loss_pct'] : null;
$leverage = isset($input['leverage']) ? trim((string)$input['leverage']) : null;
if ($leverage === '') $leverage = null;
$projectedPnl = $maxPotentialProfit;

$breachOverrideReason = $input['breach_override_reason'] ?? null;
$accountStatusAtCapture = $input['account_status_at_capture'] ?? null;

$maxPotentialLoss = isset($input['max_potential_loss']) ? (float)$input['max_potential_loss'] : $riskPerPosition;
// The calculator already computes this correctly per-instrument (real pips
// for forex, points for indices/commodities — see calculator-source.html's
// pipMultiplier table) and sends it in every save payload, but it was never
// captured here — silently discarded on every trade until now.
$slDistancePips = isset($input['sl_distance_pips']) ? (float)$input['sl_distance_pips'] : null;

// Planned Timeframe (2026-07-27, item 2 — Stage 2 Calculator Intent).
// Optional, no default — restricted to the same canonical option set as
// Strategy Builder's Timeframes traded field and Draft Resolution's
// actual_timeframe (see src/RenderHelpers.php's timeframeOptions(), the
// single source of truth all three read from).
$plannedTimeframeRaw = (string)($input['planned_timeframe'] ?? '');
$plannedTimeframe = in_array($plannedTimeframeRaw, timeframeOptions(), true) ? $plannedTimeframeRaw : null;

try {
    $pdo->beginTransaction();

    $tradeId = $db->insert('trades', [
        'trade_lifecycle_id'   => $tradeLifecycleId,
        'trade_group_id'       => $tradeGroupId,
        'account_id'           => $accountId,
        'strategy_version_id'  => $strategyVersionId,
        'captured_at'          => $capturedAt,
        'session_utc_time'     => $input['session_utc_time'] ?? '',
        'instrument'           => $input['instrument'] ?? '',
        'instrument_display'   => $input['instrument_display'] ?? '',
        'direction'            => $input['direction'] ?? 'long',
        'session'              => $input['session'] ?? '',
        'entry_price'          => $entryPrice,
        'sl_price'             => $slPrice,
        'sl_distance_pips'     => $slDistancePips,
        'tp_price'             => $tpPrice,
        'tp2_price'            => $tp2Price,
        'tp3_price'            => $tp3Price,
        'tp4_price'            => $tp4Price,
        'tp5_price'            => $tp5Price,
        'tp6_price'            => $tp6Price,
        'tp7_price'            => $tp7Price,
        'tp8_price'            => $tp8Price,
        'tp9_price'            => $tp9Price,
        'tp10_price'           => $tp10Price,
        'risk_mode'            => $input['risk_mode'] ?? '',
        'risk_per_position'    => $riskPerPosition,
        'risk_pct'             => $riskPct,
        'lots_per_position'    => $lotsPerPosition,
        'recommended_lots'     => $recommendedLots,
        'risk_reward_tp1'      => $riskRewardTp1,
        'planned_timeframe'    => $plannedTimeframe,
        'account_size_at_entry'=> $accountSizeAtEntry,
        'balance_at_entry'     => $balanceAtEntry,
        'account_status_label' => $accountStatusLabel,
        'drawdown_at_entry_pct'=> $drawdownAtEntryPct,
        'breathing_room_used_pct' => $breathingRoomUsedPct,
        'breathing_room_remaining' => $breathingRoomRemaining,
        'trades_remaining_buffer' => $tradesRemainingBuffer,
        'max_potential_profit' => $maxPotentialProfit,
        'max_potential_loss'   => $maxPotentialLoss,
        'projected_pnl'        => $projectedPnl,
        'challenge_pnl'        => $challengePnl,
        'excess_pnl'           => $excessPnl,
        'prop_firm_mode'       => !empty($input['prop_firm_mode']) ? 1 : 0,
        'prop_firm_name'       => $input['prop_firm_name'] ?? null,
        'prop_firm_phase'      => $input['prop_phase_label'] ?? null,
        'prop_phase_at_entry'  => $input['prop_phase_at_entry'] ?? null,
        'prop_firm_funded'     => !empty($input['prop_firm_funded']) ? 1 : 0,
        'phase_profit_target_pct' => $phaseProfitTargetPct,
        'phase_max_dd_pct'     => $phaseMaxDdPct,
        'phase_daily_loss_pct' => $phaseDailyLossPct,
        'leverage'             => $leverage,
        'warnings_fired'       => json_encode($input['warnings_fired'] ?? []),
        'blocker_fired'        => !empty($input['blocker_fired']) ? 1 : 0,
        'user_overrode_blocker'=> !empty($input['user_overrode_blocker']) ? 1 : 0,
        'mood_before'          => $input['mood_before'] ?? null,
        'energy_before'        => $input['energy_before'] ?? null,
        'confidence_before'    => $input['confidence_before'] ?? null,
        'mode_at_capture'      => $input['mode_at_capture'] ?? null,
        'mt5_comment_hash'     => $mt5CommentHash,
        'trade_context'        => $tradeContext,
        'breach_override_reason'    => $breachOverrideReason,
        'account_status_at_capture' => $accountStatusAtCapture,
        'edge_snapshot'        => $edgeSnapshot,
        'is_validation'        => $isValidationTrade,
    ]);

    // ──── No balance mutation, no breach detection, no auto-close here ─────
    // draft.php is a pure save. current_balance (trading_accounts) and the
    // frozen challenge balance (challenge_accounts.current_balance) only ever
    // change at Draft Outcome confirmation, in dashboard.php's resolve_trade_id
    // handler — using the trader's ACTUAL confirmed P&L, and only after the
    // trader explicitly confirms Still Active / Passed Phase / Breached
    // Account. Capital-depleted auto-close for personal accounts lives there
    // too now, for the same reason (it must react to a confirmed loss, not a
    // projected one).
    $stmt = $pdo->prepare("UPDATE checklist_results SET status = 'completed' WHERE trade_lifecycle_id = :tid AND user_id = :uid");
    $stmt->execute([':tid' => $tradeLifecycleId, ':uid' => $userId]);

    $pdo->commit();

    $response = [
        'success'          => true,
        'trade_id'         => $tradeId,
        'trade_context'    => $tradeContext,
        'mt5_comment_hash' => $mt5CommentHash,
    ];

    echo json_encode($response);

} catch (\Throwable $e) {
    if ($pdo->inTransaction()) $pdo->rollBack();
    // 2026-08-19 security fix: the 'debug' field echoed the raw exception
    // message to every client regardless of environment — logged
    // server-side only now (this is exactly the pattern that surfaced a
    // real, unrelated bug for me while verifying an earlier fix this same
    // session: a missing required field produced a raw
    // "SQLSTATE[23000]...cannot be null" string here).
    error_log('trades/draft.php failed: ' . $e->getMessage());
    http_response_code(500);
    echo json_encode(['error' => 'Failed to save trade. Please try again.']);
    exit;
}