/**
 * Unified stock data query MCP tool.
 * Provides real-time quotes, historical data, financials, and search
 * for A-shares, HK, and US stocks.
 *
 * Uses stock-sdk (Tencent/EastMoney data) for quotes, history, and search,
 * and yahoo-finance2 for financials (fundamental data).
 * No Python bridge required — pure TypeScript.
 */
import { z } from "zod";
import { tool } from "@anthropic-ai/claude-agent-sdk";
import { StockSDK, } from "stock-sdk";
import YahooFinance from "yahoo-finance2";
import { logger } from "../logger.js";
// ---------------------------------------------------------------------------
// Shared instances (reused across calls)
// ---------------------------------------------------------------------------
const sdk = new StockSDK();
const yf = new YahooFinance();
function detectMarket(symbol) {
    const s = symbol.trim().toUpperCase();
    const bare = s
        .replace(".SH", "")
        .replace(".SZ", "")
        .replace(".HK", "")
        .replace(".SS", "");
    // A-share: 6-digit numeric
    if (/^\d{6}$/.test(bare)) {
        const pfx = bare.slice(0, 3);
        const aPrefixes = [
            "600", "601", "603", "605", "688", "689", // Shanghai
            "000", "001", "002", "003", "300", "301", // Shenzhen
            "830", "831", "832", "833", "834", "835", "836", "837", "838", "839",
            "870", "871", "872", "873", // BSE / NQ
        ];
        if (aPrefixes.includes(pfx))
            return "A";
    }
    // HK: .HK suffix or 4-5 digit numeric
    if (s.endsWith(".HK"))
        return "HK";
    if (/^\d{4,5}$/.test(bare))
        return "HK";
    // Default: US
    return "US";
}
/** Prefix bare A-share code for stock-sdk (e.g. "600519" → "sh600519"). */
function toSdkACode(code) {
    const bare = code.replace(/\.(SH|SZ|SS)/i, "").padStart(6, "0");
    return bare.startsWith("6") || bare.startsWith("9")
        ? `sh${bare}`
        : `sz${bare}`;
}
/** Bare 6-digit code from any A-share format. */
function bareACode(symbol) {
    return symbol.replace(/\.(SH|SZ|SS)/i, "").replace(/^(sh|sz)/i, "").padStart(6, "0");
}
/** Strip provider prefix from a search result code (e.g. "hk00700" → "00700", "usaapl.oq" → "AAPL"). */
function bareSearchCode(code, market) {
    if (market === "A")
        return bareACode(code);
    if (market === "HK") {
        // "hk00700" → "00700"
        return code.replace(/^hk/i, "").padStart(5, "0");
    }
    if (market === "US")
        return bareUsCode(code);
    return code;
}
/** Strip provider prefix and exchange suffix from US codes (e.g. "AAPL.OQ" → "AAPL", "usaapl.oq" → "AAPL"). */
function bareUsCode(code) {
    return code.replace(/^us/i, "").replace(/\.[a-z]+$/i, "").toUpperCase();
}
/** Normalize HK code to 5-digit for stock-sdk (e.g. "981" → "00981"). */
function toHkCode(symbol) {
    const bare = symbol.replace(/\.HK/i, "").trim();
    return /^\d+$/.test(bare) ? bare.padStart(5, "0") : bare;
}
/** Build yahoo-finance2 ticker for HK (e.g. "00981" → "0981.HK"). */
function toYfHkTicker(symbol) {
    const bare = symbol.replace(/\.HK/i, "").trim();
    const padded = /^\d+$/.test(bare) ? bare.padStart(4, "0") : bare;
    return `${padded}.HK`;
}
/** Build yahoo-finance2 ticker for A-share (e.g. "600519" → "600519.SS"). */
function toYfATicker(symbol) {
    const bare = bareACode(symbol);
    const suffix = bare.startsWith("6") || bare.startsWith("9") ? ".SS" : ".SZ";
    return `${bare}${suffix}`;
}
function safeFloat(v) {
    if (v === null || v === undefined || Number.isNaN(v))
        return null;
    return Math.abs(v) < 1e10 ? Math.round(v * 10000) / 10000 : Math.round(v * 100) / 100;
}
function makeSuccess(data) {
    return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
function makeError(msg) {
    return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
}
async function handleQuote(symbols, forcedMarket) {
    const syms = symbols.split(",").map((s) => s.trim()).filter(Boolean);
    if (syms.length === 0)
        return makeError("No symbols provided");
    const aSyms = [];
    const hkSyms = [];
    const usSyms = [];
    for (const sym of syms) {
        const m = forcedMarket !== "auto" ? forcedMarket : detectMarket(sym);
        if (m === "A")
            aSyms.push(sym);
        else if (m === "HK")
            hkSyms.push(sym);
        else
            usSyms.push(sym);
    }
    const results = [];
    // A-shares via stock-sdk (Tencent Finance)
    if (aSyms.length > 0) {
        try {
            const codes = aSyms.map(toSdkACode);
            const quotes = await sdk.getFullQuotes(codes);
            for (const q of quotes) {
                results.push({
                    symbol: bareACode(q.code),
                    name: q.name,
                    market: "A",
                    price: safeFloat(q.price),
                    change: safeFloat(q.change),
                    change_pct: safeFloat(q.changePercent),
                    open: safeFloat(q.open),
                    high: safeFloat(q.high),
                    low: safeFloat(q.low),
                    prev_close: safeFloat(q.prevClose),
                    volume: safeFloat(q.volume),
                    turnover: safeFloat(q.amount),
                    market_cap: safeFloat(q.totalMarketCap),
                    pe_ttm: safeFloat(q.pe),
                    pb: safeFloat(q.pb),
                    high_52w: safeFloat(q.high52w),
                    low_52w: safeFloat(q.low52w),
                });
            }
        }
        catch (e) {
            for (const sym of aSyms) {
                results.push({ symbol: bareACode(sym), market: "A", price: null, error: e instanceof Error ? e.message : String(e) });
            }
        }
    }
    // HK via stock-sdk
    if (hkSyms.length > 0) {
        try {
            const codes = hkSyms.map(toHkCode);
            const quotes = await sdk.getHKQuotes(codes);
            for (const q of quotes) {
                results.push({
                    symbol: q.code,
                    name: q.name,
                    market: "HK",
                    price: safeFloat(q.price),
                    change: safeFloat(q.change),
                    change_pct: safeFloat(q.changePercent),
                    open: safeFloat(q.open),
                    high: safeFloat(q.high),
                    low: safeFloat(q.low),
                    prev_close: safeFloat(q.prevClose),
                    volume: safeFloat(q.volume),
                    turnover: safeFloat(q.amount),
                    market_cap: safeFloat(q.totalMarketCap),
                });
            }
        }
        catch (e) {
            for (const sym of hkSyms) {
                results.push({ symbol: toHkCode(sym), market: "HK", price: null, error: e instanceof Error ? e.message : String(e) });
            }
        }
    }
    // US via stock-sdk
    if (usSyms.length > 0) {
        try {
            const codes = usSyms.map((s) => s.trim().toUpperCase());
            const quotes = await sdk.getUSQuotes(codes);
            for (const q of quotes) {
                results.push({
                    symbol: bareUsCode(q.code),
                    name: q.name,
                    market: "US",
                    price: safeFloat(q.price),
                    change: safeFloat(q.change),
                    change_pct: safeFloat(q.changePercent),
                    open: safeFloat(q.open),
                    high: safeFloat(q.high),
                    low: safeFloat(q.low),
                    prev_close: safeFloat(q.prevClose),
                    volume: safeFloat(q.volume),
                    turnover: safeFloat(q.amount),
                    market_cap: safeFloat(q.totalMarketCap),
                    pe_ttm: safeFloat(q.pe),
                    pb: safeFloat(q.pb),
                    high_52w: safeFloat(q.high52w),
                    low_52w: safeFloat(q.low52w),
                });
            }
        }
        catch (e) {
            for (const sym of usSyms) {
                results.push({ symbol: sym.toUpperCase(), market: "US", price: null, error: e instanceof Error ? e.message : String(e) });
            }
        }
    }
    return makeSuccess({ quotes: results, timestamp: new Date().toISOString() });
}
function mapHistoryKline(k) {
    return {
        date: k.date,
        open: safeFloat(k.open),
        high: safeFloat(k.high),
        low: safeFloat(k.low),
        close: safeFloat(k.close),
        volume: safeFloat(k.volume),
        turnover: safeFloat(k.amount),
        change_pct: safeFloat(k.changePercent),
    };
}
function mapHkUsKline(k) {
    return {
        date: k.date,
        open: safeFloat(k.open),
        high: safeFloat(k.high),
        low: safeFloat(k.low),
        close: safeFloat(k.close),
        volume: safeFloat(k.volume),
        turnover: safeFloat(k.amount),
        change_pct: safeFloat(k.changePercent),
    };
}
function buildSummary(records) {
    const closes = records.map((r) => r.close).filter((c) => c !== null);
    if (closes.length === 0)
        return {};
    const volumes = records.map((r) => r.volume).filter((v) => v !== null);
    const first = closes[0];
    const last = closes[closes.length - 1];
    return {
        period_return_pct: first !== 0
            ? Math.round(((last / first) - 1) * 10000) / 100
            : null,
        period_high: Math.max(...closes),
        period_low: Math.min(...closes),
        avg_volume: volumes.length > 0 ? Math.round(volumes.reduce((a, b) => a + b, 0) / volumes.length) : null,
        total_records: records.length,
    };
}
/** Convert period string like "3mo" to YYYYMMDD start date (for stock-sdk). */
function periodToStartDate(period) {
    const daysMap = {
        "1d": 1, "5d": 5, "1mo": 30, "3mo": 90, "6mo": 180,
        "1y": 365, "2y": 730, "5y": 1825, "max": 7300,
    };
    const days = daysMap[period] ?? 90;
    const d = new Date();
    d.setDate(d.getDate() - days);
    return d.toISOString().slice(0, 10).replace(/-/g, "");
}
/** Convert YYYYMMDD to YYYY-MM-DD (for yahoo-finance2 which rejects compact dates). */
function toIsoDate(compactOrIso) {
    if (compactOrIso.includes("-"))
        return compactOrIso;
    // "20240115" → "2024-01-15"
    return `${compactOrIso.slice(0, 4)}-${compactOrIso.slice(4, 6)}-${compactOrIso.slice(6, 8)}`;
}
async function handleHistory(symbol, forcedMarket, period, interval, startDate, endDate, limit) {
    const market = forcedMarket !== "auto" ? forcedMarket : detectMarket(symbol);
    const sdkStart = startDate?.replace(/-/g, "") ?? periodToStartDate(period);
    const sdkEnd = endDate?.replace(/-/g, "") ?? new Date().toISOString().slice(0, 10).replace(/-/g, "");
    try {
        let records;
        if (market === "A") {
            const code = toSdkACode(bareACode(symbol));
            const klines = await sdk.getHistoryKline(code, {
                period: interval,
                adjust: "qfq",
                startDate: sdkStart,
                endDate: sdkEnd,
            });
            records = klines.map(mapHistoryKline);
        }
        else if (market === "HK") {
            const code = toHkCode(symbol);
            const klines = await sdk.getHKHistoryKline(code, {
                period: interval,
                adjust: "qfq",
                startDate: sdkStart,
                endDate: sdkEnd,
            });
            records = klines.map(mapHkUsKline);
        }
        else {
            // US — stock-sdk needs "105.AAPL" format; we need to resolve the market code.
            // Use yahoo-finance2 for US history as it accepts plain tickers.
            const ticker = symbol.trim().toUpperCase();
            const yfInterval = interval === "daily" ? "1d" : interval === "weekly" ? "1wk" : "1mo";
            const hist = await yf.historical(ticker, {
                period1: startDate ?? toIsoDate(sdkStart),
                period2: endDate ?? toIsoDate(sdkEnd),
                interval: yfInterval,
            });
            records = hist.map((h) => ({
                date: h.date.toISOString().slice(0, 10),
                open: safeFloat(h.open),
                high: safeFloat(h.high),
                low: safeFloat(h.low),
                close: safeFloat(h.close),
                volume: h.volume,
            }));
        }
        // Limit rows (most recent)
        if (records.length > limit) {
            records = records.slice(-limit);
        }
        const displaySymbol = market === "A" ? bareACode(symbol) : symbol.trim().toUpperCase();
        return makeSuccess({
            symbol: displaySymbol,
            market,
            data: records,
            summary: buildSummary(records),
        });
    }
    catch (e) {
        const msg = e instanceof Error ? e.message : String(e);
        logger.warn(`stock_data history failed: ${symbol}`, { error: msg });
        return makeError(msg);
    }
}
// ---------------------------------------------------------------------------
// Financials (via yahoo-finance2)
// ---------------------------------------------------------------------------
async function handleFinancials(symbol, forcedMarket, reportType) {
    const market = forcedMarket !== "auto" ? forcedMarket : detectMarket(symbol);
    // Build yahoo-finance2 ticker
    let ticker;
    if (market === "A")
        ticker = toYfATicker(symbol);
    else if (market === "HK")
        ticker = toYfHkTicker(symbol);
    else
        ticker = symbol.trim().toUpperCase();
    try {
        const result = { symbol: ticker, market, report_type: reportType };
        const fiveYearsAgo = new Date(Date.now() - 5 * 365 * 86400000);
        if (reportType === "summary") {
            const modules = await yf.quoteSummary(ticker, {
                modules: ["defaultKeyStatistics", "financialData", "summaryProfile", "summaryDetail"],
            });
            const ks = modules.defaultKeyStatistics;
            const fd = modules.financialData;
            const sp = modules.summaryProfile;
            const sd = modules.summaryDetail;
            result.metrics = {
                name: ticker,
                sector: sp?.sector ?? "",
                industry: sp?.industry ?? "",
                pe_forward: safeFloat(ks?.forwardPE ?? null),
                pb: safeFloat(ks?.priceToBook ?? null),
                roe: safeFloat(fd?.returnOnEquity ?? null),
                roa: safeFloat(fd?.returnOnAssets ?? null),
                gross_margin: safeFloat(fd?.grossMargins ?? null),
                operating_margin: safeFloat(fd?.operatingMargins ?? null),
                net_margin: safeFloat(fd?.profitMargins ?? null),
                debt_to_equity: safeFloat(fd?.debtToEquity ?? null),
                current_ratio: safeFloat(fd?.currentRatio ?? null),
                revenue_growth: safeFloat(fd?.revenueGrowth ?? null),
                earnings_growth: safeFloat(fd?.earningsGrowth ?? null),
                market_cap: safeFloat(sd?.marketCap ?? null),
                enterprise_value: safeFloat(ks?.enterpriseValue ?? null),
            };
        }
        else if (reportType === "income") {
            const ts = await yf.fundamentalsTimeSeries(ticker, {
                period1: fiveYearsAgo,
                type: "annual",
                module: "financials",
            });
            result.income_statements = ts.slice(0, 4).map((row) => {
                const r = row;
                const dateVal = r.date;
                const period = dateVal instanceof Date ? dateVal.toISOString().slice(0, 10) : "";
                return {
                    period,
                    total_revenue: safeFloat(typeof r.totalRevenue === "number" ? r.totalRevenue : null),
                    gross_profit: safeFloat(typeof r.grossProfit === "number" ? r.grossProfit : null),
                    operating_income: safeFloat(typeof r.operatingIncome === "number" ? r.operatingIncome : null),
                    net_income: safeFloat(typeof r.netIncome === "number" ? r.netIncome : null),
                    ebitda: safeFloat(typeof r.EBITDA === "number" ? r.EBITDA : null),
                };
            });
        }
        else if (reportType === "balance") {
            const ts = await yf.fundamentalsTimeSeries(ticker, {
                period1: fiveYearsAgo,
                type: "annual",
                module: "balance-sheet",
            });
            result.balance_sheets = ts.slice(0, 4).map((row) => {
                const r = row;
                const dateVal = r.date;
                const period = dateVal instanceof Date ? dateVal.toISOString().slice(0, 10) : "";
                return {
                    period,
                    total_assets: safeFloat(typeof r.totalAssets === "number" ? r.totalAssets : null),
                    total_liabilities: safeFloat(typeof r.totalLiabilitiesNetMinorityInterest === "number" ? r.totalLiabilitiesNetMinorityInterest : null),
                    equity: safeFloat(typeof r.stockholdersEquity === "number" ? r.stockholdersEquity : null),
                    cash: safeFloat(typeof r.cashAndCashEquivalents === "number" ? r.cashAndCashEquivalents : null),
                    total_debt: safeFloat(typeof r.totalDebt === "number" ? r.totalDebt : null),
                };
            });
        }
        else if (reportType === "cashflow") {
            const ts = await yf.fundamentalsTimeSeries(ticker, {
                period1: fiveYearsAgo,
                type: "annual",
                module: "cash-flow",
            });
            result.cashflow_statements = ts.slice(0, 4).map((row) => {
                const r = row;
                const dateVal = r.date;
                const period = dateVal instanceof Date ? dateVal.toISOString().slice(0, 10) : "";
                return {
                    period,
                    operating_cf: safeFloat(typeof r.operatingCashFlow === "number" ? r.operatingCashFlow : null),
                    investing_cf: safeFloat(typeof r.investingCashFlow === "number" ? r.investingCashFlow : null),
                    financing_cf: safeFloat(typeof r.financingCashFlow === "number" ? r.financingCashFlow : null),
                    free_cf: safeFloat(typeof r.freeCashFlow === "number" ? r.freeCashFlow : null),
                    capex: safeFloat(typeof r.capitalExpenditure === "number" ? r.capitalExpenditure : null),
                };
            });
        }
        return makeSuccess(result);
    }
    catch (e) {
        const msg = e instanceof Error ? e.message : String(e);
        logger.warn(`stock_data financials failed: ${ticker}`, { error: msg });
        return makeError(msg);
    }
}
// ---------------------------------------------------------------------------
// Search (via stock-sdk)
// ---------------------------------------------------------------------------
async function handleSearch(keyword, forcedMarket, limit) {
    try {
        const results = await sdk.search(keyword);
        // Filter by market first, then apply limit
        const filtered = forcedMarket === "all"
            ? results
            : results.filter((r) => {
                if (forcedMarket === "A")
                    return r.market === "sh" || r.market === "sz";
                if (forcedMarket === "HK")
                    return r.market === "hk";
                if (forcedMarket === "US")
                    return r.market === "us";
                return true;
            });
        const limited = filtered.slice(0, limit);
        return makeSuccess({
            keyword,
            results: limited.map((r) => {
                let normalizedMarket = r.market;
                if (r.market === "sh" || r.market === "sz")
                    normalizedMarket = "A";
                else if (r.market === "hk")
                    normalizedMarket = "HK";
                else if (r.market === "us")
                    normalizedMarket = "US";
                // Strip provider prefix (e.g. "sh600519" → "600519", "hk00700" → "00700", "usaapl.oq" → "AAPL")
                const symbol = bareSearchCode(r.code, normalizedMarket);
                return {
                    symbol,
                    name: r.name,
                    market: normalizedMarket,
                    type: r.type,
                };
            }),
            total_found: filtered.length,
        });
    }
    catch (e) {
        const msg = e instanceof Error ? e.message : String(e);
        logger.warn(`stock_data search failed: ${keyword}`, { error: msg });
        return makeError(msg);
    }
}
export async function queryStockData(args) {
    const { action } = args;
    switch (action) {
        case "quote": {
            if (!args.symbols) {
                return makeError("'symbols' is required for quote action");
            }
            const marketArg = args.market === "all" ? "auto" : args.market;
            return handleQuote(args.symbols, marketArg ?? "auto");
        }
        case "history": {
            if (!args.symbol) {
                return makeError("'symbol' is required for history action");
            }
            const marketArg = args.market === "all" ? "auto" : args.market;
            return handleHistory(args.symbol, marketArg ?? "auto", args.period ?? "3mo", args.interval ?? "daily", args.start_date, args.end_date, args.limit ?? 250);
        }
        case "financials": {
            if (!args.symbol) {
                return makeError("'symbol' is required for financials action");
            }
            const marketArg = args.market === "all" ? "auto" : args.market;
            return handleFinancials(args.symbol, marketArg ?? "auto", args.report_type ?? "summary");
        }
        case "search": {
            if (!args.keyword) {
                return makeError("'keyword' is required for search action");
            }
            const marketArg = args.market === "auto" ? "all" : args.market;
            return handleSearch(args.keyword, marketArg ?? "all", args.limit ?? 20);
        }
        default:
            return makeError(`unknown action '${String(action)}'`);
    }
}
export const stockDataTool = tool("stock_data", "Query stock market data across A-shares, HK, and US markets. " +
    "Supports 4 actions: " +
    "'quote' — real-time/latest quotes; " +
    "'history' — historical OHLCV candlestick data; " +
    "'financials' — financial metrics and statements; " +
    "'search' — search stocks by name or code keyword. " +
    "Market is auto-detected: 6-digit numeric → A-share, .HK or 4-5 digit → HK, letters → US.", {
    action: z
        .enum(["quote", "history", "financials", "search"])
        .describe("Action to perform: " +
        "'quote' for real-time prices, " +
        "'history' for OHLCV K-line data, " +
        "'financials' for financial statements/metrics, " +
        "'search' to find stocks by keyword"),
    // -- Common fields --
    symbols: z
        .string()
        .optional()
        .describe("Comma-separated stock codes for 'quote' action. " +
        "Examples: '600519' (A-share), '0981.HK' (HK), 'AAPL' (US), '600519,0981.HK,AAPL' (mixed)"),
    symbol: z
        .string()
        .optional()
        .describe("Single stock code for 'history' and 'financials' actions. " +
        "Examples: '600519', '0981.HK', 'AAPL'"),
    market: z
        .enum(["A", "HK", "US", "auto", "all"])
        .optional()
        .default("auto")
        .describe("Market to query. 'auto' detects from symbol format. " +
        "'all' is only valid for 'search' action to search all markets."),
    // -- History-specific fields --
    period: z
        .enum(["1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "max"])
        .optional()
        .describe("Time period for 'history' (default '3mo', ignored if start_date provided)"),
    interval: z
        .enum(["daily", "weekly", "monthly"])
        .optional()
        .describe("Data interval for 'history' (default 'daily')"),
    start_date: z
        .string()
        .optional()
        .describe("Start date YYYY-MM-DD for 'history' (overrides period)"),
    end_date: z
        .string()
        .optional()
        .describe("End date YYYY-MM-DD for 'history'"),
    // -- Financials-specific fields --
    report_type: z
        .enum(["summary", "income", "balance", "cashflow"])
        .optional()
        .describe("For 'financials': 'summary' for key ratios (default), " +
        "'income'/'balance'/'cashflow' for detailed statements"),
    // -- Search-specific fields --
    keyword: z
        .string()
        .optional()
        .describe("Search keyword for 'search' action (e.g. '茅台', '半导体', 'AAPL')"),
    // -- Shared --
    limit: z
        .number()
        .optional()
        .describe("Max rows/results to return (history default 250, search default 20)"),
}, queryStockData);
//# sourceMappingURL=stock-data.js.map