<?php
/**
 * ftInvstr Paper Trading Bot for cPanel/Linux
 *
 * Commands:
 *   php -q paper_bot.php init
 *   php -q paper_bot.php test-telegram
 *   php -q paper_bot.php rebalance
 *   php -q paper_bot.php daily
 *   php -q paper_bot.php portfolio
 *   php -q paper_bot.php mcp-test
 *
 * Purpose:
 * - Paper trading only. Does not place broker orders.
 * - Pulls strategy holdings from ftInvstr MCP JSON-RPC endpoint.
 * - Sends Telegram weekly rebalance recommendations and daily P/L.
 */

declare(strict_types=1);

ini_set('display_errors', '1');
error_reporting(E_ALL);
date_default_timezone_set('Asia/Kolkata');

main($argv ?? []);

function main(array $argv): void
{
    $cmd = $argv[1] ?? 'help';

    try {
        $config = loadConfig(__DIR__ . '/.env');
        $pdo = openDb($config);
        initDb($pdo);

        switch ($cmd) {
            case 'init':
                echo "Database initialized: " . dbPath($config) . PHP_EOL;
                break;

            case 'test-telegram':
                sendTelegram($config, "✅ ftInvstr Paper Bot test message\nTime: " . nowIst());
                echo "Telegram test sent." . PHP_EOL;
                break;

            case 'mcp-test':
                runMcpTest($config);
                break;

            case 'rebalance':
                runWeeklyRebalance($config, $pdo);
                break;

            case 'daily':
                runDailyPnl($config, $pdo);
                break;

            case 'portfolio':
                printPortfolio($pdo);
                break;

            default:
                printHelp();
        }
    } catch (Throwable $e) {
        $msg = "❌ ftInvstr Paper Bot error\n" . $e->getMessage() . "\nTime: " . nowIst();

        // Try Telegram error alert, but do not hide the original error if Telegram also fails.
        try {
            $config = isset($config) && is_array($config) ? $config : [];
            if (!empty($config['TELEGRAM_BOT_TOKEN']) && !empty($config['TELEGRAM_CHAT_ID'])) {
                sendTelegram($config, $msg);
            }
        } catch (Throwable $ignored) {
            // Ignore Telegram failure while reporting primary exception.
        }

        file_put_contents("php://stderr", $msg . PHP_EOL);
        exit(1);
    }
}

function printHelp(): void
{
    echo <<<TXT
ftInvstr Paper Trading Bot

Usage:
  php -q paper_bot.php init
  php -q paper_bot.php test-telegram
  php -q paper_bot.php mcp-test
  php -q paper_bot.php rebalance
  php -q paper_bot.php daily
  php -q paper_bot.php portfolio

Cron examples:
  5 9 * * 1 /usr/bin/php -q /path/paper_bot.php rebalance >> /path/cron.log 2>&1
  45 15 * * 1-5 /usr/bin/php -q /path/paper_bot.php daily >> /path/cron.log 2>&1

TXT;
}

function loadConfig(string $envFile): array
{
    $env = [];

    if (file_exists($envFile)) {
        $lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        foreach ($lines as $line) {
            $line = trim($line);
            if ($line === '' || str_starts_with($line, '#')) {
                continue;
            }
            if (!str_contains($line, '=')) {
                continue;
            }
            [$key, $value] = explode('=', $line, 2);
            $key = trim($key);
            $value = trim($value);
            $value = trim($value, " \t\n\r\0\x0B\"'");
            $env[$key] = $value;
        }
    }

    // Also allow real environment variables to override .env.
    foreach ($_ENV as $k => $v) {
        $env[$k] = (string)$v;
    }
    foreach ($_SERVER as $k => $v) {
        if (is_string($v) && preg_match('/^[A-Z0-9_]+$/', $k)) {
            $env[$k] = $v;
        }
    }

    $defaults = [
        'APP_TIMEZONE' => 'Asia/Kolkata',
        'PAPER_DB_PATH' => __DIR__ . '/paper_trading.sqlite',
        'FTINVSTR_API_MODE' => 'mcp',
        'FTINVSTR_MCP_URL' => 'https://ftinvstr.in/mcp/',
        'FTINVSTR_API_TOKEN' => '',
        'TELEGRAM_BOT_TOKEN' => '',
        'TELEGRAM_CHAT_ID' => '',
        'TOTAL_CAPITAL' => '200000',
        'SPLIT_CAPITAL_EQUAL' => 'true',
        'CAPITAL_12_1' => '100000',
        'CAPITAL_MULTI_HORIZON' => '100000',
        'AUTO_APPLY_PAPER_REBALANCE' => 'true',
        'USE_FTINVSTR_WEIGHTS' => 'true',
        'ALLOW_FRACTIONAL_PAPER' => 'false',
        'MIN_TRADE_VALUE' => '100',
        'STRATEGY_12_1' => 'Momentum12Minus1_Sched_W_N500_LB20_05101551',
        'STRATEGY_MULTI_HORIZON' => 'momentum_blend_monthly_rebalance',
    ];

    $config = array_merge($defaults, $env);

    if (!empty($config['APP_TIMEZONE'])) {
        date_default_timezone_set($config['APP_TIMEZONE']);
    }

    return $config;
}

function boolValue($value): bool
{
    return in_array(strtolower((string)$value), ['1', 'true', 'yes', 'y', 'on'], true);
}

function dbPath(array $config): string
{
    return $config['PAPER_DB_PATH'] ?: (__DIR__ . '/paper_trading.sqlite');
}

function openDb(array $config): PDO
{
    $path = dbPath($config);
    $dir = dirname($path);
    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }

    $pdo = new PDO('sqlite:' . $path);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    return $pdo;
}

function initDb(PDO $pdo): void
{
    $pdo->exec("CREATE TABLE IF NOT EXISTS positions (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        strategy_key TEXT NOT NULL,
        strategy_name TEXT NOT NULL,
        symbol TEXT NOT NULL,
        isin TEXT,
        company_name TEXT,
        qty REAL NOT NULL DEFAULT 0,
        avg_price REAL NOT NULL DEFAULT 0,
        last_price REAL,
        last_price_date TEXT,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL,
        UNIQUE(strategy_key, symbol)
    )");

    $pdo->exec("CREATE TABLE IF NOT EXISTS trades (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        trade_date TEXT NOT NULL,
        strategy_key TEXT NOT NULL,
        strategy_name TEXT NOT NULL,
        symbol TEXT NOT NULL,
        isin TEXT,
        company_name TEXT,
        side TEXT NOT NULL,
        qty REAL NOT NULL,
        price REAL NOT NULL,
        value REAL NOT NULL,
        reason TEXT,
        created_at TEXT NOT NULL
    )");

    $pdo->exec("CREATE TABLE IF NOT EXISTS nav_history (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        nav_date TEXT NOT NULL,
        strategy_key TEXT NOT NULL,
        strategy_name TEXT NOT NULL,
        invested_value REAL NOT NULL DEFAULT 0,
        current_value REAL NOT NULL DEFAULT 0,
        pnl REAL NOT NULL DEFAULT 0,
        pnl_pct REAL NOT NULL DEFAULT 0,
        created_at TEXT NOT NULL,
        UNIQUE(nav_date, strategy_key)
    )");
}

function strategies(array $config): array
{
    $total = (float)$config['TOTAL_CAPITAL'];
    $splitEqual = boolValue($config['SPLIT_CAPITAL_EQUAL']);

    if ($splitEqual) {
        $cap12 = $total / 2.0;
        $capMulti = $total / 2.0;
    } else {
        $cap12 = (float)$config['CAPITAL_12_1'];
        $capMulti = (float)$config['CAPITAL_MULTI_HORIZON'];
    }

    return [
        '12_1_momentum' => [
            'display' => '12–1 Momentum Factor Weekly',
            'strategy_name' => $config['STRATEGY_12_1'],
            'capital' => $cap12,
        ],
        'multi_horizon' => [
            'display' => 'Multi-Horizon Momentum Blend',
            'strategy_name' => $config['STRATEGY_MULTI_HORIZON'],
            'capital' => $capMulti,
        ],
    ];
}

function runMcpTest(array $config): void
{
    foreach (strategies($config) as $key => $s) {
        $holdings = fetchFtInvstrHoldings($config, $s['strategy_name']);
        $count = count($holdings['data']['positions']);
        echo $s['display'] . ' => ' . $count . ' positions' . PHP_EOL;
    }
}

function runWeeklyRebalance(array $config, PDO $pdo): void
{
    $messages = [];
    $autoApply = boolValue($config['AUTO_APPLY_PAPER_REBALANCE']);

    $messages[] = "📌 ftInvstr Paper Rebalance";
    $messages[] = "Date: " . nowIst();
    $messages[] = "Mode: " . ($autoApply ? "auto-applied to paper book" : "recommendation only");
    $messages[] = "Capital: " . money((float)$config['TOTAL_CAPITAL']);
    $messages[] = "Paper trading only. Not investment advice.";

    foreach (strategies($config) as $strategyKey => $s) {
        $messages[] = "\n==============================";
        $messages[] = "Strategy: " . $s['display'];
        $messages[] = "Allocation: " . money((float)$s['capital']);

        $holdings = fetchFtInvstrHoldings($config, $s['strategy_name']);
        $positions = normalizePositions($holdings);

        if (count($positions) === 0) {
            $messages[] = "No target positions returned by ftInvstr.";
            continue;
        }

        $recommendations = calculateRebalanceRecommendations($config, $pdo, $strategyKey, $s, $positions);

        if (count($recommendations) === 0) {
            $messages[] = "No buy/sell changes. Current paper portfolio already matches target as closely as possible.";
        } else {
            foreach ($recommendations as $r) {
                $line = sprintf(
                    "%s %s qty %s @ %s = %s",
                    $r['side'] === 'BUY' ? '🟢 BUY' : '🔴 SELL',
                    $r['symbol'],
                    qtyText($r['qty']),
                    money($r['price']),
                    money($r['value'])
                );
                if (!empty($r['reason'])) {
                    $line .= " | " . $r['reason'];
                }
                $messages[] = $line;
            }
        }

        $skips = calculateSkippedTargets($config, $s, $positions);
        foreach ($skips as $skip) {
            $messages[] = "⚠️ SKIP " . $skip['symbol'] . " price " . money($skip['price']) . " > target allocation " . money($skip['target_value']);
        }

        if ($autoApply && count($recommendations) > 0) {
            applyRecommendations($pdo, $strategyKey, $s, $recommendations);
            $messages[] = "✅ Paper portfolio updated.";
        } elseif (!$autoApply) {
            $messages[] = "ℹ️ AUTO_APPLY_PAPER_REBALANCE=false, so no DB changes were made.";
        }
    }

    $text = implode("\n", $messages);
    sendTelegram($config, $text);
    echo $text . PHP_EOL;
}

function runDailyPnl(array $config, PDO $pdo): void
{
    $messages = [];
    $messages[] = "📊 ftInvstr Daily Paper P/L";
    $messages[] = "Date: " . nowIst();
    $messages[] = "Paper trading only. Not investment advice.";

    $grandInvested = 0.0;
    $grandCurrent = 0.0;

    foreach (strategies($config) as $strategyKey => $s) {
        $positionsDb = getPositions($pdo, $strategyKey);

        $messages[] = "\n==============================";
        $messages[] = "Strategy: " . $s['display'];

        if (count($positionsDb) === 0) {
            $messages[] = "No paper positions yet. Run: php -q paper_bot.php rebalance";
            continue;
        }

        // Get latest prices from current ftInvstr holdings. If a symbol is absent, use last stored price.
        $quoteMap = [];
        try {
            $holdings = fetchFtInvstrHoldings($config, $s['strategy_name']);
            foreach (normalizePositions($holdings) as $p) {
                $quoteMap[$p['symbol']] = $p;
            }
        } catch (Throwable $e) {
            $messages[] = "⚠️ Price refresh failed, using stored prices: " . $e->getMessage();
        }

        $strategyInvested = 0.0;
        $strategyCurrent = 0.0;

        foreach ($positionsDb as $pos) {
            $symbol = $pos['symbol'];
            $qty = (float)$pos['qty'];
            $avg = (float)$pos['avg_price'];
            $last = isset($quoteMap[$symbol]) ? (float)$quoteMap[$symbol]['price'] : (float)($pos['last_price'] ?: $avg);

            updateLastPrice($pdo, $strategyKey, $symbol, $last);

            $invested = $qty * $avg;
            $current = $qty * $last;
            $pnl = $current - $invested;
            $pnlPct = $invested > 0 ? ($pnl / $invested * 100.0) : 0.0;

            $strategyInvested += $invested;
            $strategyCurrent += $current;

            $sign = $pnl >= 0 ? '🟢' : '🔴';
            $messages[] = sprintf(
                "%s %s qty %s | Buy %s | Now %s | P/L %s (%s)",
                $sign,
                $symbol,
                qtyText($qty),
                money($avg),
                money($last),
                money($pnl),
                pct($pnlPct)
            );
        }

        $strategyPnl = $strategyCurrent - $strategyInvested;
        $strategyPnlPct = $strategyInvested > 0 ? ($strategyPnl / $strategyInvested * 100.0) : 0.0;
        $grandInvested += $strategyInvested;
        $grandCurrent += $strategyCurrent;

        upsertNav($pdo, $strategyKey, $s['strategy_name'], $strategyInvested, $strategyCurrent, $strategyPnl, $strategyPnlPct);

        $messages[] = "---";
        $messages[] = "Invested: " . money($strategyInvested);
        $messages[] = "Current: " . money($strategyCurrent);
        $messages[] = "Strategy P/L: " . money($strategyPnl) . " (" . pct($strategyPnlPct) . ")";
    }

    $grandPnl = $grandCurrent - $grandInvested;
    $grandPnlPct = $grandInvested > 0 ? ($grandPnl / $grandInvested * 100.0) : 0.0;
    $messages[] = "\n==============================";
    $messages[] = "Total Invested: " . money($grandInvested);
    $messages[] = "Total Current: " . money($grandCurrent);
    $messages[] = "Total Paper P/L: " . money($grandPnl) . " (" . pct($grandPnlPct) . ")";

    $text = implode("\n", $messages);
    sendTelegram($config, $text);
    echo $text . PHP_EOL;
}

function calculateRebalanceRecommendations(array $config, PDO $pdo, string $strategyKey, array $strategy, array $targetPositions): array
{
    $useWeights = boolValue($config['USE_FTINVSTR_WEIGHTS']);
    $allowFractional = boolValue($config['ALLOW_FRACTIONAL_PAPER']);
    $minTradeValue = (float)$config['MIN_TRADE_VALUE'];
    $capital = (float)$strategy['capital'];
    $current = getPositionsBySymbol($pdo, $strategyKey);
    $targetBySymbol = [];

    $equalValue = count($targetPositions) > 0 ? $capital / count($targetPositions) : 0.0;

    foreach ($targetPositions as $p) {
        $price = (float)$p['price'];
        if ($price <= 0) {
            continue;
        }
        $targetValue = $useWeights && $p['weight_pct'] > 0
            ? $capital * ((float)$p['weight_pct'] / 100.0)
            : $equalValue;

        $targetQty = $allowFractional ? round($targetValue / $price, 6) : floor($targetValue / $price);
        $targetBySymbol[$p['symbol']] = array_merge($p, [
            'target_value' => $targetValue,
            'target_qty' => $targetQty,
        ]);
    }

    $recs = [];

    // Sell symbols no longer in target list.
    foreach ($current as $symbol => $pos) {
        if (!isset($targetBySymbol[$symbol]) && (float)$pos['qty'] > 0) {
            $price = (float)($pos['last_price'] ?: $pos['avg_price']);
            $qty = (float)$pos['qty'];
            $recs[] = [
                'side' => 'SELL',
                'symbol' => $symbol,
                'isin' => $pos['isin'] ?? '',
                'company_name' => $pos['company_name'] ?? '',
                'qty' => $qty,
                'price' => $price,
                'value' => $qty * $price,
                'reason' => 'removed from target list',
            ];
        }
    }

    // Buy/sell to target quantities.
    foreach ($targetBySymbol as $symbol => $target) {
        $oldQty = isset($current[$symbol]) ? (float)$current[$symbol]['qty'] : 0.0;
        $newQty = (float)$target['target_qty'];
        $diff = $newQty - $oldQty;

        if (abs($diff) < 0.000001) {
            continue;
        }

        $side = $diff > 0 ? 'BUY' : 'SELL';
        $qty = abs($diff);
        $value = $qty * (float)$target['price'];

        if ($value < $minTradeValue) {
            continue;
        }

        $recs[] = [
            'side' => $side,
            'symbol' => $symbol,
            'isin' => $target['isin'],
            'company_name' => $target['company_name'],
            'qty' => $qty,
            'price' => (float)$target['price'],
            'value' => $value,
            'reason' => $side === 'BUY' ? 'target entry/top-up' : 'reduce to target qty',
        ];
    }

    return $recs;
}

function calculateSkippedTargets(array $config, array $strategy, array $targetPositions): array
{
    if (boolValue($config['ALLOW_FRACTIONAL_PAPER'])) {
        return [];
    }

    $useWeights = boolValue($config['USE_FTINVSTR_WEIGHTS']);
    $capital = (float)$strategy['capital'];
    $equalValue = count($targetPositions) > 0 ? $capital / count($targetPositions) : 0.0;
    $skips = [];

    foreach ($targetPositions as $p) {
        $price = (float)$p['price'];
        if ($price <= 0) {
            continue;
        }
        $targetValue = $useWeights && $p['weight_pct'] > 0
            ? $capital * ((float)$p['weight_pct'] / 100.0)
            : $equalValue;
        $targetQty = floor($targetValue / $price);
        if ($targetQty <= 0) {
            $skips[] = [
                'symbol' => $p['symbol'],
                'price' => $price,
                'target_value' => $targetValue,
            ];
        }
    }

    return $skips;
}

function applyRecommendations(PDO $pdo, string $strategyKey, array $strategy, array $recommendations): void
{
    $pdo->beginTransaction();
    try {
        foreach ($recommendations as $r) {
            recordTrade($pdo, $strategyKey, $strategy['strategy_name'], $r);

            if ($r['side'] === 'BUY') {
                applyBuy($pdo, $strategyKey, $strategy['strategy_name'], $r);
            } else {
                applySell($pdo, $strategyKey, $r);
            }
        }
        $pdo->commit();
    } catch (Throwable $e) {
        $pdo->rollBack();
        throw $e;
    }
}

function recordTrade(PDO $pdo, string $strategyKey, string $strategyName, array $r): void
{
    $stmt = $pdo->prepare("INSERT INTO trades
        (trade_date, strategy_key, strategy_name, symbol, isin, company_name, side, qty, price, value, reason, created_at)
        VALUES (:trade_date, :strategy_key, :strategy_name, :symbol, :isin, :company_name, :side, :qty, :price, :value, :reason, :created_at)");

    $stmt->execute([
        ':trade_date' => date('Y-m-d'),
        ':strategy_key' => $strategyKey,
        ':strategy_name' => $strategyName,
        ':symbol' => $r['symbol'],
        ':isin' => $r['isin'] ?? '',
        ':company_name' => $r['company_name'] ?? '',
        ':side' => $r['side'],
        ':qty' => $r['qty'],
        ':price' => $r['price'],
        ':value' => $r['value'],
        ':reason' => $r['reason'] ?? '',
        ':created_at' => nowIso(),
    ]);
}

function applyBuy(PDO $pdo, string $strategyKey, string $strategyName, array $r): void
{
    $existing = getOnePosition($pdo, $strategyKey, $r['symbol']);
    $now = nowIso();

    if ($existing) {
        $oldQty = (float)$existing['qty'];
        $oldAvg = (float)$existing['avg_price'];
        $buyQty = (float)$r['qty'];
        $buyPrice = (float)$r['price'];
        $newQty = $oldQty + $buyQty;
        $newAvg = $newQty > 0 ? (($oldQty * $oldAvg) + ($buyQty * $buyPrice)) / $newQty : 0.0;

        $stmt = $pdo->prepare("UPDATE positions SET
            qty=:qty, avg_price=:avg_price, last_price=:last_price, last_price_date=:last_price_date,
            isin=:isin, company_name=:company_name, updated_at=:updated_at
            WHERE strategy_key=:strategy_key AND symbol=:symbol");
        $stmt->execute([
            ':qty' => $newQty,
            ':avg_price' => $newAvg,
            ':last_price' => $buyPrice,
            ':last_price_date' => date('Y-m-d'),
            ':isin' => $r['isin'] ?? '',
            ':company_name' => $r['company_name'] ?? '',
            ':updated_at' => $now,
            ':strategy_key' => $strategyKey,
            ':symbol' => $r['symbol'],
        ]);
    } else {
        $stmt = $pdo->prepare("INSERT INTO positions
            (strategy_key, strategy_name, symbol, isin, company_name, qty, avg_price, last_price, last_price_date, created_at, updated_at)
            VALUES (:strategy_key, :strategy_name, :symbol, :isin, :company_name, :qty, :avg_price, :last_price, :last_price_date, :created_at, :updated_at)");
        $stmt->execute([
            ':strategy_key' => $strategyKey,
            ':strategy_name' => $strategyName,
            ':symbol' => $r['symbol'],
            ':isin' => $r['isin'] ?? '',
            ':company_name' => $r['company_name'] ?? '',
            ':qty' => $r['qty'],
            ':avg_price' => $r['price'],
            ':last_price' => $r['price'],
            ':last_price_date' => date('Y-m-d'),
            ':created_at' => $now,
            ':updated_at' => $now,
        ]);
    }
}

function applySell(PDO $pdo, string $strategyKey, array $r): void
{
    $existing = getOnePosition($pdo, $strategyKey, $r['symbol']);
    if (!$existing) {
        return;
    }

    $oldQty = (float)$existing['qty'];
    $sellQty = (float)$r['qty'];
    $newQty = $oldQty - $sellQty;

    if ($newQty <= 0.000001) {
        $stmt = $pdo->prepare("DELETE FROM positions WHERE strategy_key=:strategy_key AND symbol=:symbol");
        $stmt->execute([
            ':strategy_key' => $strategyKey,
            ':symbol' => $r['symbol'],
        ]);
    } else {
        $stmt = $pdo->prepare("UPDATE positions SET qty=:qty, last_price=:last_price, last_price_date=:last_price_date, updated_at=:updated_at WHERE strategy_key=:strategy_key AND symbol=:symbol");
        $stmt->execute([
            ':qty' => $newQty,
            ':last_price' => $r['price'],
            ':last_price_date' => date('Y-m-d'),
            ':updated_at' => nowIso(),
            ':strategy_key' => $strategyKey,
            ':symbol' => $r['symbol'],
        ]);
    }
}

function getPositions(PDO $pdo, string $strategyKey): array
{
    $stmt = $pdo->prepare("SELECT * FROM positions WHERE strategy_key=:strategy_key ORDER BY symbol");
    $stmt->execute([':strategy_key' => $strategyKey]);
    return $stmt->fetchAll();
}

function getPositionsBySymbol(PDO $pdo, string $strategyKey): array
{
    $rows = getPositions($pdo, $strategyKey);
    $out = [];
    foreach ($rows as $r) {
        $out[$r['symbol']] = $r;
    }
    return $out;
}

function getOnePosition(PDO $pdo, string $strategyKey, string $symbol): ?array
{
    $stmt = $pdo->prepare("SELECT * FROM positions WHERE strategy_key=:strategy_key AND symbol=:symbol LIMIT 1");
    $stmt->execute([':strategy_key' => $strategyKey, ':symbol' => $symbol]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function updateLastPrice(PDO $pdo, string $strategyKey, string $symbol, float $lastPrice): void
{
    $stmt = $pdo->prepare("UPDATE positions SET last_price=:last_price, last_price_date=:last_price_date, updated_at=:updated_at WHERE strategy_key=:strategy_key AND symbol=:symbol");
    $stmt->execute([
        ':last_price' => $lastPrice,
        ':last_price_date' => date('Y-m-d'),
        ':updated_at' => nowIso(),
        ':strategy_key' => $strategyKey,
        ':symbol' => $symbol,
    ]);
}

function upsertNav(PDO $pdo, string $strategyKey, string $strategyName, float $invested, float $current, float $pnl, float $pnlPct): void
{
    $stmt = $pdo->prepare("INSERT INTO nav_history
        (nav_date, strategy_key, strategy_name, invested_value, current_value, pnl, pnl_pct, created_at)
        VALUES (:nav_date, :strategy_key, :strategy_name, :invested_value, :current_value, :pnl, :pnl_pct, :created_at)
        ON CONFLICT(nav_date, strategy_key) DO UPDATE SET
        invested_value=excluded.invested_value,
        current_value=excluded.current_value,
        pnl=excluded.pnl,
        pnl_pct=excluded.pnl_pct,
        created_at=excluded.created_at");

    $stmt->execute([
        ':nav_date' => date('Y-m-d'),
        ':strategy_key' => $strategyKey,
        ':strategy_name' => $strategyName,
        ':invested_value' => $invested,
        ':current_value' => $current,
        ':pnl' => $pnl,
        ':pnl_pct' => $pnlPct,
        ':created_at' => nowIso(),
    ]);
}

function printPortfolio(PDO $pdo): void
{
    $rows = $pdo->query("SELECT * FROM positions ORDER BY strategy_key, symbol")->fetchAll();
    if (!$rows) {
        echo "No positions found." . PHP_EOL;
        return;
    }

    foreach ($rows as $r) {
        $invested = (float)$r['qty'] * (float)$r['avg_price'];
        $current = (float)$r['qty'] * (float)($r['last_price'] ?: $r['avg_price']);
        $pnl = $current - $invested;
        echo implode(' | ', [
            $r['strategy_key'],
            $r['symbol'],
            'qty ' . qtyText((float)$r['qty']),
            'avg ' . money((float)$r['avg_price']),
            'last ' . money((float)($r['last_price'] ?: $r['avg_price'])),
            'pnl ' . money($pnl),
        ]) . PHP_EOL;
    }
}

function fetchFtInvstrHoldings(array $config, string $strategyName): array
{
    $mode = strtolower((string)($config['FTINVSTR_API_MODE'] ?? 'mcp'));
    if ($mode !== 'mcp') {
        throw new RuntimeException("Set FTINVSTR_API_MODE=mcp in .env");
    }

    $mcpUrl = trim((string)($config['FTINVSTR_MCP_URL'] ?? ''));
    $token = trim((string)($config['FTINVSTR_API_TOKEN'] ?? ''));

    if ($mcpUrl === '') {
        throw new RuntimeException("Missing FTINVSTR_MCP_URL in .env");
    }
    if ($token === '') {
        throw new RuntimeException("Missing FTINVSTR_API_TOKEN in .env");
    }

    $payload = [
        'jsonrpc' => '2.0',
        'id' => random_int(1, 999999),
        'method' => 'tools/call',
        'params' => [
            'name' => 'get_holdings',
            'arguments' => [
                'strategy_name' => $strategyName,
            ],
        ],
    ];

    $response = httpPostJson($mcpUrl, $payload, [
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json',
        'Accept: application/json',
    ]);

    return normalizeMcpHoldingsResponse($response, $strategyName);
}

function httpPostJson(string $url, array $payload, array $headers = []): array
{
    if (!function_exists('curl_init')) {
        throw new RuntimeException('PHP cURL extension is missing. Install php-curl.');
    }

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_SLASHES),
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_TIMEOUT => 90,
        CURLOPT_FOLLOWLOCATION => true,
    ]);

    $body = curl_exec($ch);
    $err = curl_error($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($body === false || $err !== '') {
        throw new RuntimeException("HTTP POST failed for $url: $err");
    }
    if ($code < 200 || $code >= 300) {
        throw new RuntimeException("HTTP $code for $url: $body");
    }

    $json = json_decode((string)$body, true);
    if (!is_array($json)) {
        throw new RuntimeException("Invalid JSON from $url: " . substr((string)$body, 0, 500));
    }
    if (isset($json['error'])) {
        throw new RuntimeException('ftInvstr MCP error: ' . json_encode($json['error']));
    }

    return $json;
}

function normalizeMcpHoldingsResponse(array $mcpResponse, string $strategyName): array
{
    $payload = extractMcpPayload($mcpResponse);

    // Common shapes accepted:
    // {data:{positions:[...]}}
    // {strategy:..., positions:[...]}
    // {result:{data:{positions:[...]}}}
    if (isset($payload['data']['positions'])) {
        $data = $payload['data'];
    } elseif (isset($payload['positions'])) {
        $data = $payload;
    } elseif (isset($mcpResponse['result']['data']['positions'])) {
        $data = $mcpResponse['result']['data'];
    } elseif (isset($mcpResponse['result']['structuredContent']['data']['positions'])) {
        $data = $mcpResponse['result']['structuredContent']['data'];
    } else {
        throw new RuntimeException('Could not find positions in ftInvstr MCP response for ' . $strategyName);
    }

    if (empty($data['positions']) || !is_array($data['positions'])) {
        throw new RuntimeException('No positions returned by ftInvstr for ' . $strategyName);
    }

    return [
        'data' => [
            'strategy' => $data['strategy'] ?? $strategyName,
            'as_of' => $data['as_of'] ?? date('Y-m-d'),
            'positions' => $data['positions'],
        ],
    ];
}

function extractMcpPayload(array $mcpResponse): array
{
    if (isset($mcpResponse['result']['structuredContent']) && is_array($mcpResponse['result']['structuredContent'])) {
        return $mcpResponse['result']['structuredContent'];
    }

    if (isset($mcpResponse['result']['data']) && is_array($mcpResponse['result']['data'])) {
        return ['data' => $mcpResponse['result']['data']];
    }

    if (isset($mcpResponse['result']['content']) && is_array($mcpResponse['result']['content'])) {
        foreach ($mcpResponse['result']['content'] as $item) {
            if (!isset($item['text'])) {
                continue;
            }
            $text = trim((string)$item['text']);
            $decoded = json_decode($text, true);
            if (is_array($decoded)) {
                return $decoded;
            }
        }
    }

    if (isset($mcpResponse['data']) && is_array($mcpResponse['data'])) {
        return ['data' => $mcpResponse['data']];
    }

    // Last chance: sometimes result itself is already the payload.
    if (isset($mcpResponse['result']) && is_array($mcpResponse['result'])) {
        return $mcpResponse['result'];
    }

    return $mcpResponse;
}

function normalizePositions(array $holdings): array
{
    $positions = $holdings['data']['positions'] ?? [];
    $out = [];

    foreach ($positions as $p) {
        $symbol = trim((string)($p['symbol'] ?? $p['ticker'] ?? ''));
        if ($symbol === '') {
            continue;
        }

        $price = (float)($p['price'] ?? $p['last_price'] ?? $p['ltp'] ?? 0);
        if ($price <= 0) {
            continue;
        }

        $out[] = [
            'symbol' => $symbol,
            'isin' => (string)($p['isin'] ?? ''),
            'company_name' => (string)($p['company_name'] ?? $p['name'] ?? $symbol),
            'price' => $price,
            'weight_pct' => (float)($p['weight_pct'] ?? $p['weight'] ?? 0),
        ];
    }

    return $out;
}

function sendTelegram(array $config, string $text): void
{
    $token = trim((string)($config['TELEGRAM_BOT_TOKEN'] ?? ''));
    $chatId = trim((string)($config['TELEGRAM_CHAT_ID'] ?? ''));

    if ($token === '' || $chatId === '') {
        throw new RuntimeException('Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID in .env');
    }

    $chunks = splitTelegramText($text);
    foreach ($chunks as $chunk) {
        $url = 'https://api.telegram.org/bot' . $token . '/sendMessage';
        $payload = [
            'chat_id' => $chatId,
            'text' => $chunk,
            'disable_web_page_preview' => true,
        ];

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => http_build_query($payload),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 20,
            CURLOPT_TIMEOUT => 60,
        ]);

        $body = curl_exec($ch);
        $err = curl_error($ch);
        $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($body === false || $err !== '') {
            throw new RuntimeException('Telegram send failed: ' . $err);
        }
        if ($code < 200 || $code >= 300) {
            throw new RuntimeException("Telegram send failed HTTP $code: $body");
        }

        $json = json_decode((string)$body, true);
        if (!is_array($json) || empty($json['ok'])) {
            throw new RuntimeException('Telegram send failed: ' . $body);
        }
    }
}

function splitTelegramText(string $text): array
{
    $limit = 3900;
    if (strlen($text) <= $limit) {
        return [$text];
    }

    $parts = [];
    $lines = explode("\n", $text);
    $current = '';

    foreach ($lines as $line) {
        if (strlen($current . "\n" . $line) > $limit) {
            $parts[] = $current;
            $current = $line;
        } else {
            $current = $current === '' ? $line : $current . "\n" . $line;
        }
    }
    if ($current !== '') {
        $parts[] = $current;
    }
    return $parts;
}

function money(float $amount): string
{
    $sign = $amount < 0 ? '-' : '';
    $amount = abs($amount);
    return $sign . '₹' . number_format($amount, 2);
}

function pct(float $value): string
{
    $sign = $value > 0 ? '+' : '';
    return $sign . number_format($value, 2) . '%';
}

function qtyText(float $qty): string
{
    if (abs($qty - round($qty)) < 0.000001) {
        return (string)(int)round($qty);
    }
    return rtrim(rtrim(number_format($qty, 6, '.', ''), '0'), '.');
}

function nowIso(): string
{
    return date('Y-m-d H:i:s');
}

function nowIst(): string
{
    return date('Y-m-d H:i:s T');
}
