# -*- coding: utf-8 -*-
"""店员 H5 用的 REST API：JWT 跨域认证。
H5（FTP 域名）fetch 本蓝图，Authorization: Bearer <token>。
各模块接口随 P1-P5 逐步补充。
"""
import os
import uuid
import datetime
import json
import random
from flask import request, jsonify, g, current_app, send_file
from werkzeug.utils import secure_filename

from app.models import (Staff, Store, Chain, Exam, ExamQuestion, ExamRecord, Product,
                        ExchangeOrder, LikeTask, LikeSubmission, PointsLog, Question,
                        Article, ArticleRead, Invoice, ExamParticipant, Feedback, StaffSeen,
                        Checkin, Setting, StoreReport, CaseSubmission, DailyCompliance,
                        DailyTab, DailyTabParticipant,
                        HomeFeature, HomeFeatureParticipant,
                        DownloadCategory, DownloadCategoryParticipant, DownloadFile,
                        GameExclusion, GameReward)
from app.extensions import db
from app.auth_utils import generate_token, token_required
from app.services import add_points, calc_like_reward, get_setting_int, get_setting
from app.api import bp  # 复用 __init__ 里的同一个蓝图
from app.admin.stores import sort_chains_for_display  # 连锁置顶+拼音排序（与后台一致）
from app.region_data import CHINA_REGION
from app.game_templates import (GAME_TEMPLATES, PAYLOAD_SHAPE, LEGACY_ROUND_CODE,
                                game_name, game_config)


ALLOWED_IMG = {"png", "jpg", "jpeg", "gif", "webp"}


def _allowed(filename):
    return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_IMG


# -------------------- 健康检查 --------------------

@bp.route("/ping", methods=["GET"])
def ping():
    return jsonify({"ok": True, "msg": "staff-platform API alive"})


@bp.route("/regions", methods=["GET"])
def regions():
    """公开：全国省市级联数据（H5 注册页下拉用，无需登录）。"""
    return jsonify({"regions": CHINA_REGION})


@bp.route("/chains", methods=["GET"])
def chains():
    """公开：连锁候选列表（供 H5 注册/我的信息下拉，源自后台 Chain 表；
    后台「连锁管理」增删改连锁后这里自动同步）。无需登录。"""
    names = [c.name for c in sort_chains_for_display(Chain.query.all())]
    return jsonify({"chains": names})


# -------------------- 认证（H5 JWT）--------------------

@bp.route("/auth/login", methods=["POST"])
def auth_login():
    data = request.get_json(silent=True) or {}
    phone = (data.get("phone") or "").strip()
    pwd = data.get("password") or ""
    staff = Staff.query.filter_by(phone=phone, role="staff").first()
    if not staff or staff.status != "active" or not staff.check_password(pwd):
        return jsonify({"error": "手机号或密码错误"}), 401
    token = generate_token(staff)
    staff.last_login = datetime.datetime.now()
    db.session.commit()
    return jsonify({
        "token": token,
        "staff": {
            "id": staff.id, "name": staff.name, "phone": staff.phone,
            "role": staff.role, "points": staff.points_balance,
            "store": staff.store.name if staff.store else None,
        },
    })


@bp.route("/auth/register", methods=["POST"])
def auth_register():
    """店员自助注册：姓名/手机号/连锁名称/门店名称/密码。
    连锁、门店按名称匹配，不存在则自动创建；注册后即可登录。"""
    import re
    data = request.get_json(silent=True) or {}
    name = (data.get("name") or "").strip()
    phone = (data.get("phone") or "").strip()
    chain_name = (data.get("chain") or "").strip()
    store_name = (data.get("store") or "").strip()
    province = (data.get("province") or "").strip()
    city = (data.get("city") or "").strip()
    pwd = data.get("password") or ""
    sec_q = (data.get("security_question") or "").strip()
    sec_a = (data.get("security_answer") or "").strip()
    if not name or not phone or not store_name or not pwd:
        return jsonify({"error": "请填写姓名、手机号、门店名称、密码"}), 400
    if not province or not city:
        return jsonify({"error": "请选择省份和城市"}), 400
    if not sec_q or not sec_a:
        return jsonify({"error": "请选择密码保护问题并填写答案"}), 400
    if not re.match(r"^1\d{10}$", phone):
        return jsonify({"error": "手机号格式不正确"}), 400
    if len(pwd) < 6:
        return jsonify({"error": "密码至少 6 位"}), 400
    if Staff.query.filter_by(phone=phone, role="staff").first():
        return jsonify({"error": "该手机号已注册，请直接登录"}), 400
    # 连锁按名称匹配/创建
    chain = None
    if chain_name:
        chain = Chain.query.filter_by(name=chain_name).first()
        if not chain:
            chain = Chain(name=chain_name)
            db.session.add(chain)
            db.session.flush()
    # 门店按 名称+连锁 匹配/创建
    store = Store.query.filter_by(
        name=store_name, chain_id=(chain.id if chain else None)).first()
    if not store:
        store = Store(name=store_name, chain_id=(chain.id if chain else None))
        db.session.add(store)
        db.session.flush()
    s = Staff(name=name, phone=phone, store_id=store.id, role="staff", status="active",
              province=province, city=city)
    s.set_password(pwd)
    s.security_question = sec_q
    s.set_security_answer(sec_a)
    db.session.add(s)
    db.session.commit()
    return jsonify({"ok": True, "msg": "注册成功"})


@bp.route("/auth/question", methods=["GET"])
def auth_question():
    """根据手机号返回该店员的密码保护问题（防枚举：不存在/未设置返回空）。"""
    phone = (request.args.get("phone") or "").strip()
    s = Staff.query.filter_by(phone=phone, role="staff").first()
    return jsonify({"question": (s.security_question if s and s.security_question else "")})


@bp.route("/auth/forgot", methods=["POST"])
def auth_forgot():
    """找回密码：手机号 + 密码保护问题答案 验证后重置。"""
    data = request.get_json(silent=True) or {}
    phone = (data.get("phone") or "").strip()
    answer = (data.get("answer") or "").strip()
    new_pwd = data.get("new_password") or ""
    if not phone or not answer or not new_pwd:
        return jsonify({"error": "请填写手机号、问题答案、新密码"}), 400
    if len(new_pwd) < 6:
        return jsonify({"error": "新密码至少 6 位"}), 400
    s = Staff.query.filter_by(phone=phone, role="staff").first()
    if not s or not s.check_security_answer(answer):
        return jsonify({"error": "密码保护问题答案不正确"}), 400
    s.set_password(new_pwd)
    db.session.commit()
    return jsonify({"ok": True, "msg": "密码已重置，请用新密码登录"})


@bp.route("/auth/profile", methods=["GET"])
@token_required
def auth_profile():
    s = g.current_staff
    return jsonify({
        "id": s.id, "name": s.name, "phone": s.phone, "role": s.role,
        "points": s.points_balance,
        "store": s.store.name if s.store else None,
        "chain": s.store.chain.name if (s.store and s.store.chain) else None,
        "province": s.province, "city": s.city,
        "avatar": s.avatar,
    })


@bp.route("/auth/avatar", methods=["POST"])
@token_required
def auth_avatar():
    """店员上传/更新头像：存 uploads/avatar/uuid.ext，更新 staff.avatar。"""
    s = g.current_staff
    f = request.files.get("avatar")
    if not f or not _allowed(f.filename):
        return jsonify({"error": "请上传头像图片（png/jpg/jpeg/gif/webp）"}), 400
    ext = f.filename.rsplit(".", 1)[1].lower()
    fn = "avatar/%s.%s" % (uuid.uuid4().hex, ext)
    path = os.path.join(current_app.config["UPLOAD_DIR"], fn)
    os.makedirs(os.path.dirname(path), exist_ok=True)
    f.save(path)
    s.avatar = "/uploads/" + fn
    db.session.commit()
    return jsonify({"ok": True, "avatar_url": s.avatar})


@bp.route("/auth/update", methods=["POST"])
@token_required
def auth_update():
    """店员自助修改：姓名 / 连锁 / 门店 / 密码（改密验证旧密码）。门店不存在自动建档。"""
    s = g.current_staff
    data = request.get_json(silent=True) or {}
    name = (data.get("name") or "").strip()
    phone = (data.get("phone") or "").strip()
    chain_name = (data.get("chain") or "").strip()
    store_name = (data.get("store") or "").strip()
    province = (data.get("province") or "").strip()
    city = (data.get("city") or "").strip()
    old_pwd = data.get("old_password") or ""
    new_pwd = data.get("new_password") or ""
    import re
    if not name or not store_name or not phone:
        return jsonify({"error": "姓名、手机号、门店名称必填"}), 400
    if not re.match(r"^1\d{10}$", phone):
        return jsonify({"error": "手机号格式不正确"}), 400
    if Staff.query.filter(Staff.phone == phone, Staff.role == "staff", Staff.id != s.id).first():
        return jsonify({"error": "该手机号已被其他店员占用"}), 400
    if new_pwd and len(new_pwd) < 6:
        return jsonify({"error": "新密码至少 6 位"}), 400
    if new_pwd and not old_pwd:
        return jsonify({"error": "修改密码需填写旧密码"}), 400
    if new_pwd and not s.check_password(old_pwd):
        return jsonify({"error": "旧密码不正确"}), 400
    chain = None
    if chain_name:
        chain = Chain.query.filter_by(name=chain_name).first()
        if not chain:
            chain = Chain(name=chain_name)
            db.session.add(chain)
            db.session.flush()
    store = Store.query.filter_by(name=store_name, chain_id=(chain.id if chain else None)).first()
    if not store:
        store = Store(name=store_name, chain_id=(chain.id if chain else None))
        db.session.add(store)
        db.session.flush()
    s.name = name
    s.phone = phone
    s.store_id = store.id
    s.province = province
    s.city = city
    if new_pwd:
        s.set_password(new_pwd)
    db.session.commit()
    return jsonify({"ok": True, "msg": "已保存"})


@bp.route("/points/logs", methods=["GET"])
@token_required
def points_logs():
    """H5：当前店员积分余额 + 流水（分页）。"""
    s = g.current_staff
    page = request.args.get("page", 1, type=int)
    pag = PointsLog.query.filter_by(staff_id=s.id).order_by(
        PointsLog.id.desc()).paginate(page=page, per_page=30)
    return jsonify({
        "balance": s.points_balance,
        "logs": [{
            "id": l.id, "delta": l.delta, "type": l.type,
            "remark": l.remark, "balance_after": l.balance_after,
            "time": l.created_at.strftime("%Y-%m-%d %H:%M") if l.created_at else None,
        } for l in pag.items],
        "total": pag.total, "page": page, "pages": pag.pages,
    })


# ==================== 培训考试（H5）====================

def _exam_visible(e, staff):
    """考试/游戏对当前店员是否可见（按 scope_mode 过滤参与范围）。"""
    mode = e.scope_mode or "all"
    if mode == "all":
        return True
    if mode == "chain":
        store = staff.store
        if not store or not store.chain_id:
            return False
        chains = json.loads(e.allowed_chains) if e.allowed_chains else []
        return store.chain_id in chains
    if mode == "staff":
        return db.session.query(ExamParticipant.id).filter_by(
            exam_id=e.id, staff_id=staff.id).first() is not None
    return True


# -------------------- 首页入口可见范围（销售看板/文件下载，同考试范围模式）--------------------

def _scope_visible(obj, staff, ParticipantModel, fk_field):
    """通用范围可见性（同 _exam_visible）：all→True；chain→店员连锁在 allowed_chains；
    staff→查指定人员关联表。obj 为 None（功能未配置）时返回 False。"""
    if obj is None:
        return False
    mode = obj.scope_mode or "all"
    if mode == "all":
        return True
    if mode == "chain":
        store = staff.store
        if not store or not store.chain_id:
            return False
        chains = json.loads(obj.allowed_chains) if obj.allowed_chains else []
        return store.chain_id in chains
    if mode == "staff":
        return db.session.query(ParticipantModel.id).filter_by(
            **{fk_field: obj.id, "staff_id": staff.id}).first() is not None
    return True


def _feature_visible(feature_key, staff):
    """首页某图标功能对店员是否可见（HomeFeature 配置）。配置缺失→不可见。"""
    f = HomeFeature.query.filter_by(feature_key=feature_key).first()
    return _scope_visible(f, staff, HomeFeatureParticipant, "feature_id")


def _salesboard_visible(staff):
    return _feature_visible("salesboard", staff)


def _downloads_icon_visible(staff):
    """文件下载图标可见 = 该店员至少有一个可见的下载分类（图标层范围跟随分类层，
    避免出现「图标显示但点进去空」）。与 /downloads 列表过滤口径一致。"""
    return any(_category_visible(c, staff) for c in DownloadCategory.query.all())


def _category_visible(cat, staff):
    """文件下载某分类对店员是否可见。"""
    return _scope_visible(cat, staff, DownloadCategoryParticipant, "category_id")


def _tab_visible(tab, staff):
    """日常管理某 TAB 对店员是否可见。"""
    return _scope_visible(tab, staff, DailyTabParticipant, "tab_id")


def _daily_icon_visible(staff):
    """日常管理图标可见 = 该店员至少有一个可见的 TAB（图标层范围跟随 TAB 层，
    与文件下载图标跟随分类同口径；避免「图标显示但点进去空」）。"""
    return any(_tab_visible(t, staff) for t in DailyTab.query.all())


@bp.route("/exams", methods=["GET"])
@token_required
def exam_list():
    """开放中且在时间窗内的考试列表，附店员的尝试次数/是否已通过。"""
    s = g.current_staff
    now = datetime.datetime.now()
    exams = Exam.query.filter_by(status="open").filter(
        db.or_(Exam.start_at.is_(None), Exam.start_at <= now),
        db.or_(Exam.end_at.is_(None), Exam.end_at >= now),
    ).order_by(Exam.id.desc()).all()
    out = []
    for e in exams:
        if not _exam_visible(e, s):
            continue
        attempts = ExamRecord.query.filter_by(staff_id=s.id, exam_id=e.id).count()
        passed = db.session.query(ExamRecord.id).filter_by(
            staff_id=s.id, exam_id=e.id, passed=True).first() is not None
        out.append({
            "id": e.id, "title": e.title, "description": e.description,
            "pass_score": e.pass_score, "max_attempts": e.max_attempts,
            "time_limit": e.time_limit, "points_reward": e.points_reward,
            "mode": e.mode or "quiz",
            "question_count": e.questions.count(),
            "attempts": attempts, "passed": passed,
        })
    return jsonify({"exams": out})


@bp.route("/exams/<int:exam_id>/start", methods=["GET"])
@token_required
def exam_start(exam_id):
    """开始考试：返回题目（顺序+选项打乱），不暴露答案。"""
    s = g.current_staff
    e = Exam.query.get_or_404(exam_id)
    if not _exam_visible(e, s):
        return jsonify({"error": "无权访问该考试"}), 403
    if e.status != "open":
        return jsonify({"error": "考试未开放"}), 400
    attempts = ExamRecord.query.filter_by(staff_id=s.id, exam_id=e.id).count()
    if e.max_attempts and attempts >= e.max_attempts:
        return jsonify({"error": "已达到最大考试次数（%d 次）" % e.max_attempts}), 403
    eqs = list(e.questions.order_by(ExamQuestion.order))
    random.shuffle(eqs)
    questions = []
    for eq in eqs:
        q = eq.question
        opts = [{"key": letter, "text": txt} for letter, txt in
                [("A", q.opt_a), ("B", q.opt_b), ("C", q.opt_c), ("D", q.opt_d)] if txt]
        random.shuffle(opts)  # 选项顺序打乱（防作弊）
        questions.append({
            "id": q.id, "stem": q.stem, "qtype": q.qtype, "points": q.points, "opts": opts,
        })
    return jsonify({
        "exam": {"id": e.id, "title": e.title, "pass_score": e.pass_score,
                 "time_limit": e.time_limit, "question_count": len(questions)},
        "questions": questions,
    })


@bp.route("/exams/<int:exam_id>/submit", methods=["POST"])
@token_required
def exam_submit(exam_id):
    """提交答卷：判分→首次通过发积分→写记录。"""
    s = g.current_staff
    e = Exam.query.get_or_404(exam_id)
    if not _exam_visible(e, s):
        return jsonify({"error": "无权访问该考试"}), 403
    if e.status != "open":
        return jsonify({"error": "考试未开放"}), 400
    attempts = ExamRecord.query.filter_by(staff_id=s.id, exam_id=e.id).count()
    if e.max_attempts and attempts >= e.max_attempts:
        return jsonify({"error": "已达到最大考试次数"}), 403
    data = request.get_json(silent=True) or {}
    answers = data.get("answers", {}) or {}
    duration = int(data.get("duration", 0) or 0)
    score, total, detail = 0, 0, []
    for eq in e.questions.all():
        q = eq.question
        total += q.points
        chosen = str(answers.get(str(q.id), "")).strip().upper()
        correct = bool(chosen) and chosen == (q.answer or "").strip().upper()
        if correct:
            score += q.points
        detail.append({"qid": q.id, "chosen": chosen, "answer": q.answer, "correct": correct})
    passed = score >= e.pass_score
    already_passed = db.session.query(ExamRecord.id).filter_by(
        staff_id=s.id, exam_id=e.id, passed=True).first() is not None
    points_earned = 0
    if passed and not already_passed and e.points_reward > 0:
        points_earned = e.points_reward
        # commit=False：发分与考试记录同事务提交，避免"发了分但没写记录"的不一致
        add_points(s, points_earned, "exam", ref_id=e.id,
                   remark="考试通过：" + e.title, commit=False)
    rec = ExamRecord(staff_id=s.id, exam_id=e.id, score=score, passed=passed,
                     points_earned=points_earned, rounds=json.dumps(detail), duration=duration)
    db.session.add(rec)
    db.session.commit()
    return jsonify({
        "score": score, "total": total, "pass_score": e.pass_score, "passed": passed,
        "points_earned": points_earned, "balance": s.points_balance, "detail": detail,
    })


# ==================== 游戏闯关（H5，mode=game）====================

def _game_pair(q):
    """配对题游戏序列化（翻牌/连连看/拼图/消消乐/合成复用）：opt_a=左、opt_b=右、stem=说明。"""
    return {"id": q.id, "a": q.opt_a or "", "b": q.opt_b or "",
            "topic": q.stem or "", "image": q.image_url or ""}


def _game_judge(q):
    """判断题游戏序列化。"""
    return {"id": q.id, "stem": q.stem or "",
            "answer": (q.answer or "").strip().upper(), "image": q.image_url or ""}


def _game_question(q):
    """单选/分类/图片题的游戏序列化：保留原始选项顺序与正确答案（前端实时反馈+提交时按原始 key 对比）。"""
    opts = [{"key": letter, "text": txt} for letter, txt in
            [("A", q.opt_a), ("B", q.opt_b), ("C", q.opt_c), ("D", q.opt_d)] if txt]
    return {"id": q.id, "stem": q.stem or "", "opts": opts,
            "answer": (q.answer or "").strip().upper(), "points": q.points,
            "image": q.image_url or ""}


# payload shape → 序列化函数（game-start 按 game_code 查 PAYLOAD_SHAPE 得 shape，再查此表得 builder）
_PAYLOAD_BUILDER = {"pairs": _game_pair, "items": _game_judge, "questions": _game_question}


@bp.route("/exams/<int:exam_id>/game-start", methods=["GET"])
@token_required
def game_start(exam_id):
    """游戏闯关专用：按 round 分组返回题目（仅返回有题的关卡，2~4 关）。
    与 /start 的区别：带正确答案（前端需实时判对错给反馈）、不洗牌、按关分组。"""
    s = g.current_staff
    e = Exam.query.get_or_404(exam_id)
    if not _exam_visible(e, s):
        return jsonify({"error": "无权访问该游戏"}), 403
    if (e.mode or "quiz") != "game":
        return jsonify({"error": "该考试不是游戏闯关模式"}), 400
    if e.status != "open":
        return jsonify({"error": "游戏未开放"}), 400
    attempts = ExamRecord.query.filter_by(staff_id=s.id, exam_id=e.id).count()
    if e.max_attempts and attempts >= e.max_attempts:
        return jsonify({"error": "已达到最大游戏次数（%d 次）" % e.max_attempts}), 403
    # 按 (round, game_code) 分组；game_code 为空时按老 round 号兜底（向后兼容 V1 数据）
    groups = {}
    for eq in e.questions.all():
        code = eq.game_code or LEGACY_ROUND_CODE.get(eq.round) or "match"
        groups.setdefault((eq.round or 0, code), []).append(eq.question)
    rounds = []
    for rnd, code in sorted(groups.keys()):
        shape = PAYLOAD_SHAPE.get(code)
        if not shape:
            continue
        payload = [_PAYLOAD_BUILDER[shape](q) for q in groups[(rnd, code)]]
        if not payload:
            continue
        rounds.append({
            "round": rnd,
            "name": game_name(code),
            "game_code": code,
            "config": game_config(code),
            shape: payload,
        })
    return jsonify({
        "exam": {"id": e.id, "title": e.title, "pass_score": e.pass_score,
                 "points_reward": e.points_reward, "mode": "game"},
        "rounds": rounds,
    })


@bp.route("/exams/<int:exam_id>/game-submit", methods=["POST"])
@token_required
def game_submit(exam_id):
    """游戏闯关提交：按正确率判分（配对=matched 集合；判断/单选=answers 字头对比）
    → 首次通过发积分（type=game）→ 写 ExamRecord。趣味分数仅展示，不参与通过判定。"""
    s = g.current_staff
    e = Exam.query.get_or_404(exam_id)
    if not _exam_visible(e, s):
        return jsonify({"error": "无权访问该游戏"}), 403
    if e.status != "open":
        return jsonify({"error": "游戏未开放"}), 400
    attempts = ExamRecord.query.filter_by(staff_id=s.id, exam_id=e.id).count()
    if e.max_attempts and attempts >= e.max_attempts:
        return jsonify({"error": "已达到最大游戏次数"}), 403
    data = request.get_json(silent=True) or {}
    answers = data.get("answers", {}) or {}
    matched = set(int(x) for x in (data.get("matched", []) or []))
    duration = int(data.get("duration", 0) or 0)
    game_score = int(data.get("game_score", 0) or 0)
    round_scores = data.get("round_scores", []) or []   # 各关分项（前端 G.roundScores，预留展示）
    score, total, detail = 0, 0, []
    for eq in e.questions.all():
        q = eq.question
        total += q.points
        if q.qtype == "配对":
            chosen = "PAIR" if q.id in matched else ""
            correct = q.id in matched
        else:
            chosen = str(answers.get(str(q.id), "")).strip().upper()
            correct = bool(chosen) and chosen == (q.answer or "").strip().upper()
        if correct:
            score += q.points
        detail.append({"qid": q.id, "round": eq.round,
                       "game_code": eq.game_code or LEGACY_ROUND_CODE.get(eq.round),
                       "chosen": chosen, "answer": q.answer, "correct": correct})
    passed = score >= e.pass_score   # 保留计算（向后兼容）；游戏模式玩完即上榜，passed 不再 gating/发分
    # 游戏取消自动发分：改为管理员在后台英雄榜手动发放（GameReward 防重复，见后台英雄榜页）
    points_earned = 0
    rec = ExamRecord(staff_id=s.id, exam_id=e.id, score=score, passed=passed,
                     points_earned=points_earned,
                     rounds=json.dumps({"game_score": game_score, "round_scores": round_scores, "detail": detail}),
                     duration=duration)
    db.session.add(rec)
    db.session.commit()
    return jsonify({
        "score": score, "total": total, "pass_score": e.pass_score, "passed": passed,
        "points_earned": points_earned, "balance": s.points_balance, "game_score": game_score,
    })


@bp.route("/exams/<int:exam_id>/leaderboard", methods=["GET"])
@token_required
def game_leaderboard(exam_id):
    """游戏英雄榜（H5 玩家端）：按速度奖励分 game_score 降序，每人取最高分，排除被屏蔽店员。
    玩完即上榜（不要求 passed）。返回 board + total + my_rank + my_score。"""
    s = g.current_staff
    e = Exam.query.get_or_404(exam_id)
    excluded = set(x.staff_id for x in GameExclusion.query.filter_by(exam_id=exam_id).all())
    recs = ExamRecord.query.filter_by(exam_id=exam_id).all()
    best = {}   # staff_id -> {score, duration}
    for r in recs:
        if not r.staff_id or r.staff_id in excluded:
            continue
        try:
            gs = int((json.loads(r.rounds) if r.rounds else {}).get("game_score", 0))
        except Exception:
            gs = 0
        cur = best.get(r.staff_id)
        if cur is None or gs > cur["score"]:
            best[r.staff_id] = {"score": gs, "duration": r.duration or 0}
    rows = []
    for sid, info in best.items():
        st = Staff.query.get(sid)
        if not st:
            continue
        chain = "-"
        if st.store and st.store.chain:
            chain = st.store.chain.name
        rows.append({"staff_id": sid, "name": st.name, "chain": chain,
                     "score": info["score"], "duration": info["duration"]})
    rows.sort(key=lambda x: (-x["score"], x["duration"]))
    board, my_rank = [], None
    for i, row in enumerate(rows):
        rank = i + 1
        if row["staff_id"] == s.id:
            my_rank = rank
        board.append({"rank": rank, "name": row["name"], "chain": row["chain"],
                      "score": row["score"], "duration": row["duration"]})
    return jsonify({
        "exam": {"id": e.id, "title": e.title},
        "board": board,
        "total": len(board),
        "my_rank": my_rank,
        "my_score": best[s.id]["score"] if s.id in best else 0,
    })


# ==================== 文章资讯（H5）====================

@bp.route("/articles", methods=["GET"])
@token_required
def article_list_api():
    """H5：已发布文章列表。is_new=当前店员未读（点进详情后标记已读，NEW 消失）。"""
    s = g.current_staff
    limit = request.args.get("limit", 5, type=int)
    items = Article.query.filter_by(status="published").order_by(
        Article.sort.desc(), Article.id.desc()).limit(min(limit, 50)).all()
    read_ids = set(r[0] for r in db.session.query(ArticleRead.article_id)
                   .filter(ArticleRead.staff_id == s.id).all())
    return jsonify({"articles": [{
        "id": a.id, "title": a.title, "summary": a.summary or "",
        "cover": a.cover or "",
        "time": a.created_at.strftime("%Y-%m-%d") if a.created_at else None,
        "is_new": a.id not in read_ids,
    } for a in items]})


@bp.route("/articles/<int:aid>", methods=["GET"])
@token_required
def article_detail_api(aid):
    s = g.current_staff
    a = Article.query.get_or_404(aid)
    if a.status != "published":
        return jsonify({"error": "文章不存在"}), 404
    # 阅读即标记已读（幂等：已存在则刷新 read_at），列表里该条 NEW 随之消失
    rec = ArticleRead.query.filter_by(staff_id=s.id, article_id=aid).first()
    if rec:
        rec.read_at = datetime.datetime.now()
    else:
        db.session.add(ArticleRead(staff_id=s.id, article_id=aid))
    db.session.commit()
    return jsonify({
        "id": a.id, "title": a.title, "summary": a.summary or "", "cover": a.cover or "",
        "content": a.content or "",
        "time": a.created_at.strftime("%Y-%m-%d %H:%M") if a.created_at else None,
    })


# ==================== 发票上传（H5）====================

ALLOWED_INVOICE = {"pdf", "jpg", "jpeg", "png"}


@bp.route("/invoices", methods=["GET"])
@token_required
def invoice_list_api():
    """当前店员已上传的发票列表。"""
    s = g.current_staff
    items = Invoice.query.filter_by(staff_id=s.id).order_by(Invoice.id.desc()).all()
    return jsonify({"invoices": [{
        "id": i.id, "filename": i.filename, "file_url": i.file_url,
        "status": i.status,
        "time": i.created_at.strftime("%Y-%m-%d %H:%M") if i.created_at else None,
    } for i in items]})


@bp.route("/invoices", methods=["POST"])
@token_required
def invoice_upload():
    """店员上传高铁电子发票（PDF/图片），一人可多条。"""
    s = g.current_staff
    f = request.files.get("file")
    if not f or not f.filename or "." not in f.filename:
        return jsonify({"error": "请选择电子发票文件"}), 400
    ext = f.filename.rsplit(".", 1)[1].lower()
    if ext not in ALLOWED_INVOICE:
        return jsonify({"error": "仅支持 PDF / JPG / PNG 格式"}), 400
    fn = "invoice/%s.%s" % (uuid.uuid4().hex, ext)
    path = os.path.join(current_app.config["UPLOAD_DIR"], fn)
    os.makedirs(os.path.dirname(path), exist_ok=True)
    f.save(path)
    inv = Invoice(staff_id=s.id, filename=f.filename, file_url="/uploads/" + fn, status="uploaded")
    db.session.add(inv)
    db.session.commit()
    return jsonify({"ok": True, "id": inv.id, "file_url": inv.file_url})


# ==================== 意见反馈（H5）====================

@bp.route("/feedback", methods=["GET"])
@token_required
def feedback_list_api():
    """当前店员的意见反馈列表（含客服回复）。"""
    s = g.current_staff
    items = Feedback.query.filter_by(staff_id=s.id).order_by(Feedback.id.desc()).all()
    return jsonify({"feedback": [{
        "id": f.id, "title": f.title, "content": f.content,
        "screenshot": f.screenshot_url or "", "status": f.status,
        "reply": f.reply or "",
        "time": f.created_at.strftime("%Y-%m-%d %H:%M") if f.created_at else None,
        "reply_time": f.replied_at.strftime("%Y-%m-%d %H:%M") if f.replied_at else None,
    } for f in items]})


@bp.route("/feedback", methods=["POST"])
@token_required
def feedback_create():
    """店员提交意见反馈（标题≤20 + 内容 + 可选截图）。"""
    s = g.current_staff
    title = (request.form.get("title") or "").strip()
    content = (request.form.get("content") or "").strip()
    if not title or not content:
        return jsonify({"error": "请填写标题和内容"}), 400
    if len(title) > 20:
        return jsonify({"error": "标题不超过 20 字"}), 400
    f = Feedback(staff_id=s.id, title=title, content=content, status="pending")
    file = request.files.get("screenshot")
    if file and file.filename and "." in file.filename and \
            file.filename.rsplit(".", 1)[1].lower() in ALLOWED_IMG:
        ext = file.filename.rsplit(".", 1)[1].lower()
        fn = "feedback/%s.%s" % (uuid.uuid4().hex, ext)
        path = os.path.join(current_app.config["UPLOAD_DIR"], fn)
        os.makedirs(os.path.dirname(path), exist_ok=True)
        file.save(path)
        f.screenshot_url = "/uploads/" + fn
    db.session.add(f)
    db.session.commit()
    return jsonify({"ok": True, "id": f.id})


# ==================== 首页未读红点（H5）====================

@bp.route("/badges", methods=["GET"])
@token_required
def badges():
    """各频道未读数：趣味互动(新考试/游戏)、朋友圈集赞(新任务)、联系我们(新回复)。"""
    s = g.current_staff
    now = datetime.datetime.now()
    seen = {r.channel: r.seen_at for r in StaffSeen.query.filter_by(staff_id=s.id).all()}
    se = seen.get("exams")
    eq = Exam.query.filter_by(status="open").filter(
        db.or_(Exam.start_at.is_(None), Exam.start_at <= now),
        db.or_(Exam.end_at.is_(None), Exam.end_at >= now))
    if se:
        eq = eq.filter(Exam.created_at > se)
    exams_n = sum(1 for e in eq.all() if _exam_visible(e, s))
    sl = seen.get("likes")
    lq = LikeTask.query.filter_by(status="open").filter(
        db.or_(LikeTask.deadline.is_(None), LikeTask.deadline >= now))
    if sl:
        lq = lq.filter(LikeTask.created_at > sl)
    likes_n = lq.count()
    sf = seen.get("feedback")
    fq = Feedback.query.filter_by(staff_id=s.id, status="replied")
    if sf:
        fq = fq.filter(Feedback.replied_at > sf)
    feedback_n = fq.count()
    # points：我的积分流水晚于上次查看
    sp = seen.get("points")
    pq = PointsLog.query.filter_by(staff_id=s.id)
    if sp:
        pq = pq.filter(PointsLog.created_at > sp)
    points_n = pq.count()
    # shop：上架商品晚于上次查看
    ss = seen.get("shop")
    sq = Product.query.filter_by(status="on")
    if ss:
        sq = sq.filter(Product.created_at > ss)
    shop_n = sq.count()
    # orders：我的订单已发货(shipped_at)晚于上次查看
    so = seen.get("orders")
    oq = ExchangeOrder.query.filter(ExchangeOrder.staff_id == s.id,
                                    ExchangeOrder.shipped_at.isnot(None))
    if so:
        oq = oq.filter(ExchangeOrder.shipped_at > so)
    orders_n = oq.count()
    # checkin：未打卡天数（今天已打卡→0；否则距上次打卡的天数；从未打卡→1）；打卡后自动清零
    last_ck = Checkin.query.filter_by(staff_id=s.id).order_by(Checkin.checkin_date.desc()).first()
    if not last_ck:
        checkin_n = 1
    elif last_ck.checkin_date == now.date():
        checkin_n = 0
    else:
        checkin_n = (now.date() - last_ck.checkin_date).days
    # 销售看板：看板总金额变更时间晚于上次查看
    salesboard_n = 0
    if _salesboard_visible(s):
        sb_changed = get_setting("salesboard.changed_at")
        if sb_changed:
            try:
                sb_dt = datetime.datetime.fromisoformat(sb_changed)
                ssb = seen.get("salesboard")
                if not ssb or sb_dt > ssb:
                    salesboard_n = 1
            except (ValueError, TypeError):
                pass
    # 文件下载：每分类独立已读(downloads_<key>)，未读文件数求和（点某分类 tab 清该分类）
    downloads_n = 0
    if _downloads_icon_visible(s):
        for c in DownloadCategory.query.all():
            if not _category_visible(c, s):
                continue
            sd = seen.get("downloads_" + c.key)
            fq = c.files
            if sd:
                fq = fq.filter(DownloadFile.created_at > sd)
            downloads_n += fq.count()
    return jsonify({"badges": {"exams": exams_n, "likes": likes_n, "feedback": feedback_n,
                               "points": points_n, "shop": shop_n, "orders": orders_n,
                               "checkin": checkin_n, "salesboard": salesboard_n,
                               "downloads": downloads_n}})


@bp.route("/seen/<channel>", methods=["POST"])
@token_required
def mark_seen(channel):
    """店员进入某频道页面时标记已读（清红点）。"""
    s = g.current_staff
    if channel not in ("exams", "likes", "feedback", "articles", "points", "shop", "orders",
                       "salesboard", "downloads") and not channel.startswith("downloads_"):
        return jsonify({"error": "无效频道"}), 400
    row = StaffSeen.query.filter_by(staff_id=s.id, channel=channel).first()
    now = datetime.datetime.now()
    if row:
        row.seen_at = now
    else:
        db.session.add(StaffSeen(staff_id=s.id, channel=channel, seen_at=now))
    db.session.commit()
    return jsonify({"ok": True})


# ==================== 首页入口：销售看板 / 文件下载（H5）====================

SALESBOARD_URL = "https://yaocloud.win/xyxkb/xyx/kb.html"  # 新加坡 xyx 看板（主域名 nginx 已映射该目录）


@bp.route("/home/toggles", methods=["GET"])
@token_required
def home_toggles():
    """首页两个图标的可见性（无权→前端隐藏整行）。"""
    s = g.current_staff
    return jsonify({"salesboard": _salesboard_visible(s),
                    "downloads": _downloads_icon_visible(s),
                    "daily": _daily_icon_visible(s)})


@bp.route("/salesboard/info", methods=["GET"])
@token_required
def salesboard_info():
    """销售看板：可见性 + 跳转链接 + 当前总金额 + 变更时间。"""
    s = g.current_staff
    visible = _salesboard_visible(s)
    total = None
    t = get_setting("salesboard.total")
    if t:
        try:
            total = float(t)
        except (ValueError, TypeError):
            total = None
    return jsonify({"visible": visible, "url": SALESBOARD_URL,
                    "total": total, "changed_at": get_setting("salesboard.changed_at")})


@bp.route("/downloads", methods=["GET"])
@token_required
def downloads_list():
    """文件下载：该店员有权看到的分类及各分类文件列表（含每分类未读数）。"""
    s = g.current_staff
    seen = {r.channel: r.seen_at for r in StaffSeen.query.filter_by(staff_id=s.id).all()}
    cats = DownloadCategory.query.order_by(
        DownloadCategory.sort.desc(), DownloadCategory.id).all()
    out = []
    for c in cats:
        if not _category_visible(c, s):
            continue
        files_q = c.files.order_by(DownloadFile.pinned.desc(), DownloadFile.pinned_at.desc(), DownloadFile.id.desc())
        files_all = files_q.all()
        sd = seen.get("downloads_" + c.key)
        unread = files_q.filter(DownloadFile.created_at > sd).count() if sd else len(files_all)
        files = [{"id": f.id, "filename": f.filename, "size": f.size, "pinned": f.pinned,
                  "chain": f.chain.name if f.chain else None,
                  "time": f.created_at.strftime("%Y-%m-%d %H:%M") if f.created_at else ""}
                 for f in files_all]
        out.append({"key": c.key, "name": c.name, "files": files, "unread": unread})
    return jsonify({"categories": out})


@bp.route("/downloads/<int:fid>", methods=["GET"])
@token_required
def downloads_file(fid):
    """下载单个文件（非可见分类→403）。"""
    s = g.current_staff
    f = DownloadFile.query.get_or_404(fid)
    if not _category_visible(f.category, s):
        return jsonify({"error": "无权下载该文件"}), 403
    url = f.file_url or ""
    prefix = "/uploads/"
    rel = url[len(prefix):] if url.startswith(prefix) else url.lstrip("/")
    path = os.path.join(current_app.config["UPLOAD_DIR"], rel)
    if not os.path.isfile(path):
        return jsonify({"error": "文件不存在"}), 404
    return send_file(path, as_attachment=True,
                     download_name=f.filename or os.path.basename(path))


# ==================== 每日打卡（H5）====================

@bp.route("/checkin", methods=["GET"])
@token_required
def checkin_status():
    """今日打卡状态 + 当前连续天数（今天已打卡→今天 streak；否则昨日 streak 或 0）。"""
    s = g.current_staff
    today = datetime.date.today()
    today_row = Checkin.query.filter_by(staff_id=s.id, checkin_date=today).first()
    last = Checkin.query.filter_by(staff_id=s.id).order_by(Checkin.checkin_date.desc()).first()
    if today_row:
        streak = today_row.streak
    elif last and last.checkin_date == today - datetime.timedelta(days=1):
        streak = last.streak
    else:
        streak = 0
    start = today - datetime.timedelta(days=6)
    rows = Checkin.query.filter(Checkin.staff_id == s.id, Checkin.checkin_date >= start).all()
    checked = set(r.checkin_date for r in rows)
    recent = []
    for i in range(6, -1, -1):
        d = today - datetime.timedelta(days=i)
        recent.append({"date": d.strftime("%m-%d"), "w": "一二三四五六日"[d.weekday()],
                       "on": d in checked, "today": d == today})
    return jsonify({"checked_today": bool(today_row), "streak": streak,
                    "reward": get_setting_int("checkin_reward", 1), "recent": recent})


@bp.route("/checkin", methods=["POST"])
@token_required
def checkin_do():
    """执行今日打卡：连续第 2 天起每天发 checkin_reward 积分；中断则连击清零。"""
    s = g.current_staff
    today = datetime.date.today()
    if Checkin.query.filter_by(staff_id=s.id, checkin_date=today).first():
        return jsonify({"error": "今天已经打卡过了", "checked_today": True}), 400
    last = Checkin.query.filter_by(staff_id=s.id).order_by(Checkin.checkin_date.desc()).first()
    if last and last.checkin_date == today - datetime.timedelta(days=1):
        streak = last.streak + 1
    else:
        streak = 1
    reward = get_setting_int("checkin_reward", 1) if streak >= 2 else 0
    # 客户端真实 IP：CF-Connecting-IP(Cloudflare) > X-Real-IP > X-Forwarded-For 首段 > remote_addr
    ip = (request.headers.get("CF-Connecting-IP")
          or request.headers.get("X-Real-IP")
          or (request.headers.get("X-Forwarded-For", "").split(",")[0].strip() or None)
          or request.remote_addr)
    row = Checkin(staff_id=s.id, checkin_date=today, streak=streak, points_earned=reward, ip=ip)
    db.session.add(row)
    db.session.flush()
    if reward > 0:
        add_points(s, reward, "checkin", ref_id=row.id,
                   remark="每日打卡（连续 %d 天）" % streak, commit=False)
    db.session.commit()
    return jsonify({"ok": True, "streak": streak, "points_earned": reward,
                    "balance": s.points_balance})


# ==================== 积分商城（H5）====================

@bp.route("/products", methods=["GET"])
@token_required
def product_list():
    products = Product.query.filter_by(status="on").order_by(
        Product.points_cost.desc(), Product.id.desc()).all()
    return jsonify({"products": [{
        "id": p.id, "name": p.name, "image_url": p.image_url,
        "points_cost": p.points_cost, "stock": p.stock,
        "category": p.category, "description": p.description,
    } for p in products]})


@bp.route("/exchange", methods=["POST"])
@token_required
def exchange():
    s = g.current_staff
    data = request.get_json(silent=True) or {}
    p = Product.query.get(data.get("product_id"))
    if not p or p.status != "on":
        return jsonify({"error": "商品不存在或已下架"}), 400
    if p.stock is not None and p.stock <= 0:
        return jsonify({"error": "库存不足"}), 400
    if s.points_balance < p.points_cost:
        return jsonify({"error": "积分不足（需 %d，当前 %d）" % (p.points_cost, s.points_balance)}), 400
    cost = p.points_cost
    # 原子扣库存：仅当 stock>0 时 -1；并发下若已被抢光则更新 0 行 → 回滚
    n_stock = db.session.query(Product).filter(
        Product.id == p.id, Product.stock > 0).update(
        {Product.stock: Product.stock - 1}, synchronize_session=False)
    if not n_stock:
        db.session.rollback()
        return jsonify({"error": "手慢了，库存已被抢光"}), 409
    # 原子扣积分：走统一入口（余额不足更新 0 行 → add_points 返回 None → 回滚库存扣减）
    log = add_points(s, -cost, "exchange", ref_id=p.id,
                     remark="兑换：" + p.name, commit=False)
    if log is None:
        db.session.rollback()
        bal = db.session.query(Staff.points_balance).filter_by(id=s.id).scalar() or 0
        return jsonify({"error": "积分不足（需 %d，当前 %d）" % (cost, bal)}), 400
    o = ExchangeOrder(staff_id=s.id, product_id=p.id, points_cost=cost, status="pending",
                      address=(data.get("address") or ""), receiver=(data.get("receiver") or ""),
                      receiver_phone=(data.get("phone") or ""))
    db.session.add(o)
    db.session.commit()
    # 库存扣减 + 扣积分(余额+流水) + 建订单 一次提交，原子一致；commit 后 expire 自动重载
    return jsonify({"ok": True, "order_id": o.id, "balance": log.balance_after, "stock": p.stock})


@bp.route("/orders", methods=["GET"])
@token_required
def my_orders():
    s = g.current_staff
    orders = ExchangeOrder.query.filter_by(staff_id=s.id).order_by(ExchangeOrder.id.desc()).limit(50).all()
    return jsonify({"orders": [{
        "id": o.id, "product": o.product.name if o.product else None,
        "points": o.points_cost, "status": o.status, "tracking_no": o.tracking_no,
        "address": o.address, "receiver": o.receiver,
        "time": o.created_at.strftime("%Y-%m-%d %H:%M") if o.created_at else None,
    } for o in orders]})


# ==================== 朋友圈集赞（H5）====================

@bp.route("/like-tasks", methods=["GET"])
@token_required
def like_task_list():
    s = g.current_staff
    now = datetime.datetime.now()
    tasks = LikeTask.query.filter_by(status="open").filter(
        db.or_(LikeTask.deadline.is_(None), LikeTask.deadline >= now)
    ).order_by(LikeTask.id.desc()).all()
    out = []
    for t in tasks:
        mine = LikeSubmission.query.filter_by(staff_id=s.id, task_id=t.id).order_by(
            LikeSubmission.id.desc()).first()
        out.append({
            "id": t.id, "title": t.title, "content": t.content, "image_url": t.image_url,
            "reward_rules": json.loads(t.reward_rules) if t.reward_rules else {},
            "deadline": t.deadline.strftime("%Y-%m-%d %H:%M") if t.deadline else None,
            "my_status": mine.status if mine else None,
        })
    return jsonify({"tasks": out})


@bp.route("/like/submit", methods=["POST"])
@token_required
def like_submit():
    s = g.current_staff
    task_id = request.form.get("task_id", type=int)
    claimed = request.form.get("claimed_likes", 0, type=int)
    f = request.files.get("screenshot")
    task = LikeTask.query.get(task_id) if task_id else None
    if not task or task.status != "open":
        return jsonify({"error": "任务不存在或已关闭"}), 400
    exist = LikeSubmission.query.filter_by(staff_id=s.id, task_id=task.id, status="pending").first()
    if exist:
        return jsonify({"error": "你已有待审核的提交，请等待审核"}), 400
    if not f or not _allowed(f.filename):
        return jsonify({"error": "请上传截图（png/jpg/jpeg/gif）"}), 400
    ext = f.filename.rsplit(".", 1)[1].lower()
    fn = "like/%s.%s" % (uuid.uuid4().hex, ext)
    path = os.path.join(current_app.config["UPLOAD_DIR"], fn)
    os.makedirs(os.path.dirname(path), exist_ok=True)
    f.save(path)
    sub = LikeSubmission(staff_id=s.id, task_id=task.id, screenshot_url="/uploads/" + fn,
                         claimed_likes=claimed, status="pending")
    db.session.add(sub)
    db.session.commit()
    return jsonify({"ok": True, "id": sub.id, "status": "pending"})


@bp.route("/like/mine", methods=["GET"])
@token_required
def like_mine():
    s = g.current_staff
    subs = LikeSubmission.query.filter_by(staff_id=s.id).order_by(
        LikeSubmission.id.desc()).limit(50).all()
    return jsonify({"submissions": [{
        "id": su.id, "task": su.task.title if su.task else None,
        "screenshot": su.screenshot_url, "claimed_likes": su.claimed_likes,
        "status": su.status, "points_earned": su.points_earned, "reject_reason": su.reject_reason,
        "time": su.created_at.strftime("%Y-%m-%d %H:%M") if su.created_at else None,
    } for su in subs]})


# ==================== 门店运营月报（H5）====================


@bp.route("/store-reports", methods=["GET"])
@token_required
def store_report_list():
    """当前店员的门店运营提交记录（仅时间+状态，不回显图片；图片仅供后台审核）。"""
    s = g.current_staff
    items = StoreReport.query.filter_by(staff_id=s.id).order_by(StoreReport.id.desc()).all()
    cur_period = datetime.datetime.now().strftime("%Y-%m")
    # 仅当本月无 pending/approved 时可提交；rejected 不阻止（可改图重交）
    cur_month = [r for r in items if r.period == cur_period]
    can_submit = not any(r.status in ("pending", "approved") for r in cur_month)
    return jsonify({
        "can_submit": can_submit,
        "period": cur_period,
        "reward": get_setting_int("store_report_reward", 100),
        "reports": [{
            "id": r.id, "status": r.status, "points": r.points_earned,
            "reason": r.reject_reason or "",
            "time": r.created_at.strftime("%Y-%m-%d %H:%M") if r.created_at else None,
            "review_time": r.reviewed_at.strftime("%Y-%m-%d %H:%M") if r.reviewed_at else None,
        } for r in items],
    })


@bp.route("/store-reports", methods=["POST"])
@token_required
def store_report_upload():
    """上传门店运营月报（3张图）。每人自然月1次；rejected 可改图重交（覆盖原记录）。
    行锁串行化 + status 二次校验，防双击/并发把 approved 覆盖回 pending 致重复发分。"""
    s = g.current_staff
    cur_period = datetime.datetime.now().strftime("%Y-%m")
    existing = db.session.query(StoreReport).filter(
        StoreReport.staff_id == s.id, StoreReport.period == cur_period
    ).with_for_update().first()
    if existing and existing.status in ("pending", "approved"):
        return jsonify({"error": "本月已提交，审核中或已通过，无需重复提交"}), 400
    display = request.files.get("display_img")
    stock = request.files.get("stock_img")
    sales = request.files.get("sales_img")
    if not display or not stock or not sales:
        return jsonify({"error": "请上传陈列照、库存截图、销量截图（各1张）"}), 400
    urls = []
    for f in (display, stock, sales):
        if not _allowed(f.filename):
            return jsonify({"error": "仅支持 png/jpg/jpeg/gif/webp 图片"}), 400
        ext = f.filename.rsplit(".", 1)[1].lower()
        fn = "store_report/%s.%s" % (uuid.uuid4().hex, ext)
        path = os.path.join(current_app.config["UPLOAD_DIR"], fn)
        os.makedirs(os.path.dirname(path), exist_ok=True)
        f.save(path)
        urls.append("/uploads/" + fn)
    if existing:
        # rejected 重交：覆盖原记录（staff+period 唯一约束只允许一条）
        existing.display_img = urls[0]
        existing.stock_img = urls[1]
        existing.sales_img = urls[2]
        existing.store_id = s.store_id if s.store_id else None
        existing.status = "pending"
        existing.reject_reason = None
        existing.reviewer_id = None
        existing.reviewed_at = None
        existing.points_earned = 0
        existing.created_at = datetime.datetime.now()
    else:
        existing = StoreReport(staff_id=s.id, store_id=(s.store_id if s.store_id else None),
                               display_img=urls[0], stock_img=urls[1], sales_img=urls[2],
                               period=cur_period, status="pending")
        db.session.add(existing)
    db.session.commit()
    return jsonify({"ok": True, "msg": "提交成功，审核通过后将发放 %d 积分" % get_setting_int("store_report_reward", 100)})


# ==================== 日常管理（合规照，H5）====================


@bp.route("/daily", methods=["GET"])
@token_required
def daily_list():
    """当前店员可见的日常管理 TAB + 各 TAB 提交记录（每条仅标题/时间/状态，不回显图；图仅供后台审核）。"""
    s = g.current_staff
    cur_period = datetime.datetime.now().strftime("%Y-%m")
    reward = get_setting_int("daily_reward", 100)
    tabs = []
    for t in DailyTab.query.order_by(DailyTab.sort.desc(), DailyTab.id).all():
        if not _tab_visible(t, s):
            continue
        items = DailyCompliance.query.filter_by(staff_id=s.id, tab_key=t.key).order_by(DailyCompliance.id.desc()).all()
        tabs.append({
            "key": t.key, "name": t.name,
            "door": t.door_label or "门头照", "office": t.office_label or "办公区照",
            "form_type": t.form_type or "normal",
            "can_submit": True,        # 不限频，可重复提交
            "period": cur_period,
            "items": [{
                "id": r.id, "status": r.status, "points": r.points_earned,
                "title": (r.ka_chain or r.cust_name or "提交"),
                "time": r.created_at.strftime("%Y-%m-%d %H:%M") if r.created_at else None,
                "review_time": r.reviewed_at.strftime("%Y-%m-%d %H:%M") if r.reviewed_at else None,
            } for r in items],
        })
    return jsonify({"reward": reward, "tabs": tabs})


@bp.route("/daily", methods=["POST"])
@token_required
def daily_upload():
    """上传日常管理合规照（某 TAB 的 2 张图 + 客户信息）。按 TAB.form_type 区分字段：
    ka = KA连锁/省/市/已陈列产品；normal = 客户名称/省/市/拜访内容。不限频。"""
    s = g.current_staff
    tab_key = (request.form.get("tab_key") or "").strip()
    tab = DailyTab.query.filter_by(key=tab_key).first()
    if not tab or not _tab_visible(tab, s):
        return jsonify({"error": "参数错误或无该栏目权限"}), 400
    cur_period = datetime.datetime.now().strftime("%Y-%m")
    cust_province = (request.form.get("cust_province") or "").strip()
    cust_city = (request.form.get("cust_city") or "").strip()
    if not cust_province or not cust_city:
        return jsonify({"error": "请选择省份、城市"}), 400
    cust_name = ka_chain = displayed_products = None
    visit_purpose = product_name = visit_content = None
    if tab.form_type == "ka":
        ka_chain = (request.form.get("ka_chain") or "").strip()
        cust_name = (request.form.get("cust_name") or "").strip()   # 门店名称（选填）
        if len(cust_name) > 6:
            return jsonify({"error": "门店名称请在 6 字以内"}), 400
        prods = []
        for x in request.form.getlist("displayed_products"):
            for part in str(x).split(","):
                part = part.strip()
                if part and part not in prods:
                    prods.append(part)
        displayed_products = ",".join(prods)
        if not ka_chain:
            return jsonify({"error": "请选择KA连锁"}), 400
        if not displayed_products:
            return jsonify({"error": "请勾选门店已陈列产品"}), 400
    else:
        cust_name = (request.form.get("cust_name") or "").strip()
        if not cust_name:
            return jsonify({"error": "请填写客户名称"}), 400
        if len(cust_name) > 6:
            return jsonify({"error": "客户名称请在 6 字以内"}), 400
        visit_purpose = (request.form.get("visit_purpose") or "").strip()
        product_name = (request.form.get("product_name") or "").strip()
        visit_content = (request.form.get("visit_content") or "").strip()
        if not visit_purpose or not product_name or not visit_content:
            return jsonify({"error": "请完整填写拜访内容（拜访目的/产品/沟通量）"}), 400
    door = request.files.get("door_img")
    office = request.files.get("office_img")
    if not door or not office:
        return jsonify({"error": "请上传 2 张照片"}), 400
    urls = []
    for f in (door, office):
        if not _allowed(f.filename):
            return jsonify({"error": "仅支持 png/jpg/jpeg/gif/webp 图片"}), 400
        ext = f.filename.rsplit(".", 1)[1].lower()
        fn = "daily/%s.%s" % (uuid.uuid4().hex, ext)
        path = os.path.join(current_app.config["UPLOAD_DIR"], fn)
        os.makedirs(os.path.dirname(path), exist_ok=True)
        f.save(path)
        urls.append("/uploads/" + fn)
    rec = DailyCompliance(staff_id=s.id, store_id=(s.store_id if s.store_id else None),
                          tab_key=tab_key, door_img=urls[0], office_img=urls[1],
                          cust_name=cust_name, cust_province=cust_province, cust_city=cust_city,
                          visit_purpose=visit_purpose, product_name=product_name, visit_content=visit_content,
                          ka_chain=ka_chain, displayed_products=displayed_products,
                          period=cur_period, status="pending")
    db.session.add(rec)
    db.session.commit()
    return jsonify({"ok": True, "msg": "提交成功，审核通过后将发放 %d 积分" % get_setting_int("daily_reward", 100)})


# ==================== 案例投稿（H5）====================

CASE_REWARD = 50  # 审核通过固定发放积分
CASE_DISEASES = ["泌尿感染", "肠道感染", "呼吸道感染", "其他"]


@bp.route("/case-submissions", methods=["GET"])
@token_required
def case_list():
    """当前店员的案例投稿记录（仅时间+疾病类型+状态，不回显完整内容；内容仅供后台审核）。"""
    s = g.current_staff
    items = CaseSubmission.query.filter_by(staff_id=s.id).order_by(CaseSubmission.id.desc()).all()
    cur_period = datetime.datetime.now().strftime("%Y-%m")
    limit = get_setting_int("case_monthly_limit", 100)
    used = sum(1 for c in items if c.created_at and c.created_at.strftime("%Y-%m") == cur_period)
    return jsonify({
        "reward": CASE_REWARD,
        "diseases": CASE_DISEASES,
        "monthly_limit": limit,
        "used_this_month": used,
        "can_submit": limit > 0 and used < limit,
        "cases": [{
            "id": c.id, "status": c.status, "points": c.points_earned,
            "disease": c.disease,
            "time": c.created_at.strftime("%Y-%m-%d %H:%M") if c.created_at else None,
            "review_time": c.reviewed_at.strftime("%Y-%m-%d %H:%M") if c.reviewed_at else None,
        } for c in items],
    })


@bp.route("/case-submissions", methods=["POST"])
@token_required
def case_create():
    """提交患教投稿。每人每月上限由后台 case_monthly_limit 控制(0=禁用)；提交即 pending，审核通过才发分。"""
    s = g.current_staff
    cur_period = datetime.datetime.now().strftime("%Y-%m")
    limit = get_setting_int("case_monthly_limit", 100)
    if limit <= 0:
        return jsonify({"error": "患教投稿暂未开放"}), 400
    used = sum(1 for c in CaseSubmission.query.filter_by(staff_id=s.id).all()
               if c.created_at and c.created_at.strftime("%Y-%m") == cur_period)
    if used >= limit:
        return jsonify({"error": "本月投稿已达上限（%d 次/月），下月1号重置" % limit}), 400
    data = request.get_json(silent=True) or {}
    disease = (data.get("disease") or "").strip()
    gender = (data.get("customer_gender") or "").strip()
    reason = (data.get("recommend_reason") or "").strip()
    process = (data.get("service_process") or "").strip()
    feedback = (data.get("customer_feedback") or "").strip()
    if disease not in CASE_DISEASES:
        return jsonify({"error": "请选择疾病类型"}), 400
    if not reason:
        return jsonify({"error": "请填写推荐原因"}), 400
    if len(reason) > 100:
        return jsonify({"error": "推荐原因不超过 100 字"}), 400
    if not process:
        return jsonify({"error": "请填写服务过程"}), 400
    if len(process) > 200:
        return jsonify({"error": "服务过程不超过 200 字"}), 400
    try:
        age = int(data.get("customer_age")) if data.get("customer_age") not in (None, "") else None
    except (ValueError, TypeError):
        age = None
    c = CaseSubmission(staff_id=s.id, store_id=(s.store_id if s.store_id else None),
                       disease=disease, customer_age=age, customer_gender=gender,
                       recommend_reason=reason, service_process=process,
                       customer_feedback=feedback or None, status="pending")
    db.session.add(c)
    db.session.commit()
    return jsonify({"ok": True, "msg": "投稿成功，审核通过后将发放 %d 积分" % CASE_REWARD})
