"""
财务报销自动化 — Web 界面

用户上传 PDF 发票和支付截图，配置账号信息，自动完成：
  1. 发票提取  2. OCR 识别  3. 浏览器填报（可选）

启动: python web/app.py
访问: http://localhost:5000
"""

import io
import json
import logging
import sys
import threading
import time
import uuid
from pathlib import Path

from flask import Flask, Response, jsonify, render_template, request, stream_with_context

# 确保项目根目录在 sys.path
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
sys.path.insert(0, str(PROJECT_ROOT))

from app.config import load_config as load_project_config
from app.extractor import extract_invoices, save_csv, save_markdown
from app.ocr import enrich_with_ocr, _save_csv as save_ocr_csv, save_markdown_from_csv, _load_csv as load_ocr_csv

app = Flask(__name__, template_folder="templates")

UPLOAD_BASE = PROJECT_ROOT / "web" / "uploads"
SESSION_LOG_FILE = "session.log"
SESSION_RESULT_FILE = "result.json"


# ================================================================
# 日志收集器 — 捕获管道日志到文件，SSE 端点通过 tail -f 读取
# ================================================================

class _SSELogHandler(logging.Handler):
    """将日志写入指定文件（线程安全）"""

    def __init__(self, log_path: Path):
        super().__init__()
        self._lock = threading.Lock()
        self._file = open(log_path, "w", encoding="utf-8")

    def emit(self, record: logging.LogRecord):
        try:
            msg = self.format(record) + "\n"
            with self._lock:
                self._file.write(msg)
                self._file.flush()
        except Exception:
            pass

    def close_file(self):
        try:
            self._file.close()
        except Exception:
            pass


def _install_log_collector(session_dir: Path) -> _SSELogHandler:
    """安装日志收集器到 app.* 模块"""
    log_path = session_dir / SESSION_LOG_FILE
    fmt = logging.Formatter(
        "%(asctime)s [%(levelname)-5s] %(name)s: %(message)s",
        "%Y-%m-%d %H:%M:%S",
    )
    handler = _SSELogHandler(log_path)
    handler.setFormatter(fmt)
    handler.setLevel(logging.INFO)

    for name in ["extractor", "ocr", "pipeline", "bot"]:
        logger = logging.getLogger(name)
        logger.setLevel(logging.INFO)
        logger.addHandler(handler)

    return handler


def _remove_log_collector(handler: _SSELogHandler):
    for name in ["extractor", "ocr", "pipeline", "bot"]:
        logging.getLogger(name).removeHandler(handler)
    handler.close_file()


# ================================================================
# 管道入口
# ================================================================

# ================================================================
# 管道入口
# ================================================================

def run_pipeline_web(session_dir: Path, config: dict, run_bot: bool = False):
    """在 Web 会话目录中执行管道，结果写入 session 目录下的文件"""
    start = time.time()

    # ---- Step 1: 发票提取 ----
    invoices = extract_invoices(str(session_dir))
    if not invoices:
        return {"ok": False, "error": "未提取到任何发票数据"}

    save_csv(invoices, session_dir / "invoice_summary.csv")
    save_markdown(invoices, session_dir / "invoice_summary.md")

    # ---- Step 2: OCR 识别 ----
    csv_path = session_dir / "invoice_summary.csv"
    rows = load_ocr_csv(csv_path)
    if rows is None:
        return {"ok": False, "error": "CSV 读取失败"}

    rows = enrich_with_ocr(rows, str(session_dir))
    save_ocr_csv(csv_path, rows)
    save_markdown_from_csv(csv_path, rows)

    # ---- Step 3: 浏览器填报（可选）----
    if run_bot:
        from app.bot import load_invoice_data, run_bot_web

        bot_invoices = load_invoice_data(str(csv_path), config)
        run_bot_web(config, bot_invoices, session_dir)

    elapsed = time.time() - start
    return {
        "ok": True,
        "elapsed": f"{elapsed:.1f}s",
        "invoice_count": len(rows),
        "csv_url": f"/api/download/{session_dir.name}/invoice_summary.csv",
        "md_url": f"/api/download/{session_dir.name}/invoice_summary.md",
    }


def run_csv_pipeline_web(session_dir: Path, config: dict, csv_filename: str, run_bot: bool = False):
    """直接使用上传的 CSV 文件进行填报，跳过 PDF 提取和 OCR"""
    start = time.time()

    csv_path = session_dir / csv_filename
    if not csv_path.exists():
        return {"ok": False, "error": "CSV 文件不存在"}

    # 读取 CSV 行数
    rows = load_ocr_csv(csv_path)
    if rows is None:
        return {"ok": False, "error": "CSV 读取失败"}

    # 浏览器填报
    if run_bot:
        from app.bot import load_invoice_data, run_bot_web

        bot_invoices = load_invoice_data(str(csv_path), config)
        run_bot_web(config, bot_invoices, session_dir)

    elapsed = time.time() - start
    return {
        "ok": True,
        "elapsed": f"{elapsed:.1f}s",
        "invoice_count": len(rows),
        "csv_url": f"/api/download/{session_dir.name}/{csv_filename}",
    }


# ================================================================
# Flask 路由
# ================================================================

@app.route("/")
def index():
    return render_template("index.html")


@app.route("/api/session", methods=["POST"])
def create_session():
    """创建上传会话，返回 session_id"""
    sid = uuid.uuid4().hex[:12]
    session_dir = UPLOAD_BASE / sid
    session_dir.mkdir(parents=True, exist_ok=True)
    return jsonify({"session_id": sid})


@app.route("/api/upload/<session_id>", methods=["POST"])
def upload_file(session_id: str):
    """上传 PDF 或图片"""
    session_dir = _validate_session(session_id)
    if isinstance(session_dir, tuple):
        return session_dir

    f = request.files.get("file")
    if not f or not f.filename:
        return jsonify({"error": "未选择文件"}), 400

    safe_name = Path(f.filename).name
    f.save(str(session_dir / safe_name))
    return jsonify({"ok": True, "filename": safe_name})


@app.route("/api/upload-csv/<session_id>", methods=["POST"])
def upload_csv(session_id: str):
    """上传 CSV 发票数据文件（跳过 PDF 提取和 OCR）"""
    session_dir = _validate_session(session_id)
    if isinstance(session_dir, tuple):
        return session_dir

    f = request.files.get("file")
    if not f or not f.filename:
        return jsonify({"error": "未选择文件"}), 400

    safe_name = Path(f.filename).name
    f.save(str(session_dir / safe_name))
    return jsonify({"ok": True, "filename": safe_name})


@app.route("/api/files/<session_id>", methods=["GET"])
def list_files(session_id: str):
    """列出会话目录中的文件"""
    session_dir = _validate_session(session_id)
    if isinstance(session_dir, tuple):
        return session_dir

    pdfs = sorted(f.name for f in session_dir.glob("*.pdf"))
    imgs = sorted(
        f.name for ext in {".png", ".jpg", ".jpeg", ".bmp", ".webp"}
        for f in session_dir.glob(f"*{ext}")
    )
    return jsonify({"pdfs": pdfs, "images": imgs})


@app.route("/api/process/<session_id>", methods=["POST"])
def start_process(session_id: str):
    """启动管道处理"""
    session_dir = _validate_session(session_id)
    if isinstance(session_dir, tuple):
        return session_dir

    body = request.get_json(silent=True) or {}
    run_bot_flag = body.get("submit", False)
    mode = body.get("mode", "auto")  # "pdf", "csv", or "auto"

    # 读取配置
    config = _build_web_config(body)

    # 写入配置到会话目录
    with open(session_dir / "config.json", "w", encoding="utf-8") as f:
        json.dump(config, f, ensure_ascii=False, indent=2, default=str)

    # 在后台线程执行
    handler = _install_log_collector(session_dir)

    def _run():
        result = {"ok": False, "error": "未知错误"}
        try:
            if mode == "csv":
                # CSV 模式：直接使用上传的 CSV，跳过 PDF 提取和 OCR
                csv_files = list(session_dir.glob("*.csv"))
                if not csv_files:
                    result = {"ok": False, "error": "未找到 CSV 文件"}
                else:
                    result = run_csv_pipeline_web(session_dir, config, csv_files[0].name, run_bot_flag)
            else:
                # 自动检测：如果有 CSV 则走 csv 管道，否则走 pdf 管道
                csv_files = list(session_dir.glob("*.csv"))
                pdf_files = list(session_dir.glob("*.pdf"))
                if csv_files and not pdf_files:
                    result = run_csv_pipeline_web(session_dir, config, csv_files[0].name, run_bot_flag)
                else:
                    result = run_pipeline_web(session_dir, config, run_bot_flag)
        except BaseException as e:
            result = {"ok": False, "error": str(e)}
            if isinstance(e, (KeyboardInterrupt, SystemExit)):
                raise
        finally:
            try:
                # 原子写入：先写临时文件，再重命名，避免 SSE 读到截断的空文件
                tmp_path = session_dir / (SESSION_RESULT_FILE + ".tmp")
                with open(tmp_path, "w", encoding="utf-8") as f:
                    json.dump(result, f, ensure_ascii=False)
                tmp_path.replace(session_dir / SESSION_RESULT_FILE)
            except Exception:
                pass
            _remove_log_collector(handler)

    threading.Thread(target=_run, daemon=True).start()

    return jsonify({"status": "started"})


@app.route("/api/logs/<session_id>")
def stream_logs(session_id: str):
    """SSE 日志流"""
    session_dir = _validate_session(session_id)
    if isinstance(session_dir, tuple):
        return session_dir

    def generate():
        # 先发送已有日志
        log_file = session_dir / SESSION_LOG_FILE
        last_size = 0
        start_time = time.time()
        timeout = 600  # 10 分钟超时

        while time.time() - start_time < timeout:
            if log_file.exists():
                current_size = log_file.stat().st_size
                if current_size > last_size:
                    with open(log_file, encoding="utf-8", errors="replace") as f:
                        f.seek(last_size)
                        chunk = f.read()
                        if chunk:
                            yield f"data: {_escape_sse(chunk)}\n\n"
                    last_size = current_size

            # 也通过队列发送实时日志
            # 检查是否完成
            result_file = session_dir / SESSION_RESULT_FILE
            if result_file.exists():
                with open(result_file, encoding="utf-8") as f:
                    result = json.load(f)
                yield f"data: {_escape_sse(json.dumps({'type': 'done', 'result': result}, ensure_ascii=False))}\n\n"
                break

            time.sleep(0.5)

    return Response(
        stream_with_context(generate()),
        mimetype="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
    )


@app.route("/api/download/<session_id>/<filename>")
def download_file(session_id: str, filename: str):
    """下载生成的文件"""
    session_dir = _validate_session(session_id)
    if isinstance(session_dir, tuple):
        return session_dir

    filepath = session_dir / filename
    if not filepath.exists():
        return jsonify({"error": "文件不存在"}), 404

    return Response(filepath.read_bytes(), mimetype="application/octet-stream")


# ================================================================
# 辅助函数
# ================================================================

def _validate_session(session_id: str):
    session_dir = UPLOAD_BASE / session_id
    if not session_dir.exists():
        return jsonify({"error": "会话不存在"}), 404
    return session_dir


def _build_web_config(body: dict) -> dict:
    """从请求体构建配置"""
    config = load_project_config()
    for key in ("username", "password", "default_name", "default_card_no", "default_person_id"):
        if body.get(key):
            config[key] = body[key]
    return config


def _escape_sse(text: str) -> str:
    """SSE 数据转义，同时处理 Windows 行尾 \\r\\n"""
    return text.replace("\r\n", "\n").replace("\r", "\n").replace("\n", "\ndata: ")


@app.route("/mobile/<session_id>")
def mobile_upload(session_id: str):
    """移动端上传页面"""
    session_dir = UPLOAD_BASE / session_id
    if not session_dir.exists():
        return render_template("mobile_upload.html", error="会话不存在"), 404
    return render_template("mobile_upload.html", session_id=session_id)


@app.route("/api/mobile-upload/<session_id>", methods=["POST"])
def mobile_upload_file(session_id: str):
    """移动端上传图片（复用 PC 上传逻辑）"""
    return upload_file(session_id)


if __name__ == "__main__":
    UPLOAD_BASE.mkdir(parents=True, exist_ok=True)
    print(f"启动 Web 服务: http://localhost:5000")
    app.run(host="0.0.0.0", port=5000, debug=True, threaded=True, use_reloader=False)