# -*- coding: utf-8 -*-
"""Flask app factory。"""
import os
import click
from dotenv import load_dotenv
from flask import Flask
from flask_cors import CORS
from flask_login import current_user

load_dotenv()  # 加载 .env（开发用）

from app.config import Config
from app.extensions import db, migrate, login_manager, csrf


def create_app(config_class=Config):
    _root = os.path.dirname(os.path.dirname(__file__))  # 项目根
    app = Flask(__name__, template_folder=os.path.join(_root, "templates"))
    app.config.from_object(config_class)

    os.makedirs(app.config["UPLOAD_DIR"], exist_ok=True)

    db.init_app(app)
    migrate.init_app(app, db)
    login_manager.init_app(app)
    csrf.init_app(app)

    # CORS：仅 /api/* 对 H5 跨域开放（H5 在 FTP 域名）
    CORS(app, resources={r"/api/*": {"origins": app.config["CORS_ORIGINS"]}},
         methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])

    # 注册蓝图：API(/api, JWT) + 后台(/admin, session)
    from app.api import bp as api_bp
    from app.admin import bp as admin_bp
    csrf.exempt(api_bp)  # /api/ 走 JWT header 认证，浏览器不自动带 cookie，本就不受 CSRF，豁免以免打断 H5
    app.register_blueprint(api_bp)
    app.register_blueprint(admin_bp)

    from app.models import Staff

    @login_manager.user_loader
    def load_user(uid):
        try:
            return db.session.get(Staff, int(uid))
        except (ValueError, TypeError):
            return None

    @app.context_processor
    def inject_globals():
        return {"current_user": current_user}

    # 上传文件静态服务（开发期 Flask 服务；生产由 Nginx 接管 /uploads）
    from flask import send_from_directory

    @app.route("/uploads/<path:filename>")
    def serve_upload(filename):
        return send_from_directory(app.config["UPLOAD_DIR"], filename)

    register_cli(app)
    return app


def register_cli(app):
    from app.models import Staff, Chain, Store

    @app.cli.command("initdb")
    def initdb():
        """一次性建表（开发用；生产用 flask db upgrade）。"""
        db.create_all()
        click.echo("OK: tables created.")

    @app.cli.command("seed")
    def seed():
        """创建超级管理员种子账号。"""
        phone = os.environ.get("SUPER_PHONE", "13800000000")
        pwd = os.environ.get("SUPER_PASSWORD", "admin123")
        if Staff.query.filter_by(phone=phone).first():
            click.echo("Super admin already exists.")
            return
        s = Staff(name="超级管理员", phone=phone, role="super", status="active")
        s.set_password(pwd)
        db.session.add(s)
        db.session.commit()
        click.echo("OK: super admin created -> %s / %s" % (phone, pwd))

    @app.cli.command("initdb-seed")
    def initdb_seed():
        """建表 + 种子一步到位。"""
        db.create_all()
        click.echo("OK: tables created.")
        # 首页入口（销售看板/文件下载）+ 文件下载 4 分类 初始数据（幂等 upsert）
        from app.models import HomeFeature, DownloadCategory, DailyTab
        for fk, nm in [("salesboard", "销售看板"), ("downloads", "文件下载"), ("daily", "日常管理")]:
            if not HomeFeature.query.filter_by(feature_key=fk).first():
                db.session.add(HomeFeature(feature_key=fk, name=nm, scope_mode="all"))
        for i, (k, nm) in enumerate([("ka", "KA连锁"), ("medium", "中小连锁"),
                                     ("single", "单体药店"), ("cend", "C端平台")]):
            if not DownloadCategory.query.filter_by(key=k).first():
                db.session.add(DownloadCategory(key=k, name=nm, sort=100 - i, scope_mode="all"))
        if not DownloadCategory.query.filter_by(key="common").first():
            db.session.add(DownloadCategory(key="common", name="共用文件", sort=200, scope_mode="all"))
        # 日常管理 3 个 TAB（KA连锁访店首；每 TAB 各自配可见范围，见后台 /daily/settings）
        for k, nm, sort, door, office, ft in [
            ("ka", "KA连锁访店", 300, "药友经理+KA门店门头", "KA门店药友产品陈列", "ka"),
            ("yaoyou", "中小连锁拜访", 200, "药友经理+客户门头", "药友经理+客户办公区", "normal"),
            ("yiyao", "壹药网联合拜访", 100, "壹药网经理+客户门头", "壹药网经理+客户办公区", "normal"),
        ]:
            if not DailyTab.query.filter_by(key=k).first():
                db.session.add(DailyTab(key=k, name=nm, sort=sort,
                                        door_label=door, office_label=office,
                                        form_type=ft, scope_mode="all"))
        db.session.commit()
        click.echo("OK: home features + download categories + daily tabs seeded")
        phone = os.environ.get("SUPER_PHONE", "13800000000")
        pwd = os.environ.get("SUPER_PASSWORD", "admin123")
        if not Staff.query.filter_by(phone=phone).first():
            s = Staff(name="超级管理员", phone=phone, role="super", status="active")
            s.set_password(pwd)
            db.session.add(s)
            db.session.commit()
            click.echo("OK: super admin created -> %s / %s" % (phone, pwd))
        else:
            click.echo("Super admin already exists.")

    @app.cli.command("migrate-security")
    def migrate_security():
        """给 staff 表补 security_question/security_answer 列（幂等，兼容 SQLite/MySQL）。"""
        from sqlalchemy import text, inspect as sa_inspect
        insp = sa_inspect(db.engine)
        cols = [c["name"] for c in insp.get_columns("staff")]
        if "security_question" not in cols:
            db.session.execute(text("ALTER TABLE staff ADD COLUMN security_question VARCHAR(255)"))
            click.echo("added: security_question")
        if "security_answer" not in cols:
            db.session.execute(text("ALTER TABLE staff ADD COLUMN security_answer VARCHAR(255)"))
            click.echo("added: security_answer")
        db.session.commit()
        click.echo("OK: security columns ready")

    @app.cli.command("migrate-avatar")
    def migrate_avatar():
        """给 staff 表补 avatar 列（幂等，兼容 SQLite/MySQL）。"""
        from sqlalchemy import text, inspect as sa_inspect
        insp = sa_inspect(db.engine)
        cols = [c["name"] for c in insp.get_columns("staff")]
        if "avatar" not in cols:
            db.session.execute(text("ALTER TABLE staff ADD COLUMN avatar VARCHAR(255)"))
            click.echo("added: avatar")
        db.session.commit()
        click.echo("OK: avatar column ready")

    @app.cli.command("migrate-gamemode")
    def migrate_gamemode():
        """游戏闯关功能：exam 补 mode 列、exam_question 补 round 列（幂等，兼容 SQLite/MySQL）。"""
        from sqlalchemy import text, inspect as sa_inspect
        insp = sa_inspect(db.engine)
        exam_cols = [c["name"] for c in insp.get_columns("exam")]
        if "mode" not in exam_cols:
            db.session.execute(text("ALTER TABLE exam ADD COLUMN mode VARCHAR(20) DEFAULT 'quiz'"))
            click.echo("added: exam.mode")
        eq_cols = [c["name"] for c in insp.get_columns("exam_question")]
        if "round" not in eq_cols:
            db.session.execute(text("ALTER TABLE exam_question ADD COLUMN round INT NULL"))
            click.echo("added: exam_question.round")
        db.session.commit()
        click.echo("OK: gamemode columns ready")

    @app.cli.command("migrate-gamecode")
    def migrate_gamecode():
        """游戏 V2：exam_question 补 game_code、exam 补 game_config、question 补 image_url，
        并按 round 回填老游戏题目的 game_code（幂等，兼容 SQLite/MySQL）。"""
        from sqlalchemy import text, inspect as sa_inspect
        insp = sa_inspect(db.engine)
        # 首次部署表尚未创建（由 initdb-seed 的 create_all 建，新列已在 models.py 定义）→ 跳过
        if not insp.has_table("exam"):
            click.echo("skip: tables not created yet (create_all will include new columns)")
            return
        eq_cols = [c["name"] for c in insp.get_columns("exam_question")]
        if "game_code" not in eq_cols:
            db.session.execute(text("ALTER TABLE exam_question ADD COLUMN game_code VARCHAR(20)"))
            click.echo("added: exam_question.game_code")
        exam_cols = [c["name"] for c in insp.get_columns("exam")]
        if "game_config" not in exam_cols:
            db.session.execute(text("ALTER TABLE exam ADD COLUMN game_config TEXT"))
            click.echo("added: exam.game_config")
        q_cols = [c["name"] for c in insp.get_columns("question")]
        if "image_url" not in q_cols:
            db.session.execute(text("ALTER TABLE question ADD COLUMN image_url VARCHAR(255)"))
            click.echo("added: question.image_url")
        db.session.commit()
        # 按 round 回填老游戏题目的 game_code（1→match/2→judge/3→runner/4→mole），仅回填空值，幂等
        db.session.execute(text(
            "UPDATE exam_question SET game_code = CASE round "
            "WHEN 1 THEN 'match' WHEN 2 THEN 'judge' WHEN 3 THEN 'runner' WHEN 4 THEN 'mole' END "
            "WHERE game_code IS NULL AND round IS NOT NULL"
        ))
        db.session.commit()
        click.echo("OK: gamecode columns ready (legacy rounds backfilled)")

    @app.cli.command("migrate-checkin-ip")
    def migrate_checkin_ip():
        """给 checkin 表补 ip 列（记录打卡客户端 IP；幂等，兼容 SQLite/MySQL）。"""
        from sqlalchemy import text, inspect as sa_inspect
        insp = sa_inspect(db.engine)
        cols = [c["name"] for c in insp.get_columns("checkin")]
        if "ip" not in cols:
            db.session.execute(text("ALTER TABLE checkin ADD COLUMN ip VARCHAR(64)"))
            click.echo("added: checkin.ip")
        db.session.commit()
        click.echo("OK: checkin.ip column ready")

    @app.cli.command("migrate-staff-softdel")
    def migrate_staff_softdel():
        """给 staff 表补 deleted(软删除标记) + last_login(最后登录时间) 两列（幂等）。"""
        from sqlalchemy import text, inspect as sa_inspect
        insp = sa_inspect(db.engine)
        cols = [c["name"] for c in insp.get_columns("staff")]
        if "deleted" not in cols:
            db.session.execute(text("ALTER TABLE staff ADD COLUMN deleted BOOLEAN DEFAULT 0"))
            click.echo("added: staff.deleted")
        if "last_login" not in cols:
            db.session.execute(text("ALTER TABLE staff ADD COLUMN last_login DATETIME NULL"))
            click.echo("added: staff.last_login")
        db.session.commit()
        click.echo("OK: staff softdel columns ready")

    @app.cli.command("migrate-invoice-downloaded")
    def migrate_invoice_downloaded():
        """给 invoice 表补 downloaded/downloaded_at/downloaded_by 三列（后台下载标记；幂等，兼容 SQLite/MySQL）。"""
        from sqlalchemy import text, inspect as sa_inspect
        insp = sa_inspect(db.engine)
        cols = [c["name"] for c in insp.get_columns("invoice")]
        if "downloaded" not in cols:
            db.session.execute(text("ALTER TABLE invoice ADD COLUMN downloaded BOOLEAN DEFAULT 0"))
            click.echo("added: invoice.downloaded")
        if "downloaded_at" not in cols:
            db.session.execute(text("ALTER TABLE invoice ADD COLUMN downloaded_at DATETIME NULL"))
            click.echo("added: invoice.downloaded_at")
        if "downloaded_by" not in cols:
            db.session.execute(text("ALTER TABLE invoice ADD COLUMN downloaded_by INT NULL"))
            click.echo("added: invoice.downloaded_by")
        db.session.commit()
        click.echo("OK: invoice downloaded columns ready")

    @app.cli.command("migrate-daily-ka")
    def migrate_daily_ka():
        """给 daily_compliance 表补 ka_chain/displayed_products 两列（KA连锁访店 TAB 用；
        幂等，兼容 SQLite/MySQL）。"""
        from sqlalchemy import text, inspect as sa_inspect
        insp = sa_inspect(db.engine)
        cols = [c["name"] for c in insp.get_columns("daily_compliance")]
        if "ka_chain" not in cols:
            db.session.execute(text("ALTER TABLE daily_compliance ADD COLUMN ka_chain VARCHAR(60) NULL"))
            click.echo("added: daily_compliance.ka_chain")
        if "displayed_products" not in cols:
            db.session.execute(text("ALTER TABLE daily_compliance ADD COLUMN displayed_products VARCHAR(120) NULL"))
            click.echo("added: daily_compliance.displayed_products")
        db.session.commit()
        click.echo("OK: daily_compliance ka columns ready")

    @app.cli.command("seed-demo-game")
    def seed_demo_game():
        """创建一个示例闯关游戏（可乐必妥，4 关），含配对/判断/单选并分关。幂等（按标题判重）。"""
        from app.models import Question, Exam, ExamQuestion
        TITLE = "【示例】可乐必妥知识闯关"
        if Exam.query.filter_by(title=TITLE).first():
            click.echo("demo game already exists.")
            return
        cat = "可乐必妥示例"

        def Q(**kw):
            kw.setdefault("category", cat)
            kw.setdefault("points", 10)
            kw.setdefault("difficulty", 1)
            q = Question(**kw)
            db.session.add(q)
            return q

        pairs = [
            Q(qtype="配对", stem="商品名↔通用名", opt_a="可乐必妥", opt_b="左氧氟沙星", answer="PAIR"),
            Q(qtype="配对", stem="适应症", opt_a="社区获得性肺炎", opt_b="尿路感染", answer="PAIR"),
            Q(qtype="配对", stem="原研", opt_a="原研药", opt_b="日本第一三共", answer="PAIR"),
            Q(qtype="配对", stem="疗程·肺炎", opt_a="肺炎疗程", opt_b="7-14天", answer="PAIR"),
            Q(qtype="配对", stem="疗程·尿感", opt_a="尿路感染疗程", opt_b="3天", answer="PAIR"),
            Q(qtype="配对", stem="禁忌人群", opt_a="18岁以下", opt_b="禁用", answer="PAIR"),
        ]
        judges = [
            Q(qtype="判断", stem="可乐必妥属于氟喹诺酮类抗菌药", answer="T"),
            Q(qtype="判断", stem="18岁以下青少年可以使用可乐必妥", answer="F"),
            Q(qtype="判断", stem="孕妇及哺乳期妇女禁用可乐必妥", answer="T"),
            Q(qtype="判断", stem="可乐必妥与钙片需间隔2小时服用", answer="T"),
            Q(qtype="判断", stem="服用可乐必妥期间应少饮水", answer="F"),
            Q(qtype="判断", stem="可乐必妥是日本第一三共原研药", answer="T"),
            Q(qtype="判断", stem="症状好转后即可立即停药", answer="F"),
            Q(qtype="判断", stem="可乐必妥抑菌率高于仿制药", answer="T"),
        ]

        def single(stem, opts, ans):
            return Q(qtype="单选", stem=stem, opt_a=opts[0], opt_b=opts[1],
                     opt_c=opts[2], opt_d=opts[3], answer=ans)

        s3 = [
            single("可乐必妥的通用名是？", ["左氧氟沙星", "阿莫西林", "头孢克肟", "阿奇霉素"], "A"),
            single("以下哪类人群禁用可乐必妥？", ["18岁以下青少年", "65岁以上老人", "成年男性", "成年女性"], "A"),
            single("可乐必妥与钙片服用应间隔？", ["至少2小时", "30分钟", "可同服", "需停一种"], "A"),
            single("可乐必妥原研企业是？", ["日本第一三共", "德国拜耳", "美国辉瑞", "瑞士诺华"], "A"),
            single("尿路感染疗程通常为？", ["3天", "14天", "1天", "30天"], "A"),
            single("社区获得性肺炎疗程通常为？", ["7-14天", "1天", "半年", "3天"], "A"),
        ]
        s4 = [
            single("服药期间应？", ["多饮水保持尿量", "少饮水", "禁止饮水", "大量喝牛奶"], "A"),
            single("可乐必妥属于哪类抗菌药？", ["氟喹诺酮类", "头孢类", "青霉素类", "大环内酯类"], "A"),
            single("原研药的优势主要是？", ["安全性和疗效证据更充分", "价格更便宜", "完全无副作用", "口感更好"], "A"),
            single("可乐必妥体外抑菌率约为？", ["61.5%", "28.8%", "100%", "10%"], "A"),
            single("服药后出现皮疹应？", ["立即停药并就诊", "继续服药", "减半服用", "多喝水等待"], "A"),
            single("可乐必妥主要适用于？", ["细菌感染", "病毒感染", "真菌感染", "过敏反应"], "A"),
        ]
        db.session.commit()
        e = Exam(title=TITLE, description="示例闯关游戏——可在后台「考试管理」删除，或新建自己的游戏",
                 status="open", mode="game", pass_score=80, max_attempts=0, points_reward=50)
        db.session.add(e)
        db.session.commit()

        def add(qs, rnd):
            for i, q in enumerate(qs):
                db.session.add(ExamQuestion(exam_id=e.id, question_id=q.id, round=rnd, order=i))

        add(pairs, 1)
        add(judges, 2)
        add(s3, 3)
        add(s4, 4)
        db.session.commit()
        click.echo("OK: demo game created -> id=%d (配对%d/判断%d/单选%d+%d)" %
                   (e.id, len(pairs), len(judges), len(s3), len(s4)))
