"""
标伙伴 - AI 标书助手（单机版）
启动命令：python app.py
访问地址：http://localhost:5000
"""
import os
import sys


def _bootstrap_env_file():
    """在 import config 之前加载项目根目录 .env，便于注入 API Key；不覆盖已存在的环境变量。"""
    if getattr(sys, 'frozen', False):
        base = os.path.dirname(sys.executable)
    else:
        base = os.path.dirname(os.path.abspath(__file__))
    path = os.path.join(base, '.env')
    if not os.path.isfile(path):
        return
    try:
        with open(path, encoding='utf-8') as f:
            for raw in f:
                line = raw.strip()
                if not line or line.startswith('#') or '=' not in line:
                    continue
                key, _, val = line.partition('=')
                key, val = key.strip(), val.strip().strip('"').strip("'")
                if key and key not in os.environ:
                    os.environ[key] = val
    except OSError:
        pass


_bootstrap_env_file()
import json
import sqlite3
import threading
import logging
from datetime import datetime
from flask import Flask, request, jsonify, render_template, send_from_directory, abort

import config
from utils import settings as _settings

# ── 日志配置 ────────────────────────────────────────────────────────────────
_log_handlers = [logging.StreamHandler()]
if getattr(sys, 'frozen', False):
    _log_file = os.path.join(os.path.dirname(sys.executable), 'bid_partner.log')
    try:
        _log_handlers.append(logging.FileHandler(_log_file, encoding='utf-8'))
    except Exception:
        pass

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
    datefmt='%H:%M:%S',
    handlers=_log_handlers,
)
logger = logging.getLogger(__name__)


def _safe_json_load(raw):
    if not raw or not isinstance(raw, str):
        return None
    try:
        return json.loads(raw)
    except Exception:
        return None


# ── Flask 应用 ───────────────────────────────────────────────────────────────
_bundle = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
app = Flask(__name__,
            template_folder=os.path.join(_bundle, 'templates'),
            static_folder=os.path.join(_bundle, 'static'))
app.secret_key = config.SECRET_KEY
app.config['MAX_CONTENT_LENGTH'] = config.MAX_FILE_SIZE_MB * 1024 * 1024


# ═══════════════════════════════════════════════════════════════════════════
#  数据库初始化
# ═══════════════════════════════════════════════════════════════════════════

def init_db():
    """创建所有必要的目录和数据库表"""
    for d in [config.DATA_DIR, config.UPLOAD_DIR, config.EXPORT_DIR,
              config.KNOWLEDGE_DIR, config.CHROMA_DIR]:
        os.makedirs(d, exist_ok=True)

    # 初始化持久化配置，启动时恢复上次保存的 API Key 等设置
    settings_path = os.path.join(config.DATA_DIR, 'settings.json')
    _settings.init(settings_path)
    _settings.load(config)
    logger.info(f'当前模型: {config.MODEL_PROVIDER}')

    conn = sqlite3.connect(config.DB_PATH)
    cur = conn.cursor()
    # WAL 模式：允许多个读写线程并发操作，不互相阻塞
    cur.execute('PRAGMA journal_mode=WAL')
    cur.execute('PRAGMA synchronous=NORMAL')  # WAL 下可适当降低同步级别以提速

    cur.executescript('''
        CREATE TABLE IF NOT EXISTS projects (
            id              INTEGER PRIMARY KEY AUTOINCREMENT,
            name            TEXT    NOT NULL,
            outline_status  TEXT    DEFAULT 'none',
            outline_error   TEXT    DEFAULT '',
            anon_requirements TEXT  DEFAULT '',
            enable_figure   INTEGER DEFAULT 0,
            enable_table    INTEGER DEFAULT 0,
            created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );

        CREATE TABLE IF NOT EXISTS tender_data (
            id                  INTEGER PRIMARY KEY AUTOINCREMENT,
            project_id          INTEGER NOT NULL UNIQUE,
            file_name           TEXT,
            raw_text            TEXT,
            summary             TEXT,
            rating_requirements TEXT,
            rating_json         TEXT,
            outline             TEXT,
            boq_file_name       TEXT    DEFAULT '',
            boq_text            TEXT    DEFAULT '',
            boq_summary         TEXT    DEFAULT '',
            boq_analysis_json  TEXT    DEFAULT '',
            boq_status          TEXT    DEFAULT 'none',
            boq_error           TEXT    DEFAULT '',
            tender_kind         TEXT    DEFAULT 'engineering',
            status              TEXT DEFAULT 'pending',
            error_message       TEXT DEFAULT '',
            created_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
        );

        CREATE TABLE IF NOT EXISTS bid_sections (
            id              INTEGER PRIMARY KEY AUTOINCREMENT,
            project_id      INTEGER NOT NULL,
            section_number  TEXT,
            section_title   TEXT NOT NULL,
            level           INTEGER DEFAULT 1,
            is_leaf         INTEGER DEFAULT 1,
            content         TEXT    DEFAULT '',
            intro_content   TEXT    DEFAULT '',
            order_index     INTEGER DEFAULT 0,
            status          TEXT    DEFAULT 'pending',
            error_message   TEXT    DEFAULT '',
            created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
        );

        CREATE TABLE IF NOT EXISTS knowledge_files (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            file_name   TEXT NOT NULL UNIQUE,
            file_path   TEXT,
            chunk_count INTEGER DEFAULT 0,
            added_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );
    ''')
    conn.commit()
    # 兼容旧数据库：追加新列（已存在时忽略错误）
    migrations = [
        ("ALTER TABLE projects ADD COLUMN anon_requirements TEXT DEFAULT ''",
         'projects.anon_requirements'),
        ("ALTER TABLE projects ADD COLUMN enable_figure INTEGER DEFAULT 0",
         'projects.enable_figure'),
        ("ALTER TABLE projects ADD COLUMN enable_table INTEGER DEFAULT 0",
         'projects.enable_table'),
        ("ALTER TABLE tender_data ADD COLUMN boq_file_name TEXT DEFAULT ''",
         'tender_data.boq_file_name'),
        ("ALTER TABLE tender_data ADD COLUMN boq_text TEXT DEFAULT ''",
         'tender_data.boq_text'),
        ("ALTER TABLE tender_data ADD COLUMN boq_summary TEXT DEFAULT ''",
         'tender_data.boq_summary'),
        ("ALTER TABLE tender_data ADD COLUMN boq_status TEXT DEFAULT 'none'",
         'tender_data.boq_status'),
        ("ALTER TABLE tender_data ADD COLUMN boq_error TEXT DEFAULT ''",
         'tender_data.boq_error'),
        ("ALTER TABLE tender_data ADD COLUMN boq_analysis_json TEXT DEFAULT ''",
         'tender_data.boq_analysis_json'),
        ("ALTER TABLE tender_data ADD COLUMN tender_kind TEXT DEFAULT 'engineering'",
         'tender_data.tender_kind'),
        ("ALTER TABLE tender_data ADD COLUMN target_pages INTEGER DEFAULT 0",
         'tender_data.target_pages'),
    ]
    for sql, col in migrations:
        try:
            conn.execute(sql)
            conn.commit()
            logger.info(f'数据库迁移：新增 {col} 列')
        except Exception:
            pass  # 列已存在
    conn.close()
    logger.info('数据库初始化完成')


def get_db():
    return sqlite3.connect(config.DB_PATH)


# ═══════════════════════════════════════════════════════════════════════════
#  页面路由
# ═══════════════════════════════════════════════════════════════════════════

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


@app.route('/project/<int:project_id>')
def project_page(project_id):
    conn = get_db()
    cur = conn.cursor()
    cur.execute("SELECT id, name, created_at FROM projects WHERE id=?", (project_id,))
    row = cur.fetchone()
    conn.close()
    if not row:
        abort(404)
    return render_template('project.html', project={'id': row[0], 'name': row[1], 'created_at': row[2]})


# ═══════════════════════════════════════════════════════════════════════════
#  API：项目管理
# ═══════════════════════════════════════════════════════════════════════════

@app.route('/api/projects', methods=['GET'])
def api_list_projects():
    conn = get_db()
    cur = conn.cursor()
    cur.execute('''
        SELECT p.id, p.name, p.created_at, p.outline_status,
               td.status as parse_status, td.file_name,
               (SELECT COUNT(*) FROM bid_sections WHERE project_id=p.id) as section_count,
               (SELECT COUNT(*) FROM bid_sections WHERE project_id=p.id AND status='done') as done_count
        FROM projects p
        LEFT JOIN tender_data td ON td.project_id = p.id
        ORDER BY p.created_at DESC
    ''')
    rows = cur.fetchall()
    conn.close()
    projects = []
    for r in rows:
        projects.append({
            'id': r[0], 'name': r[1], 'created_at': r[2],
            'outline_status': r[3], 'parse_status': r[4] or 'none',
            'file_name': r[5], 'section_count': r[6], 'done_count': r[7],
        })
    return jsonify({'projects': projects})


@app.route('/api/projects', methods=['POST'])
def api_create_project():
    data = request.get_json()
    name = (data or {}).get('name', '').strip()
    if not name:
        return jsonify({'error': '项目名称不能为空'}), 400
    conn = get_db()
    cur = conn.cursor()
    cur.execute("INSERT INTO projects (name) VALUES (?)", (name,))
    project_id = cur.lastrowid
    conn.commit()
    conn.close()
    return jsonify({'id': project_id, 'name': name}), 201


@app.route('/api/projects/<int:project_id>', methods=['DELETE'])
def api_delete_project(project_id):
    conn = get_db()
    cur = conn.cursor()
    cur.execute("DELETE FROM projects WHERE id=?", (project_id,))
    conn.commit()
    conn.close()
    return jsonify({'success': True})


@app.route('/api/projects/<int:project_id>', methods=['GET'])
def api_get_project(project_id):
    conn = get_db()
    cur = conn.cursor()
    cur.execute('''
        SELECT p.id, p.name, p.created_at, p.outline_status, p.outline_error,
               td.file_name, td.status as parse_status, td.error_message,
               td.summary, td.rating_requirements, td.rating_json, td.outline,
               p.anon_requirements, p.enable_figure, p.enable_table,
               td.boq_file_name, td.boq_summary, td.boq_status, td.boq_error,
               td.boq_analysis_json, td.tender_kind, COALESCE(td.target_pages, 0)
        FROM projects p
        LEFT JOIN tender_data td ON td.project_id = p.id
        WHERE p.id=?
    ''', (project_id,))
    row = cur.fetchone()
    conn.close()
    if not row:
        return jsonify({'error': '项目不存在'}), 404

    return jsonify({
        'id': row[0], 'name': row[1], 'created_at': row[2],
        'outline_status': row[3], 'outline_error': row[4],
        'file_name': row[5], 'parse_status': row[6] or 'none',
        'parse_error': row[7], 'summary': row[8],
        'rating_requirements': row[9], 'rating_json': row[10],
        'outline': row[11], 'anon_requirements': row[12] or '',
        'enable_figure': bool(row[13]), 'enable_table': bool(row[14]),
        'boq_file_name': row[15] or '', 'boq_summary': row[16] or '',
        'boq_status': row[17] or 'none', 'boq_error': row[18] or '',
        'boq_analysis': _safe_json_load(row[19]),
        'tender_kind': row[20] or 'engineering',
        'target_pages': int(row[21] or 0),
    })


# ═══════════════════════════════════════════════════════════════════════════
#  API：文件上传与解析
# ═══════════════════════════════════════════════════════════════════════════

@app.route('/api/projects/<int:project_id>/upload', methods=['POST'])
def api_upload(project_id):
    from utils.file_utils import allowed_file, safe_filename

    if 'file' not in request.files:
        return jsonify({'error': '未选择文件'}), 400
    f = request.files['file']
    if not f.filename:
        return jsonify({'error': '文件名为空'}), 400
    if not allowed_file(f.filename):
        return jsonify({'error': '仅支持 PDF / DOC / DOCX 格式'}), 400

    filename = safe_filename(f.filename)
    save_path = os.path.join(config.UPLOAD_DIR, f'{project_id}_{filename}')
    f.save(save_path)

    # 初始化 tender_data 记录
    conn = get_db()
    cur = conn.cursor()
    cur.execute('''
        INSERT INTO tender_data (project_id, file_name, status)
        VALUES (?, ?, 'uploaded')
        ON CONFLICT(project_id) DO UPDATE SET file_name=?, status='uploaded', error_message='', updated_at=?
    ''', (project_id, filename, filename, datetime.now()))
    conn.commit()
    conn.close()

    return jsonify({'success': True, 'file_name': filename, 'path': save_path})


@app.route('/api/projects/<int:project_id>/tender-data', methods=['PUT'])
def api_update_tender_data(project_id):
    """允许用户手动修改并保存解析结果（摘要、技术评分要求、标书类型）"""
    data = request.get_json() or {}
    fields = {}
    if 'summary' in data:
        fields['summary'] = data['summary']
    if 'rating_requirements' in data:
        fields['rating_requirements'] = data['rating_requirements']
    if 'tender_kind' in data:
        tk = (data.get('tender_kind') or 'engineering').strip().lower()
        if tk not in ('engineering', 'service', 'goods'):
            return jsonify({'error': 'tender_kind 须为 engineering / service / goods'}), 400
        fields['tender_kind'] = tk
    if 'target_pages' in data:
        try:
            fields['target_pages'] = max(0, int(data['target_pages']))
        except (TypeError, ValueError):
            return jsonify({'error': 'target_pages 须为非负整数'}), 400
    if not fields:
        return jsonify({'error': '无可更新字段'}), 400

    conn = get_db()
    cur = conn.cursor()
    set_clause = ', '.join(f'{k}=?' for k in fields)
    values = list(fields.values()) + [datetime.now(), project_id]
    cur.execute(
        f'UPDATE tender_data SET {set_clause}, updated_at=? WHERE project_id=?',
        values
    )
    conn.commit()
    conn.close()
    return jsonify({'success': True})


@app.route('/api/projects/<int:project_id>/upload-boq', methods=['POST'])
def api_upload_boq(project_id):
    """上传工程量清单文件（独立于招标文件）"""
    from utils.file_utils import safe_filename

    if 'file' not in request.files:
        return jsonify({'error': '未选择文件'}), 400
    f = request.files['file']
    if not f.filename:
        return jsonify({'error': '文件名为空'}), 400

    ext = os.path.splitext(f.filename)[1].lower()
    allowed_exts = {'.xlsx', '.xls', '.csv', '.pdf', '.docx', '.doc'}
    if ext not in allowed_exts:
        return jsonify({'error': f'不支持的格式 {ext}，请使用 xlsx/xls/csv/pdf/docx/doc'}), 400

    filename = safe_filename(f.filename)
    save_path = os.path.join(config.UPLOAD_DIR, f'{project_id}_boq_{filename}')
    f.save(save_path)

    # 确保 tender_data 记录存在
    conn = get_db()
    cur = conn.cursor()
    cur.execute('''
        INSERT INTO tender_data (project_id, boq_file_name, boq_status)
        VALUES (?, ?, 'uploaded')
        ON CONFLICT(project_id) DO UPDATE
        SET boq_file_name=?, boq_status='uploaded', boq_error='', updated_at=?
    ''', (project_id, filename, filename, datetime.now()))
    conn.commit()
    conn.close()

    return jsonify({'success': True, 'file_name': filename, 'path': save_path})


@app.route('/api/projects/<int:project_id>/parse-boq', methods=['POST'])
def api_parse_boq(project_id):
    """后台解析工程量清单 → AI 摘要"""
    from modules.parser import parse_boq_file

    conn = get_db()
    cur = conn.cursor()
    cur.execute("SELECT boq_file_name FROM tender_data WHERE project_id=?", (project_id,))
    row = cur.fetchone()
    conn.close()

    if not row or not row[0]:
        return jsonify({'error': '请先上传工程量清单文件'}), 400

    file_name = row[0]
    # 同时尝试带/不带 boq_ 前缀的路径
    path1 = os.path.join(config.UPLOAD_DIR, f'{project_id}_boq_{file_name}')
    path2 = os.path.join(config.UPLOAD_DIR, f'{project_id}_{file_name}')
    file_path = path1 if os.path.exists(path1) else path2
    if not os.path.exists(file_path):
        return jsonify({'error': '清单文件不存在，请重新上传'}), 404

    t = threading.Thread(
        target=parse_boq_file,
        args=(config.DB_PATH, project_id, file_path, file_name),
        daemon=True,
    )
    t.start()
    return jsonify({'success': True})


@app.route('/api/projects/<int:project_id>/boq', methods=['PUT'])
def api_update_boq(project_id):
    """手动保存用户编辑后的工程量清单摘要"""
    data = request.get_json() or {}
    boq_summary = data.get('boq_summary', '')
    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        "UPDATE tender_data SET boq_summary=?, updated_at=? WHERE project_id=?",
        (boq_summary, datetime.now(), project_id)
    )
    conn.commit()
    conn.close()
    return jsonify({'success': True})


@app.route('/api/projects/<int:project_id>/parse', methods=['POST'])
def api_parse(project_id):
    from modules.parser import parse_tender_file

    conn = get_db()
    cur = conn.cursor()
    cur.execute("SELECT file_name FROM tender_data WHERE project_id=?", (project_id,))
    row = cur.fetchone()
    conn.close()

    if not row or not row[0]:
        return jsonify({'error': '请先上传招标文件'}), 400

    file_name = row[0]
    file_path = os.path.join(config.UPLOAD_DIR, f'{project_id}_{file_name}')
    if not os.path.exists(file_path):
        return jsonify({'error': f'文件不存在: {file_name}'}), 404

    t = threading.Thread(
        target=parse_tender_file,
        args=(config.DB_PATH, project_id, file_path, file_name),
        daemon=True,
    )
    t.start()
    return jsonify({'success': True, 'message': '解析任务已启动'})


@app.route('/api/projects/<int:project_id>/parse-status', methods=['GET'])
def api_parse_status(project_id):
    conn = get_db()
    cur = conn.cursor()
    # Extended for deep integration: include settings from projects for parse tab UI
    cur.execute('''
        SELECT td.status, td.error_message, td.summary, td.rating_requirements, td.rating_json, td.tender_kind,
               p.anon_requirements, p.enable_figure, p.enable_table
        FROM tender_data td
        LEFT JOIN projects p ON p.id = td.project_id
        WHERE td.project_id=?
    ''', (project_id,))
    row = cur.fetchone()
    conn.close()
    if not row:
        return jsonify({'status': 'none'})
    return jsonify({
        'status': row[0] or 'none',
        'message': row[1] or '',
        'has_summary': bool(row[2]),
        'has_rating': bool(row[3]),
        'has_rating_json': bool(row[4]),
        'tender_kind': row[5] or 'engineering',
        'anon_requirements': row[6] or '',
        'enable_figure': bool(row[7]),
        'enable_table': bool(row[8]),
    })


# ═══════════════════════════════════════════════════════════════════════════
#  API：大纲生成
# ═══════════════════════════════════════════════════════════════════════════

@app.route('/api/projects/<int:project_id>/outline', methods=['PUT'])
def api_update_outline(project_id):
    """
    用户手动修改大纲后保存：重新解析大纲文本，更新 bid_sections。
    注意：已生成的章节内容将被清除，需重新生成。
    """
    from modules.generator import _parse_outline, _save_sections, _save_outline_text

    data = request.get_json() or {}
    outline_text = (data.get('outline') or '').strip()
    if not outline_text:
        return jsonify({'error': '大纲内容不能为空'}), 400

    try:
        # 解析并自动重排序号，返回规范化文本
        _, sections, normalized_text = _parse_outline(outline_text)
        if not sections:
            return jsonify({'error': '大纲解析失败，未识别到任何章节，请检查格式'}), 400

        conn = get_db()
        cur = conn.cursor()
        # 存储重排序号后的规范文本
        _save_outline_text(conn, project_id, normalized_text)
        _save_sections(conn, project_id, sections)
        cur.execute(
            "UPDATE projects SET outline_status='outline_done', outline_error='', updated_at=? WHERE id=?",
            (datetime.now(), project_id)
        )
        cur.execute("SELECT length(outline) FROM tender_data WHERE project_id=?", (project_id,))
        persisted_len = (cur.fetchone() or [0])[0] or 0
        conn.commit()
        conn.close()
        # 把规范化文本返回给前端，前端据此更新编辑器内容
        return jsonify({
            'success': True,
            'section_count': len(sections),
            'normalized_outline': normalized_text,
            'persisted_outline_len': persisted_len,
        })
    except Exception as e:
        logger.exception('手动保存大纲失败')
        return jsonify({'error': str(e)}), 500


@app.route('/api/projects/<int:project_id>/generate-outline', methods=['POST'])
def api_generate_outline(project_id):
    from modules.generator import generate_outline
    data = request.get_json(silent=True) or {}
    force = bool(data.get('force', False))

    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        '''
        SELECT p.outline_status, COALESCE(td.outline, '')
        FROM projects p
        LEFT JOIN tender_data td ON td.project_id = p.id
        WHERE p.id = ?
        ''',
        (project_id,),
    )
    row = cur.fetchone()
    if not row:
        conn.close()
        return jsonify({'success': False, 'error': '项目不存在'}), 404

    ostatus, outline_text = (row[0] or 'none', row[1] or '')
    if ostatus == 'outline_generating':
        conn.close()
        return jsonify({'success': True, 'message': '大纲正在生成中，请稍候'}), 200

    if not force and outline_text.strip():
        conn.close()
        return jsonify({
            'success': False,
            'error': '当前项目已有大纲，重新生成会覆盖现有大纲。请确认后以 force=true 再次请求。'
        }), 409

    # 立即落库，避免仅依赖后台线程的首条 SQL 时产生竞态（重复点击、轮询、刷新时状态不一致）
    cur.execute(
        "UPDATE projects SET outline_status=?, outline_error=?, updated_at=? WHERE id=?",
        ('outline_generating', '', datetime.now(), project_id),
    )
    conn.commit()
    conn.close()

    t = threading.Thread(
        target=generate_outline,
        args=(config.DB_PATH, project_id),
        daemon=True,
    )
    t.start()
    return jsonify({'success': True, 'message': '大纲生成任务已启动'})


@app.route('/api/projects/<int:project_id>/expand-outline', methods=['POST'])
def api_expand_outline(project_id):
    """根据当前编辑大纲自动补全小章节，并直接落库重建章节树。"""
    from modules.generator import (
        expand_outline,
        _parse_outline,
        _save_outline_text,
        _save_sections,
    )

    from utils import volume_chapters as vol_ch

    data = request.get_json() or {}
    outline = data.get('outline', '')
    if not outline.strip():
        return jsonify({'success': False, 'error': '大纲内容不能为空'}), 400

    no_subchapter_limit = data.get('no_subchapter_limit') in (True, 1, '1', 'true', 'True')
    if 'target_pages' in data:
        try:
            request_pages = max(0, int(data['target_pages']))
        except (TypeError, ValueError):
            request_pages = None
    else:
        request_pages = None

    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        "SELECT summary, rating_requirements, COALESCE(target_pages, 0) FROM tender_data "
        "WHERE project_id=?",
        (project_id,),
    )
    row = cur.fetchone()
    conn.close()

    summary = row[0] if row else ''
    rating_requirements = row[1] if row else ''
    db_tp = int(row[2] or 0) if row else 0
    cfg_tp = int(getattr(config, 'TARGET_PAGES', 0) or 0)
    effective_target_pages = vol_ch.resolve_expand_target_pages(
        request_pages, no_subchapter_limit, db_tp, cfg_tp
    )

    try:
        expanded_outline = expand_outline(
            outline, summary, rating_requirements, project_id, effective_target_pages
        )
        _, sections, normalized_text = _parse_outline(expanded_outline)
        if not sections:
            return jsonify({'success': False, 'error': '扩充后大纲解析失败，请检查章节格式'}), 400

        conn = get_db()
        cur = conn.cursor()
        _save_outline_text(conn, project_id, normalized_text)
        _save_sections(conn, project_id, sections)
        cur.execute(
            "UPDATE projects SET outline_status='outline_done', outline_error='', updated_at=? WHERE id=?",
            (datetime.now(), project_id),
        )
        conn.commit()
        cur.execute("SELECT length(outline) FROM tender_data WHERE project_id=?", (project_id,))
        persisted_len = (cur.fetchone() or [0])[0] or 0
        conn.close()

        return jsonify({
            'success': True,
            'expanded_outline': expanded_outline,
            'normalized_outline': normalized_text,
            'section_count': len(sections),
            'persisted_outline_len': persisted_len,
            'used_target_pages': effective_target_pages,
            'no_subchapter_limit': no_subchapter_limit,
        })
    except Exception as e:
        logger.exception(f'expand_outline failed for project {project_id}')
        return jsonify({'success': False, 'error': str(e)}), 500


@app.route('/api/projects/<int:project_id>/outline-status', methods=['GET'])
def api_outline_status(project_id):
    conn = get_db()
    cur = conn.cursor()
    cur.execute("SELECT outline_status, outline_error FROM projects WHERE id=?", (project_id,))
    row = cur.fetchone()
    conn.close()
    if not row:
        return jsonify({'status': 'none'})
    return jsonify({'status': row[0], 'error': row[1]})


# ═══════════════════════════════════════════════════════════════════════════
#  API：章节管理与内容生成
# ═══════════════════════════════════════════════════════════════════════════

@app.route('/api/projects/<int:project_id>/sections', methods=['GET'])
def api_list_sections(project_id):
    conn = get_db()
    cur = conn.cursor()
    cur.execute('''
        SELECT id, section_number, section_title, level, is_leaf,
               status, error_message, length(content) as content_len
        FROM bid_sections
        WHERE project_id=?
        ORDER BY order_index
    ''', (project_id,))
    rows = cur.fetchall()
    conn.close()
    sections = []
    for r in rows:
        sections.append({
            'id': r[0], 'number': r[1], 'title': r[2], 'level': r[3],
            'is_leaf': bool(r[4]), 'status': r[5], 'error': r[6],
            'has_content': (r[7] or 0) > 0,
        })
    return jsonify({'sections': sections})


@app.route('/api/projects/<int:project_id>/sections/<int:section_id>', methods=['GET'])
def api_get_section(project_id, section_id):
    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        "SELECT id, section_number, section_title, level, is_leaf, content, intro_content, status FROM bid_sections WHERE id=? AND project_id=?",
        (section_id, project_id)
    )
    row = cur.fetchone()
    conn.close()
    if not row:
        return jsonify({'error': '章节不存在'}), 404
    return jsonify({
        'id': row[0], 'number': row[1], 'title': row[2], 'level': row[3],
        'is_leaf': bool(row[4]), 'content': row[5], 'intro_content': row[6], 'status': row[7],
    })


@app.route('/api/projects/<int:project_id>/sections/<int:section_id>', methods=['PUT'])
def api_update_section(project_id, section_id):
    data = request.get_json() or {}
    content = data.get('content', '')
    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        "UPDATE bid_sections SET content=?, status='done', updated_at=? WHERE id=? AND project_id=?",
        (content, datetime.now(), section_id, project_id)
    )
    conn.commit()
    conn.close()
    return jsonify({'success': True})


@app.route('/api/projects/<int:project_id>/sections/<int:section_id>/chat', methods=['POST'])
def api_section_chat(project_id, section_id):
    """
    对话式章节生成：接受多轮对话历史，结合章节上下文调用 AI，返回新一轮回复。
    请求体：{ "messages": [{"role": "user"|"assistant", "content": "..."}] }
    """
    from utils import ai_client

    data = request.get_json() or {}
    messages = data.get('messages', [])
    if not messages:
        return jsonify({'error': '消息列表不能为空'}), 400

    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        "SELECT section_title FROM bid_sections WHERE id=? AND project_id=?",
        (section_id, project_id)
    )
    row = cur.fetchone()
    if not row:
        conn.close()
        return jsonify({'error': '章节不存在'}), 404
    section_title = row[0]

    cur.execute(
        "SELECT summary, outline, tender_kind FROM tender_data WHERE project_id=?",
        (project_id,),
    )
    td = cur.fetchone()
    conn.close()

    summary = ((td[0] or '')[:3000]) if td else ''
    outline = ((td[1] or '')[:2000]) if td else ''
    tk = (td[2] or 'engineering').strip().lower() if td else 'engineering'
    if tk not in ('engineering', 'service', 'goods'):
        tk = 'engineering'

    from utils.tender_kind_sections import CHAT_KIND_INSTRUCTION

    kind_hint = CHAT_KIND_INSTRUCTION.get(tk, CHAT_KIND_INSTRUCTION['engineering'])

    system = f"""你是一位资深的投标文件撰写专家，正在协助用户以对话方式撰写技术标书中「{section_title}」章节的正文内容。

【项目背景摘要】
{summary or '（未提供）'}

【标书目录结构】
{outline or '（未提供）'}
{kind_hint}

【撰写规范（必须遵守）】
- 投标方自称统一用"我方"，禁用"我们""我公司"
- 禁止套话：综上所述、高度重视、全力以赴、不断优化、稳步推进等
- 每项措施须有可检验的实质内容（做法、节点、标准编号，或招标文件/清单已给出的量化依据）；
  未载明的型号、数量、吨位、时限等不得编造，用概括性定性表述写清含义；禁止使用方括号待填项（如[型号][数量]）
- 列举用(1)(2)(3)编号，禁止"首先其次最后"连接
- 纯文本输出，段落间用空行分隔，不使用 Markdown 符号
- 直接输出正文，不含章节标题、解释说明或"以下是..."引导语"""

    valid_messages = [m for m in messages if m.get('role') in ('user', 'assistant')]

    try:
        content = ai_client.chat_with_history(system, valid_messages,
                                              temperature=0.7, max_tokens=4096)
        return jsonify({'success': True, 'content': content})
    except Exception as e:
        logger.exception(f'对话式章节生成失败 section_id={section_id}')
        return jsonify({'error': str(e)}), 500


@app.route('/api/projects/<int:project_id>/generate-section', methods=['POST'])
def api_generate_section(project_id):
    from modules.generator import generate_section

    data = request.get_json() or {}
    section_id = data.get('section_id')
    if not section_id:
        return jsonify({'error': '缺少 section_id'}), 400

    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        "SELECT anon_requirements, enable_figure, enable_table FROM projects WHERE id=?",
        (project_id,)
    )
    row = cur.fetchone()
    conn.close()
    anon_req = (row[0] or '') if row else ''
    enable_fig = bool(row[1]) if row else False
    enable_tbl = bool(row[2]) if row else False

    t = threading.Thread(
        target=generate_section,
        args=(config.DB_PATH, project_id, section_id, anon_req, enable_fig, enable_tbl),
        daemon=True,
    )
    t.start()
    return jsonify({'success': True})


@app.route('/api/projects/<int:project_id>/diagram', methods=['PUT'])
def api_update_diagram(project_id):
    """保存图表模式开关"""
    data = request.get_json() or {}
    enable_figure = 1 if data.get('enable_figure') else 0
    enable_table = 1 if data.get('enable_table') else 0
    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        "UPDATE projects SET enable_figure=?, enable_table=?, updated_at=? WHERE id=?",
        (enable_figure, enable_table, datetime.now(), project_id)
    )
    conn.commit()
    conn.close()
    return jsonify({'success': True})


@app.route('/api/projects/<int:project_id>/anon', methods=['PUT'])
def api_update_anon(project_id):
    """保存暗标要求"""
    data = request.get_json() or {}
    anon_requirements = data.get('anon_requirements', '')
    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        "UPDATE projects SET anon_requirements=?, updated_at=? WHERE id=?",
        (anon_requirements, datetime.now(), project_id)
    )
    conn.commit()
    conn.close()
    return jsonify({'success': True})


@app.route('/api/projects/<int:project_id>/generate-all-sections', methods=['POST'])
def api_generate_all_sections(project_id):
    from modules.generator import generate_all_sections

    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        "SELECT anon_requirements, enable_figure, enable_table FROM projects WHERE id=?",
        (project_id,)
    )
    row = cur.fetchone()
    conn.close()
    anon_req = (row[0] or '') if row else ''
    enable_fig = bool(row[1]) if row else False
    enable_tbl = bool(row[2]) if row else False

    t = threading.Thread(
        target=generate_all_sections,
        args=(config.DB_PATH, project_id, anon_req, enable_fig, enable_tbl),
        daemon=True,
    )
    t.start()
    return jsonify({'success': True, 'message': '全量生成任务已启动'})


@app.route('/api/projects/<int:project_id>/section-progress', methods=['GET'])
def api_section_progress(project_id):
    conn = get_db()
    cur = conn.cursor()
    cur.execute('''
        SELECT
            COUNT(*) as total,
            SUM(CASE WHEN status='done' THEN 1 ELSE 0 END) as done,
            SUM(CASE WHEN status='generating' THEN 1 ELSE 0 END) as running,
            SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) as error_count
        FROM bid_sections WHERE project_id=?
    ''', (project_id,))
    r = cur.fetchone()
    conn.close()
    total, done, running, errors = r
    return jsonify({
        'total': total or 0, 'done': done or 0,
        'running': running or 0, 'errors': errors or 0,
        'percent': round((done or 0) / max(total or 1, 1) * 100),
    })


# ═══════════════════════════════════════════════════════════════════════════
#  API：合规检查
# ═══════════════════════════════════════════════════════════════════════════

@app.route('/api/projects/<int:project_id>/check', methods=['POST'])
def api_check(project_id):
    from modules.checker import check_compliance
    result = check_compliance(config.DB_PATH, project_id)
    return jsonify(result)


@app.route('/api/projects/<int:project_id>/check-dark-bid-format', methods=['POST'])
def api_check_dark_bid_format(project_id):
    """技术暗标 HTML 格式清标（不调用 AI）。请求体 JSON：{"html": "..."}"""
    conn = get_db()
    cur = conn.cursor()
    cur.execute("SELECT id FROM projects WHERE id=?", (project_id,))
    if not cur.fetchone():
        conn.close()
        return jsonify({'error': '项目不存在'}), 404
    conn.close()

    data = request.get_json(silent=True) or {}
    html = (data.get('html') or '').strip()
    if not html:
        return jsonify({'error': '请提供 html 字段（技术暗标完整 HTML 内容）'}), 400

    from modules.dark_bid_format_check import check_technical_bid
    try:
        result = check_technical_bid(html)
        return jsonify(result)
    except Exception as e:
        logger.exception('技术暗标格式清标失败')
        return jsonify({'error': str(e)}), 500


# ═══════════════════════════════════════════════════════════════════════════
#  API：导出
# ═══════════════════════════════════════════════════════════════════════════

@app.route('/api/projects/<int:project_id>/export', methods=['POST'])
def api_export(project_id):
    from modules.exporter import export_to_word
    data = request.get_json() or {}
    style_preset = data.get('style_preset', 'standard')
    try:
        filename = export_to_word(config.DB_PATH, project_id, style_preset)
        return jsonify({'success': True, 'filename': filename, 'url': f'/api/download/{filename}'})
    except Exception as e:
        logger.exception('导出失败')
        return jsonify({'error': str(e)}), 500


@app.route('/api/download/<filename>')
def api_download(filename):
    return send_from_directory(config.EXPORT_DIR, filename, as_attachment=True)


# ═══════════════════════════════════════════════════════════════════════════
#  API：知识库管理
# ═══════════════════════════════════════════════════════════════════════════

@app.route('/api/knowledge/status', methods=['GET'])
def api_knowledge_status():
    from modules.knowledge import is_available, list_files
    status = is_available()
    status['file_count'] = len(list_files(config.DB_PATH))
    return jsonify(status)


@app.route('/api/knowledge/files', methods=['GET'])
def api_knowledge_list():
    from modules.knowledge import list_files
    files = list_files(config.DB_PATH)
    return jsonify({'files': files})


@app.route('/api/knowledge/upload', methods=['POST'])
def api_knowledge_upload():
    from modules.knowledge import add_file
    from utils.file_utils import allowed_file, safe_filename
    import threading

    if 'file' not in request.files:
        return jsonify({'error': '未选择文件'}), 400
    f = request.files['file']
    if not f.filename or not allowed_file(f.filename):
        return jsonify({'error': '仅支持 PDF / DOC / DOCX'}), 400

    filename = safe_filename(f.filename)
    save_path = os.path.join(config.KNOWLEDGE_DIR, filename)
    f.save(save_path)

    # 后台线程入库（提取文本 + 向量化可能耗时，避免请求超时）
    def _ingest():
        result = add_file(save_path, config.DB_PATH)
        if not result.get('success'):
            logger.error(f'知识库入库失败 {filename}: {result.get("error")}')

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

    return jsonify({'success': True, 'queued': True, 'filename': filename})


@app.route('/api/knowledge/delete', methods=['POST'])
def api_knowledge_delete():
    from modules.knowledge import delete_file
    data = request.get_json() or {}
    file_name = data.get('file_name', '')
    if not file_name:
        return jsonify({'error': '缺少 file_name'}), 400
    result = delete_file(file_name, config.DB_PATH)
    return jsonify(result)


# ═══════════════════════════════════════════════════════════════════════════
#  API：AI 配置
# ═══════════════════════════════════════════════════════════════════════════

@app.route('/api/config', methods=['GET'])
def api_get_config():
    def _has_key(k): return bool(k and not k.startswith('sk-your'))
    return jsonify({
        'model_provider':    config.MODEL_PROVIDER,
        'qwen_model':        config.QWEN_MODEL,
        'qwen_base_url':     config.QWEN_BASE_URL,
        'openai_model':      config.OPENAI_MODEL,
        'openai_base_url':   config.OPENAI_BASE_URL,
        'deepseek_model':    config.DEEPSEEK_MODEL,
        'deepseek_base_url': config.DEEPSEEK_BASE_URL,
        'ollama_base_url':   config.OLLAMA_BASE_URL,
        'ollama_model':      config.OLLAMA_MODEL,
        'doubao_model':      config.DOUBAO_MODEL,
        'doubao_base_url':   config.DOUBAO_BASE_URL,
        'kimi_model':        config.KIMI_MODEL,
        'kimi_base_url':     config.KIMI_BASE_URL,
        'has_qwen_key':      _has_key(config.QWEN_API_KEY),
        'has_openai_key':    _has_key(config.OPENAI_API_KEY),
        'has_deepseek_key':  _has_key(config.DEEPSEEK_API_KEY),
        'has_doubao_key':    _has_key(config.DOUBAO_API_KEY),
        'has_kimi_key':      _has_key(config.KIMI_API_KEY),
        'max_concurrent':    config.MAX_CONCURRENT_SECTIONS,
        'llm_concurrency_limit': getattr(config, 'LLM_CONCURRENCY_LIMIT', 20),
        'content_volume':    config.CONTENT_VOLUME,
        'target_pages':      getattr(config, 'TARGET_PAGES', 0),
        'page_char_estimate': getattr(config, 'PAGE_CHAR_ESTIMATE', 700),
        # 监控指标：并发状态已集成到 progress.running 和日志；UI显示 "X路并发"
    })


@app.route('/api/config', methods=['POST'])
def api_save_config():
    data = request.get_json() or {}
    if 'model_provider' in data:
        config.MODEL_PROVIDER = data['model_provider']
    if 'qwen_api_key' in data and data['qwen_api_key']:
        config.QWEN_API_KEY = data['qwen_api_key']
    if 'qwen_model' in data and data['qwen_model']:
        config.QWEN_MODEL = data['qwen_model']
    if 'qwen_base_url' in data and data['qwen_base_url']:
        config.QWEN_BASE_URL = data['qwen_base_url']
    if 'openai_api_key' in data and data['openai_api_key']:
        config.OPENAI_API_KEY = data['openai_api_key']
    if 'openai_model' in data and data['openai_model']:
        config.OPENAI_MODEL = data['openai_model']
    if 'openai_base_url' in data and data['openai_base_url']:
        config.OPENAI_BASE_URL = data['openai_base_url']
    if 'deepseek_api_key' in data and data['deepseek_api_key']:
        config.DEEPSEEK_API_KEY = data['deepseek_api_key']
    if 'deepseek_model' in data and data['deepseek_model']:
        config.DEEPSEEK_MODEL = data['deepseek_model']
    if 'deepseek_base_url' in data and data['deepseek_base_url']:
        config.DEEPSEEK_BASE_URL = data['deepseek_base_url']
    if 'ollama_base_url' in data and data['ollama_base_url']:
        config.OLLAMA_BASE_URL = data['ollama_base_url']
    if 'ollama_model' in data and data['ollama_model']:
        config.OLLAMA_MODEL = data['ollama_model']
    if 'doubao_api_key' in data and data['doubao_api_key']:
        config.DOUBAO_API_KEY = data['doubao_api_key']
    if 'doubao_model' in data and data['doubao_model']:
        config.DOUBAO_MODEL = data['doubao_model']
    if 'doubao_base_url' in data and data['doubao_base_url']:
        config.DOUBAO_BASE_URL = data['doubao_base_url']
    if 'kimi_api_key' in data and data['kimi_api_key']:
        config.KIMI_API_KEY = data['kimi_api_key']
    if 'kimi_model' in data and data['kimi_model']:
        config.KIMI_MODEL = data['kimi_model']
    if 'kimi_base_url' in data and data['kimi_base_url']:
        config.KIMI_BASE_URL = data['kimi_base_url']
    if 'max_concurrent' in data:
        v = int(data['max_concurrent'])
        config.MAX_CONCURRENT_SECTIONS = max(1, min(v, 20))
    if 'llm_concurrency_limit' in data:
        try:
            v = int(data['llm_concurrency_limit'])
            config.LLM_CONCURRENCY_LIMIT = max(1, min(v, 30))
            # Note: semaphore recreated on next import/restart for simplicity
        except (ValueError, TypeError):
            pass
    if 'content_volume' in data and data['content_volume'] in ('concise', 'standard', 'detailed', 'full'):
        config.CONTENT_VOLUME = data['content_volume']
    if 'target_pages' in data:
        try:
            config.TARGET_PAGES = max(0, int(data['target_pages']))
        except (ValueError, TypeError):
            pass
    if 'page_char_estimate' in data:
        try:
            config.PAGE_CHAR_ESTIMATE = max(300, min(3000, int(data['page_char_estimate'])))
        except (ValueError, TypeError):
            pass
    if 'style_preset' in data:
        # Save selected style preset for the project (future extension)
        pass

    _settings.save(config)
    return jsonify({'success': True})


# 新增 /api/styles endpoints for homepage style settings
@app.route('/api/styles', methods=['GET'])
def api_get_styles():
    from utils.style_manager import DEFAULT_PRESETS
    return jsonify({'presets': DEFAULT_PRESETS, 'success': True})


@app.route('/api/styles', methods=['POST'])
def api_save_style_preset():
    data = request.get_json() or {}
    name = data.get('name', 'custom')
    preset_config = data.get('config', {})
    from utils.style_manager import save_preset
    if save_preset(name, preset_config):
        return jsonify({'success': True, 'name': name})
    return jsonify({'error': '保存失败'}), 500


# ═══════════════════════════════════════════════════════════════════════════
#  启动
# ═══════════════════════════════════════════════════════════════════════════

if __name__ == '__main__':
    init_db()
    print('\n' + '=' * 60)
    print('  BidPartner - AI Bid Writing Assistant')
    print('=' * 60)
    print('  URL: http://localhost:5000')
    print('  Press Ctrl+C to quit\n')
    app.run(host='0.0.0.0', port=5000, debug=False, threaded=True)
