#!/usr/bin/env python3
"""MoFin Dashboard - 莫荷持仓情报可视化系统"""

import base64
import json
import os
import re
import uuid
import urllib.request
from datetime import datetime
from pathlib import Path

from flask import Flask, jsonify, send_from_directory, request

# 提示词管理模块
from prompt_manager.dashboard_views import register_routes

app = Flask(__name__, static_folder="static", static_url_path="")

DATA_DIR = Path(__file__).parent / "data"
UPLOAD_DIR = Path(__file__).parent / "uploads"

# Hermes Gateway
GATEWAY = "http://localhost:8642/v1/chat/completions"
API_KEY = "hermes123"


def _load_json(path, default=None):
    try:
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        return {} if default is None else default


def _load_from_db(query_func, json_path, default=None):
    """优先从 SQLite 读取，失败回退 JSON"""
    try:
        from mofin_db import get_conn
        conn = get_conn()
        result = query_func(conn)
        conn.close()
        if result:
            return result
    except Exception:
        pass
    return _load_json(json_path, default)


def _save_json(path, data):
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


# ── API 路由 ──────────────────────────────────────────

@app.route("/")
def index():
    return send_from_directory(app.static_folder, "index.html")


@app.route("/api/portfolio")
def api_portfolio():
    """持仓列表"""
    try:
        from mofin_db import get_conn, query_holdings, query_portfolio_summary
        conn = get_conn()
        holdings = query_holdings(conn)
        summary = query_portfolio_summary(conn)
        conn.close()
        if holdings:
            data = dict(summary)
            data["holdings"] = holdings
            return jsonify(data)
    except Exception:
        pass
    return jsonify(_load_json(DATA_DIR / "portfolio.json"))


@app.route("/api/watchlist")
def api_watchlist():
    """自选列表"""
    try:
        from mofin_db import get_conn, query_watchlist
        conn = get_conn()
        stocks = query_watchlist(conn)
        conn.close()
        if stocks:
            return jsonify({"stocks": stocks})
    except Exception:
        pass
    return jsonify(_load_json(DATA_DIR / "watchlist.json"))


@app.route("/api/overview")
def api_overview():
    """概览数据"""
    try:
        from mofin_db import get_conn, query_holdings, query_portfolio_summary, query_latest_market
        conn = get_conn()
        holdings = query_holdings(conn)
        summary = query_portfolio_summary(conn)
        market = query_latest_market(conn)
        conn.close()
        if holdings:
            total_assets = summary.get("total_assets", 0) or 0
            stock_value = summary.get("stock_value", 0) or 0
            cash = summary.get("cash", 0) or 0
            position_pct = summary.get("position_pct", 0) or 0
            total_pnl = summary.get("total_pnl", 0) or 0
            top_movers = sorted(
                [h for h in holdings if abs(h.get("change_pct", 0) or 0) >= 3],
                key=lambda x: abs(x.get("change_pct", 0) or 0), reverse=True)[:5]
            return jsonify({
                "total_assets": total_assets, "stock_value": stock_value,
                "cash": cash, "position_pct": position_pct, "total_pnl": total_pnl,
                "top_movers": top_movers, "market": market,
                "alerts": _load_json(DATA_DIR / "alerts.json", [])[:10],
                "updated_at": summary.get("updated_at", ""),
            })
    except Exception:
        pass
    portfolio = _load_json(DATA_DIR / "portfolio.json", [])
    market = _load_json(DATA_DIR / "market.json", {})
    alerts = _load_json(DATA_DIR / "alerts.json", [])
    total_assets = portfolio.get("total_assets", 0)
    stock_value = portfolio.get("stock_value", 0)
    cash = portfolio.get("cash", 0)
    position_pct = portfolio.get("position_pct", 0)
    total_pnl = portfolio.get("total_pnl", 0)
    holdings = portfolio.get("holdings", [])
    top_movers = sorted(
        [h for h in holdings if abs(h.get("change_pct", 0)) >= 3],
        key=lambda x: abs(x.get("change_pct", 0)), reverse=True)[:5]
    return jsonify({
        "total_assets": total_assets, "stock_value": stock_value,
        "cash": cash, "position_pct": position_pct, "total_pnl": total_pnl,
        "top_movers": top_movers, "market": market, "alerts": alerts[:10],
        "updated_at": portfolio.get("updated_at", ""),
    })


@app.route("/api/reports")
def api_reports():
    """历史报告列表"""
    reports_dir = DATA_DIR / "reports"
    reports = []
    if reports_dir.exists():
        for f in sorted(reports_dir.iterdir(), reverse=True)[:100]:
            if f.suffix == ".json":
                data = _load_json(f)
                reports.append({
                    "id": f.stem,
                    "title": data.get("title", f.stem),
                    "type": data.get("type", "未知"),
                    "created_at": data.get("created_at", ""),
                    "summary": data.get("summary", ""),
                })
    return jsonify(reports)


@app.route("/api/report/<report_id>")
def api_report(report_id):
    """单个报告详情"""
    # Try exact file first
    path = DATA_DIR / "reports" / f"{report_id}.json"
    if path.exists():
        return jsonify(_load_json(path))
    # Try prefix match
    reports_dir = DATA_DIR / "reports"
    if reports_dir.exists():
        for f in reports_dir.iterdir():
            if f.stem.startswith(report_id) and f.suffix == ".json":
                return jsonify(_load_json(f))
    return jsonify({"error": "report not found"}), 404


@app.route("/api/stock/<code>")
def api_stock(code):
    """个股详情 + 操作建议历史"""
    stock_data = _load_json(DATA_DIR / "stocks" / f"{code}.json", {})
    return jsonify(stock_data)


@app.route("/api/market")
def api_market():
    """市场观察"""
    try:
        from mofin_db import get_conn, query_latest_market
        conn = get_conn()
        data = query_latest_market(conn)
        conn.close()
        if data and data.get("sectors"):
            return jsonify(data)
    except Exception:
        pass
    return jsonify(_load_json(DATA_DIR / "market.json", {}))


# ── 数据写入API（供 cron/update_data.py 调用） ──────────

@app.route("/api/update/portfolio", methods=["POST"])
def update_portfolio():
    data = request.get_json(force=True)
    _save_json(DATA_DIR / "portfolio.json", data)
    return jsonify({"status": "ok"})


@app.route("/api/update/watchlist", methods=["POST"])
def update_watchlist():
    data = request.get_json(force=True)
    _save_json(DATA_DIR / "watchlist.json", data)
    return jsonify({"status": "ok"})


@app.route("/api/update/report", methods=["POST"])
def update_report():
    data = request.get_json(force=True)
    report_id = data.pop("_id", datetime.now().strftime("%Y%m%d_%H%M%S"))
    data["created_at"] = data.get("created_at", datetime.now().isoformat())
    _save_json(DATA_DIR / "reports" / f"{report_id}.json", data)
    return jsonify({"status": "ok", "id": report_id})


@app.route("/api/update/stock/<code>", methods=["POST"])
def update_stock(code):
    data = request.get_json(force=True)
    existing = _load_json(DATA_DIR / "stocks" / f"{code}.json", {})
    history = existing.get("history", [])
    if data.get("entry"):
        history.append({
            "time": datetime.now().isoformat(),
            "price": data.get("price"),
            "recommendation": data.get("recommendation"),
            "stop_loss": data.get("stop_loss"),
            "take_profit": data.get("take_profit"),
            "reason": data.get("reason"),
        })
    existing.update(data)
    existing["history"] = history[-50:]
    _save_json(DATA_DIR / "stocks" / f"{code}.json", existing)
    return jsonify({"status": "ok"})


@app.route("/api/update/market", methods=["POST"])
def update_market():
    data = request.get_json(force=True) or {}
    _save_json(DATA_DIR / "market.json", data)
    return jsonify({"status": "ok"})


# ── 知微分析结果写入API ──
@app.route("/api/analysis/batch", methods=["POST"])
def analysis_batch():
    """接收知微cron的分析结果，写回持仓/自选JSON的analysis字段"""
    data = request.get_json(force=True) or {}
    
    # 更新持仓
    if "holdings" in data:
        pf = _load_json(DATA_DIR / "portfolio.json", {})
        idx = {h["code"]: i for i, h in enumerate(pf.get("holdings", []))}
        for item in data["holdings"]:
            code = item.get("code", "")
            if code not in idx:
                continue
            h = pf["holdings"][idx[code]]
            h["analysis"] = {
                "suggestion": item.get("suggestion"),
                "stop_loss": item.get("stop_loss"),
                "take_profit": item.get("take_profit"),
                "buy_zone_low": item.get("buy_zone_low"),
                "buy_zone_high": item.get("buy_zone_high"),
                "position_suggested": item.get("position_suggested"),
                "reason": item.get("reason"),
                "updated_at": datetime.now().isoformat(),
            }
        _save_json(DATA_DIR / "portfolio.json", pf)
    
    # 更新自选
    if "watchlist" in data:
        wl = _load_json(DATA_DIR / "watchlist.json", {})
        idx = {s["code"]: i for i, s in enumerate(wl.get("stocks", []))}
        for item in data["watchlist"]:
            code = item.get("code", "")
            if code not in idx:
                continue
            s = wl["stocks"][idx[code]]
            s["analysis"] = {
                "buy_low": item.get("buy_low"),
                "buy_high": item.get("buy_high"),
                "position_recommend": item.get("position_recommend"),
                "reason": item.get("reason"),
                "updated_at": datetime.now().isoformat(),
            }
        _save_json(DATA_DIR / "watchlist.json", wl)
    
    return jsonify({"status": "ok", "updated_at": datetime.now().isoformat()})


# ── 操作决策库API ──
@app.route("/api/decisions", methods=["GET"])
def get_decisions():
    """返回决策库数据，统一新旧格式"""
    raw = _load_json(DATA_DIR / "decisions.json", {"decisions": []})
    decisions = raw.get("decisions", [])
    if not decisions and isinstance(raw, list):
        decisions = raw

    # portfolio 用来判断是持仓还是自选
    portfolio = _load_json(DATA_DIR / "portfolio.json", {"holdings": []})
    watchlist = _load_json(DATA_DIR / "watchlist.json", {"stocks": []})
    holding_codes = {h.get("code","") for h in portfolio.get("holdings",[])}
    watch_codes = {s.get("code","") for s in watchlist.get("stocks",[])}

    normalized = []
    for d in decisions:
        if not isinstance(d, dict):
            continue

        # 检测新旧格式：新格式有 stop_loss 顶层字段，旧格式有 trigger 对象
        is_new = "stop_loss" in d and "trigger" not in d

        if is_new:
            code = d.get("code", "")
            name = d.get("name", "")
            price = d.get("price", 0)
            sl = d.get("stop_loss")
            tp = d.get("take_profit")
            el = d.get("entry_low")
            eh = d.get("entry_high")
            ts = d.get("tech_snapshot", "")

            # type: 持仓还是自选
            if code in holding_codes:
                dtype = "持仓策略"
            elif code in watch_codes:
                dtype = "自选策略"
            else:
                dtype = "—"

            # 判断 active
            status_raw = d.get("status", "")
            status = "active" if status_raw in ("active", "updated", "") else "superseded"

            # trigger 对象
            entry_zone_str = ""
            if el and eh:
                entry_zone_str = f"¥{el}~¥{eh}"
            elif el:
                entry_zone_str = f"≥¥{el}"

            trigger = {}
            if sl:
                trigger["stop_loss"] = f"¥{sl}" if isinstance(sl, (int,float)) else str(sl)
            if tp:
                trigger["take_profit"] = f"¥{tp}" if isinstance(tp, (int,float)) else str(tp)
            if entry_zone_str:
                trigger["entry_zone"] = entry_zone_str

            # current
            current = ""
            if price:
                current = f"现价¥{price}" if code and not code.startswith(("0","1")) else f"¥{price}"

            # zone_breach
            zone_breach = d.get("zone_breach", "")

            # updated_reason
            note = d.get("note", "")
            timing = d.get("timing_signal", "")
            reason_parts = []
            if note:
                reason_parts.append(note)
            if timing and timing != "neutral":
                reason_parts.append(f"时机:{timing}")
            if d.get("rr_ratio"):
                reason_parts.append(f"盈亏比:{d['rr_ratio']}")

            # advice_timeline - 从新格式重建
            timeline = []

            entry = {
                "code": code,
                "name": name,
                "type": dtype,
                "status": status,
                "tag": d.get("tag", ""),
                "action": d.get("action", ""),
                "trigger": trigger,
                "current": current,
                "zone_breach": zone_breach,
                "updated_reason": " | ".join(reason_parts) if reason_parts else "",
                "advice_timeline": timeline,
                "changelog": d.get("changelog", []),
                "execution": d.get("execution", {}),
                "analysis": d.get("analysis", {}),
                "tech_snapshot": ts,
                "timestamp": d.get("timestamp", ""),
                "updated_by": "知微",
            }
            # 保留原始数据供前端扩展
            entry["_raw_action"] = d.get("action", "")
            normalized.append(entry)
        else:
            # 旧格式：已有 trigger 等字段，直接保留
            entry = dict(d)
            # 确保 status 正确
            if entry.get("status") not in ("active", "superseded"):
                entry["status"] = "active"
            if not entry.get("type"):
                code = entry.get("code", "")
                if code in holding_codes:
                    entry["type"] = "持仓策略"
                elif code in watch_codes:
                    entry["type"] = "自选策略"
                else:
                    entry["type"] = "—"
            normalized.append(entry)

    # 添加 execution 和 analysis 信息，按执行状态排序
    for n in normalized:
        code = n.get("code", "")
        # 从原始数据中找到 execution 和 analysis
        raw_entry = next((d for d in decisions if isinstance(d, dict) and d.get("code") == code), {})
        n["execution"] = raw_entry.get("execution", {"status": "none"})
        n["analysis"] = raw_entry.get("analysis", {})

    # 排序规则：推荐>执行中>观察>无标签
    def sort_key(x):
        tag = x.get("tag", "")
        exec_status = x.get("execution", {}).get("status", "none")
        # 标签优先级（current_recommend才靠前，active_manual只是记录不升序）
        tag_order = {"current_recommend": 0}
        tag_priority = tag_order.get(tag, 50)
        # 执行状态优先级
        exec_order = {"partial_exit": 0, "executing": 1, "observing": 2, "none": 99}
        exec_priority = exec_order.get(exec_status, 99)
        # 组合：先按标签排，再按执行状态排
        return (tag_priority, exec_priority, x.get("code", ""))

    normalized.sort(key=sort_key)

    return jsonify({
        "decisions": normalized,
        "total": len(normalized),
        "regenerated_at": raw.get("regenerated_at", ""),
    })


@app.route("/api/decisions/add", methods=["POST"])
def add_decision():
    """新增/更新一条决策（新格式）"""
    data = request.get_json(force=True) or {}
    code = data.get("code", "")
    if not code:
        return jsonify({"status": "error", "message": "code required"}), 400

    d = _load_json(DATA_DIR / "decisions.json", {"decisions": []})

    # 同一股票旧决策标记为superseded
    for e in d["decisions"]:
        if e["code"] == code and e.get("status") in ("active", "updated"):
            e["status"] = "superseded"

    entry = {
        "code": code,
        "name": data.get("name", ""),
        "price": data.get("price", 0),
        "action": data.get("action", ""),
        "stop_loss": data.get("stop_loss"),
        "take_profit": data.get("take_profit"),
        "entry_low": data.get("entry_low"),
        "entry_high": data.get("entry_high"),
        "tech_snapshot": data.get("tech_snapshot", ""),
        "timing_signal": data.get("timing_signal", ""),
        "rr_ratio": data.get("rr_ratio"),
        "tag": data.get("tag", ""),
        "note": data.get("note", ""),
        "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
        "updated_reason": data.get("updated_reason", ""),
        "status": "updated",
        "changelog": data.get("changelog", []),
        "execution": data.get("execution", {"status": "none"}),
        "analysis": data.get("analysis", {}),
    }
    d["decisions"].append(entry)
    _save_json(DATA_DIR / "decisions.json", d)
    return jsonify({"status": "ok", "entry": entry})


@app.route("/api/decisions/tag", methods=["POST"])
def set_decision_tag():
    """设置/清除某只股票的推荐标签"""
    data = request.get_json(force=True) or {}
    code = data.get("code", "")
    tag = data.get("tag", "")  # 'current_recommend', 'active_manual', or '' to clear
    if not code:
        return jsonify({"status": "error", "message": "code required"}), 400
    
    d = _load_json(DATA_DIR / "decisions.json", {"decisions": []})
    found = False
    for e in d.get("decisions", []):
        if e.get("code") == code:
            e["tag"] = tag
            e["tag_updated"] = datetime.now().isoformat()
            found = True
            break
    
    if not found:
        return jsonify({"status": "error", "message": f"stock {code} not found"}), 404
    
    _save_json(DATA_DIR / "decisions.json", d)
    return jsonify({"status": "ok", "code": code, "tag": tag})


@app.route("/api/decisions/pending")
def get_pending_decisions():
    """返回所有有未确认建议的条目"""
    d = _load_json(DATA_DIR / "decisions.json", {"decisions": []})
    pending = []
    for entry in d["decisions"]:
        timeline = entry.get("advice_timeline", [])
        unconfirmed = [a for a in timeline if a.get("status") in (None, "pending")]
        if unconfirmed:
            pending.append({
                "code": entry["code"],
                "name": entry["name"],
                "current": entry.get("current", ""),
                "pending_advice": unconfirmed,
            })
    return jsonify(pending)


@app.route("/api/advice/record", methods=["POST"])
def record_advice():
    """记录一条分析建议到 decisions.json 的 advice_timeline"""
    data = request.get_json(force=True) or {}
    code = data.get("code", "")
    if not code:
        return jsonify({"status": "error", "message": "code required"}), 400

    d = _load_json(DATA_DIR / "decisions.json", {"decisions": []})

    entry = None
    for e in d["decisions"]:
        if e["code"] == code and e["status"] == "active":
            entry = e
            break

    if not entry:
        return jsonify({"status": "error", "message": f"no active decision for {code}"}), 404

    timeline = entry.setdefault("advice_timeline", [])
    advice = {
        "date": datetime.now().strftime("%Y-%m-%d %H:%M"),
        "direction": data.get("direction", "持有"),
        "price": data.get("price", ""),
        "summary": data.get("summary", ""),
        "status": "pending",
    }
    timeline.append(advice)
    _save_json(DATA_DIR / "decisions.json", d)
    return jsonify({"status": "ok", "advice": advice})


@app.route("/api/advice/confirm", methods=["POST"])
def confirm_advice():
    """确认/忽略一条建议"""
    data = request.get_json(force=True) or {}
    code = data.get("code", "")
    idx = data.get("index", -1)
    action = data.get("action", "confirmed")  # confirmed | ignored

    d = _load_json(DATA_DIR / "decisions.json", {"decisions": []})
    for e in d["decisions"]:
        if e["code"] == code and e["status"] == "active":
            timeline = e.get("advice_timeline", [])
            if 0 <= idx < len(timeline):
                timeline[idx]["status"] = action
                _save_json(DATA_DIR / "decisions.json", d)
                return jsonify({"status": "ok"})
    return jsonify({"status": "error", "message": "not found"}), 404


# ── 准确率统计API ──
@app.route("/api/stats/accuracy")
def get_accuracy_stats():
    data = _load_json(DATA_DIR / "accuracy_stats.json", {})
    return jsonify(data)


# ── 策略评估API ──
@app.route("/api/evaluation")
def get_evaluation():
    """返回所有策略的双维度评估结果"""
    # 主数据源：evaluation.json
    eval_data = _load_json(DATA_DIR / "evaluation.json", {})
    strategies = eval_data.get("strategies", [])
    if strategies:
        return jsonify(strategies)

    # 备选：从 decisions.json 的 evaluation 字段读取（尚未反写时的兼容）
    decisions = _load_json(DECISIONS_PATH if 'DECISIONS_PATH' in dir() else DATA_DIR / "decisions.json", {"decisions": []})
    evals = []
    for d in decisions.get("decisions", []):
        e = d.get("evaluation", [])
        if e:
            evals.append({
                "code": d["code"],
                "name": d["name"],
                "type": d.get("type", ""),
                "current": d.get("current", ""),
                "evaluations": e,
            })
    return jsonify(evals)


@app.route("/api/evaluation/trigger", methods=["POST"])
def trigger_evaluation():
    """手动触发策略评估"""
    import subprocess
    try:
        r = subprocess.run(
            ["python3", str(DATA_DIR.parent / "strategy_evaluator.py")],
            capture_output=True, timeout=60, text=True,
        )
        return jsonify({"status": "ok", "output": r.stdout, "error": r.stderr})
    except Exception as e:
        return jsonify({"status": "error", "message": str(e)}), 500


# ── 策略反馈API ──
@app.route("/api/feedback")
def get_feedback():
    data = _load_json(DATA_DIR / "strategy_feedback.json", {})
    return jsonify(data)


# ── 持仓截图上传与解析 ────────────────────────────────


@app.route("/upload")
def upload_page():
    return send_from_directory(app.static_folder, "upload.html")


def _ocr_image(image_path):
    """用Tesseract OCR提取图片中的文字（预处理优化中文表格识别）"""
    from PIL import Image, ImageEnhance, ImageFilter
    import pytesseract

    img = Image.open(image_path)

    # 预处理：放大 + 锐化 + 二值化，提升小字识别率
    w, h = img.size
    if w < 2000 or h < 2000:
        scale = max(2, 2000 // min(w, h))
        img = img.resize((w * scale, h * scale), Image.LANCZOS)

    # 转灰度
    img = img.convert("L")

    # 增强对比度
    enhancer = ImageEnhance.Contrast(img)
    img = enhancer.enhance(2.0)

    # 锐化
    img = img.filter(ImageFilter.SHARPEN)

    # 二值化（自适应阈值）
    threshold = 128
    img = img.point(lambda x: 255 if x > threshold else 0)

    # OCR：chip_sim+eng，PSM 6（统一文本块）
    text = pytesseract.image_to_string(
        img,
        lang="chi_sim+eng",
        config="--psm 6 --oem 3",
    )
    return text.strip()


ANALYZE_PROMPT = """你是股票持仓数据分析助手。以下是用户上传的持仓/自选截图经过OCR提取的文字，请从中提取所有股票信息。

判断这是「持仓截图」还是「自选截图」：
- 持仓截图：每支股票有"证券数量"（持股数）、成本价、盈亏
- 自选截图：只有股票列表和价格，没有持股数/成本

股票代码格式：
- A股：6位数字（如 600519, 000858, 300750）
- 港股：纯数字代码（如 0700, 3690, 1211），不带HK前缀

⚠️ 重要：截图顶部通常有汇总数据，如总资产、股票市值、可用资金、当日盈亏等。
如果OCR文字中有这些汇总数字，请一并提取到JSON的summary字段中。
不要自己计算汇总值，直接从OCR原文中提取。

请严格按照以下JSON格式回复，只输出JSON：

```json
{
  "type": "portfolio" 或 "watchlist",
  "summary": {
    "total_assets": "总资产数字（可选，从截图中提取）",
    "stock_value": "股票市值/持仓市值数字（可选，从截图中提取）",
    "cash": "可用资金/现金数字（可选，从截图中提取）",
    "day_pnl": "当日盈亏金额（可选，从截图中提取）"
  },
  "stocks": [
    {
      "code": "股票代码",
      "name": "股票名称（中文）",
      "price": "现价（数字）",
      "shares": "持股数量（数字，持仓截图才有）",
      "cost": "成本价（数字，持仓截图才有）",
      "pnl": "盈亏百分比如+15.1%（持仓截图才有）",
      "position_pct": "仓位占比数字如12.5（可选）"
    }
  ]
}
```

OCR原文：
"""


@app.route("/api/upload/analyze", methods=["POST"])
def upload_analyze():
    """接收图片，OCR提取文字 → LLM解析结构化数据"""
    if "image" not in request.files:
        return jsonify({"error": "请上传图片"}), 400

    f = request.files["image"]
    if not f.filename:
        return jsonify({"error": "空文件"}), 400

    # 保存到临时目录
    UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
    ext = Path(f.filename).suffix or ".png"
    save_path = UPLOAD_DIR / f"{uuid.uuid4().hex}{ext}"
    f.save(str(save_path))

    try:
        # 第一步：OCR提取文字
        raw_text = _ocr_image(str(save_path))
        if not raw_text:
            return jsonify({"error": "OCR未识别到文字，请确认图片清晰"}), 400
    except Exception as e:
        os.unlink(str(save_path))
        return jsonify({"error": f"OCR失败: {e}"}), 500

    # 第二步：LLM解析结构化数据（走文本API，不走视觉）
    llm_text = _llm_parse(raw_text, ANALYZE_PROMPT)

    os.unlink(str(save_path))

    # 从LLM回复中提取JSON
    json_match = re.search(r"```(?:json)?\s*({.*?})\s*```", llm_text, re.DOTALL)
    if json_match:
        try:
            parsed = json.loads(json_match.group(1))
        except json.JSONDecodeError:
            return jsonify({"error": f"LLM解析JSON失败: {llm_text[:500]}"}), 500
    else:
        # 尝试直接找JSON（没被代码块包裹）
        try:
            parsed = json.loads(llm_text)
        except json.JSONDecodeError:
            return jsonify({"error": f"未提取到结构化数据: {raw_text[:300]}...\n\nLLM回复: {llm_text[:500]}"}), 500

    return jsonify(parsed)


def _llm_parse(text, prompt_template):
    """发送OCR文本到Hermes LLM解析，返回JSON字符串"""
    payload = json.dumps({
        "model": "hermes-agent",
        "messages": [
            {"role": "system", "content": "你是一个数据提取助手。从OCR文字中提取结构化JSON数据。"},
            {"role": "user", "content": prompt_template + "\n" + text},
        ],
        "max_tokens": 4096,
    }).encode()

    req = urllib.request.Request(GATEWAY, data=payload, method="POST")
    req.add_header("Content-Type", "application/json")
    req.add_header("Authorization", f"Bearer {API_KEY}")
    req.add_header("X-Hermes-Session-Id", "upload-ocr-parse")

    try:
        resp = urllib.request.urlopen(req, timeout=120)
        data = json.loads(resp.read())
        return data.get("choices", [{}])[0].get("message", {}).get("content", "")
    except Exception as e:
        return f"ERROR: {e}"


@app.route("/api/upload/confirm", methods=["POST"])
def upload_confirm():
    """确认解析结果，更新数据文件"""
    data = request.get_json(force=True)
    stocks = data.get("stocks", [])
    doc_type = data.get("type", "portfolio")

    # 尝试获取实时行情补充数据
    try:
        codes = [s["code"] for s in stocks if s.get("code")]
        if codes:
            qs = " ".join(
                f"hk{c}" if len(c) == 5  # 港股5位代码
                else f"sz{c}" if c.startswith("0") or c.startswith("3")
                else f"sh{c}" if c.startswith("6")
                else f"hk{c}"
                for c in codes
            )
            url = f"https://qt.gtimg.cn/q={qs}"
            req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
            resp = urllib.request.urlopen(req, timeout=10)
            qt_text = resp.read().decode("gbk", errors="replace")
            # map realtime prices
            for stock in stocks:
                code = stock.get("code", "")
                prefix = "hk" if len(code) == 5 else "sz" if code.startswith(("0","3")) else "sh" if code.startswith("6") else "hk"
                # 腾讯 API 格式: prefix+code="市场~名称~代码~当前价~昨收~今开~成交量~..."
                m = re.search(rf'{prefix}{code}="([^"]+)"', qt_text)
                if m:
                    fields = m.group(1).split('~')
                    name = fields[1]
                    price = fields[3]  # 当前价
                    if not stock.get("price"):
                        stock["price"] = price
                    if not stock.get("name"):
                        stock["name"] = name
    except:
        pass  # 行情获取失败不影响主流程

    # 更新对应数据文件
    if doc_type == "portfolio":
        existing = _load_json(DATA_DIR / "portfolio.json", {})
        old_holdings = {h["code"]: h for h in existing.get("holdings", []) if h.get("code")}
        new_holdings = []
        for s in stocks:
            code = s.get("code", "")
            old = old_holdings.get(code, {})
            new_shares = int(s["shares"]) if str(s.get("shares", "")).lstrip('-').isdigit() else old.get("shares", 0)
            old_shares = old.get("shares", 0)
            # 股数突变检测：旧200→新0是合理卖出，但旧0→新200可能是OCR错读
            if old_shares > 0 and new_shares == 0 and old_shares != new_shares:
                print(f"[仓位变动] {code} {s.get('name','')}: {old_shares}→{new_shares} (卖出清仓)")
            elif abs(new_shares - old_shares) > max(old_shares * 0.5, 100) and old_shares > 0:
                print(f"[仓位变动] {code} {s.get('name','')}: {old_shares}→{new_shares} (变动较大)")
            new_holdings.append({
                "code": code,
                "name": s.get("name") or old.get("name", ""),
                "shares": new_shares,
                "price": float(s.get("price", 0)) or old.get("price", 0),
                "cost": float(s.get("cost", 0)) if s.get("cost") else old.get("cost", 0),
                "pnl": s.get("pnl") or old.get("pnl", ""),
                "position_pct": float(s.get("position_pct", 0)) if s.get("position_pct") else old.get("position_pct", 0),
                "change_pct": old.get("change_pct", 0),
            })
        existing["holdings"] = new_holdings

        # 使用截图中的汇总数据（优先），没有则用旧数据
        summary = data.get("summary", {})
        if summary.get("stock_value"):
            existing["stock_value"] = float(summary["stock_value"])
        else:
            existing["stock_value"] = round(
                sum(h["shares"] * h["price"] for h in existing["holdings"]), 2
            )
        if summary.get("cash"):
            existing["cash"] = float(summary["cash"])
        if summary.get("total_assets"):
            existing["total_assets"] = float(summary["total_assets"])
        else:
            existing["total_assets"] = existing["stock_value"] + existing.get("cash", 0)
        if summary.get("day_pnl"):
            existing["day_pnl"] = float(summary["day_pnl"])
        existing["updated_at"] = datetime.now().isoformat()
        # 计算仓位%
        if existing["total_assets"] > 0:
            existing["position_pct"] = round(existing["stock_value"] / existing["total_assets"] * 100, 2)
        _save_json(DATA_DIR / "portfolio.json", existing)
        msg = f"更新了 {len(stocks)} 只持仓股"

    elif doc_type == "watchlist":
        existing = _load_json(DATA_DIR / "watchlist.json", {})
        existing["stocks"] = [
            {
                "code": s.get("code", ""),
                "name": s.get("name", ""),
                "price": float(s.get("price", 0)) if s.get("price") else 0,
            }
            for s in stocks
        ]
        existing["updated_at"] = datetime.now().isoformat()
        _save_json(DATA_DIR / "watchlist.json", existing)
        msg = f"更新了 {len(stocks)} 只自选股"

    else:
        return jsonify({"error": f"未知类型: {doc_type}"}), 400

    return jsonify({"status": "ok", "message": msg})


# ── TDX中继实时行情接收API ──
@app.route("/api/update/realtime", methods=["POST"])
def update_realtime():
    """接收小小莫中继的实时行情数据"""
    data = request.get_json(force=True) or {}
    stocks = data.get("stocks", [])
    source = data.get("source", "unknown")

    if not stocks:
        return jsonify({"status": "error", "message": "没有股票数据"}), 400

    # 更新 portfolio.json 中的实时价格（change_pct字段）
    pf = _load_json(DATA_DIR / "portfolio.json", {"holdings": []})
    pf_holdings = {h["code"]: h for h in pf.get("holdings", [])}

    updated = 0
    for s in stocks:
        code = s.get("code", "")
        if code in pf_holdings:
            pf_holdings[code]["price"] = float(s.get("price", pf_holdings[code].get("price", 0)))
            pf_holdings[code]["change_pct"] = float(s.get("change_pct", 0))
            pf_holdings[code]["high"] = float(s.get("high", 0))
            pf_holdings[code]["low"] = float(s.get("low", 0))
            pf_holdings[code]["open"] = float(s.get("open", 0))
            pf_holdings[code]["volume"] = int(s.get("volume", 0))
            pf_holdings[code]["data_source"] = source
            pf_holdings[code]["updated_at"] = datetime.now().isoformat()
            updated += 1

    # 也更新 watchlist.json
    wl = _load_json(DATA_DIR / "watchlist.json", {"stocks": []})
    wl_stocks = {s["code"]: s for s in wl.get("stocks", [])}

    for s in stocks:
        code = s.get("code", "")
        if code in wl_stocks:
            wl_stocks[code]["price"] = float(s.get("price", wl_stocks[code].get("price", 0)))
            wl_stocks[code]["change_pct"] = float(s.get("change_pct", 0))

    pf["updated_at"] = datetime.now().isoformat()
    wl["updated_at"] = datetime.now().isoformat()
    _save_json(DATA_DIR / "portfolio.json", pf)
    _save_json(DATA_DIR / "watchlist.json", wl)

    return jsonify({
        "status": "ok",
        "updated": updated,
        "source": source,
        "timestamp": datetime.now().isoformat(),
    })


# 注册提示词管理路由
register_routes(app)


if __name__ == "__main__":
    port = int(os.environ.get("PORT", 8899))
    print(f"🚀 MoFin Dashboard → http://0.0.0.0:{port}")
    app.run(host="0.0.0.0", port=port, debug=False)