# -*- coding: utf-8 -*-
"""中台：连锁 / 门店 / 店员 管理 + Excel 批量导入。"""
import csv
import io
import os
import datetime
from flask import render_template, request, redirect, url_for, flash, Response, abort, current_app
from flask_login import login_required

from app.models import (Chain, Store, Staff, PointsLog, ExamRecord, ExamParticipant,
                        LikeSubmission, ExchangeOrder, StoreReport, CaseSubmission, Invoice,
                        Feedback, Checkin, ArticleRead, StaffSeen, DailyCompliance,
                        HomeFeatureParticipant, DownloadCategoryParticipant, DailyTabParticipant,
                        GameExclusion, GameReward, DownloadFile, OperationLog)
from app.extensions import db
from app.admin import bp
from app.admin.views import _admin_only
from app.admin.admins import _super_only
from app.services import log_action
from app.region_data import CHINA_REGION, STAFF_POSITIONS
from pypinyin import lazy_pinyin


def sort_chains_for_display(chains):
    """连锁展示排序：置顶在前（按置顶时间倒序，最后置顶的最前），未置顶按拼音首字母。
       后台连锁列表与 H5 /api/chains 共用，保证两端顺序一致。"""
    chains = list(chains)
    chains.sort(key=lambda c: "".join(lazy_pinyin(c.name)).lower())               # 末级：拼音正序
    chains.sort(key=lambda c: c.pinned_at or datetime.datetime.min, reverse=True)  # 次级：置顶时间倒序
    chains.sort(key=lambda c: 0 if c.pinned else 1)                               # 主级：置顶在前
    return chains


# ==================== 连锁 ====================

@bp.route("/chains")
@login_required
def chains():
    _admin_only()
    q = request.args.get("q", "").strip()
    query = Chain.query
    if q:
        query = query.filter(Chain.name.contains(q))
    chains = query.all()
    # 附带每家连锁的门店数
    for c in chains:
        c.store_count = c.stores.count()
    chains = sort_chains_for_display(chains)
    return render_template("admin/chains.html", chains=chains, q=q, active="stores")


@bp.route("/chains/new", methods=["POST"])
@login_required
def chain_new():
    _admin_only()
    name = request.form.get("name", "").strip()
    if not name:
        flash("请输入连锁名称", "danger")
    elif Chain.query.filter_by(name=name).first():
        flash("连锁名称已存在", "danger")
    else:
        db.session.add(Chain(name=name))
        db.session.commit()
        flash("连锁已添加", "success")
    return redirect(url_for("admin.chains"))


@bp.route("/chains/<int:id>/edit", methods=["GET", "POST"])
@login_required
def chain_edit(id):
    """连锁更名（仅超级管理员）。"""
    _super_only()
    c = Chain.query.get_or_404(id)
    if request.method == "POST":
        name = request.form.get("name", "").strip()
        if not name:
            flash("连锁名称必填", "danger")
        elif Chain.query.filter(Chain.name == name, Chain.id != id).first():
            flash("该连锁名称已存在", "danger")
        else:
            old = c.name
            c.name = name
            db.session.commit()
            log_action("chain_rename", target_type="chain", target_id=id,
                       summary="连锁更名：%s → %s" % (old, name))
            flash("已更名：%s → %s" % (old, name), "success")
            return redirect(url_for("admin.chains"))
    return render_template("admin/chain_form.html", c=c, active="stores")


@bp.route("/chains/<int:id>/delete", methods=["POST"])
@login_required
def chain_delete(id):
    """删除连锁（仅超级管理员）。
    连锁下有店员（=有积分/订单挂钩）→ 拒绝；仅空门店 → 一并删除连锁及其下门店。"""
    _super_only()
    c = Chain.query.get_or_404(id)
    staff_n = (Staff.query.join(Store, Staff.store_id == Store.id)
               .filter(Store.chain_id == id, Staff.deleted == False).count())
    if staff_n > 0:
        flash("该连锁下有 %d 名店员（含积分/订单记录），不能删除。请先转移或删除这些店员。" % staff_n, "danger")
        return redirect(url_for("admin.chains"))
    stores = c.stores.all()
    name = c.name
    for s in stores:
        db.session.delete(s)
    db.session.delete(c)
    log_action("delete_chain", target_type="chain", target_id=id,
               summary="删除连锁：%s（含 %d 家空门店）" % (name, len(stores)), commit=False)
    db.session.commit()
    flash("已删除连锁「%s」（含 %d 家门店）" % (name, len(stores)), "success")
    return redirect(url_for("admin.chains"))


@bp.route("/chains/<int:id>/pin", methods=["POST"])
@login_required
def chain_pin(id):
    """切换连锁置顶（仅超级管理员）。置顶=在 H5 下拉与后台列表里排到最前；再次点击取消。
       多个置顶连锁按置顶时间倒序（最后置顶的排最前）。"""
    _super_only()
    c = Chain.query.get_or_404(id)
    c.pinned = not c.pinned
    c.pinned_at = datetime.datetime.now() if c.pinned else None
    db.session.commit()
    log_action("chain_pin" if c.pinned else "chain_unpin", target_type="chain", target_id=id,
               summary="%s置顶连锁：%s" % ("" if c.pinned else "取消", c.name))
    flash(("已置顶「%s」（排在最前）" if c.pinned else "已取消置顶「%s」") % c.name, "success")
    return redirect(url_for("admin.chains"))


# ==================== 门店 ====================

@bp.route("/stores")
@login_required
def stores():
    _admin_only()
    chain_id = request.args.get("chain_id", type=int)
    province = request.args.get("province", "").strip()
    city = request.args.get("city", "").strip()
    q = request.args.get("q", "").strip()
    query = Store.query
    if chain_id:
        query = query.filter_by(chain_id=chain_id)
    if province:
        query = query.filter(Store.province == province)
    if city:
        query = query.filter(Store.city == city)
    if q:
        query = query.filter(Store.name.contains(q))
    page = request.args.get("page", 1, type=int)
    pag = query.order_by(Store.id.desc()).paginate(page=page, per_page=60, error_out=False)
    stores = pag.items
    for s in stores:
        s.staff_count = s.staff.filter(Staff.deleted.is_(False)).count()
    chains = Chain.query.order_by(Chain.name).all()
    from urllib.parse import urlencode
    base_qs = urlencode({k: v for k, v in request.args.items() if k != "page"})
    return render_template("admin/store_list.html", stores=stores, chains=chains,
                           chain_id=chain_id, province=province, city=city, q=q,
                           page=page, pages=pag.pages, total=pag.total, per_page=60,
                           base_qs=base_qs, china_region=CHINA_REGION, active="stores")


@bp.route("/stores/new", methods=["GET", "POST"])
@bp.route("/stores/<int:id>/edit", methods=["GET", "POST"])
@login_required
def store_edit(id=None):
    _admin_only()
    store = Store.query.get_or_404(id) if id else Store()
    if request.method == "POST":
        store.name = request.form.get("name", "").strip()
        store.chain_id = request.form.get("chain_id", type=int)
        store.region = request.form.get("region", "").strip()
        store.province = request.form.get("province", "").strip()
        store.city = request.form.get("city", "").strip()
        store.address = request.form.get("address", "").strip()
        store.code = request.form.get("code", "").strip()
        store.status = request.form.get("status", "active")
        if not store.name:
            flash("门店名称必填", "danger")
        else:
            if id is None:
                db.session.add(store)
            db.session.commit()
            flash("已保存", "success")
            return redirect(url_for("admin.stores"))
    chains = Chain.query.order_by(Chain.name).all()
    return render_template("admin/store_form.html", store=store, chains=chains,
                           china_region=CHINA_REGION,
                           active="stores", is_new=(id is None))


@bp.route("/stores/<int:id>/delete", methods=["POST"])
@login_required
def store_delete(id):
    _admin_only()
    s = Store.query.get_or_404(id)
    if s.staff.count() > 0:
        flash("该门店下有店员，不能删除（请先转移或清空店员）", "danger")
    else:
        name = s.name
        db.session.delete(s)
        log_action("delete_store", target_type="store", target_id=id,
                   summary="删除门店：%s" % name, commit=False)
        db.session.commit()
        flash("已删除", "success")
    return redirect(url_for("admin.stores"))


# ==================== 店员 ====================

def _default_pwd(phone):
    return phone[-6:] if phone and len(phone) >= 6 else "123456"


@bp.route("/staff")
@login_required
def staff_list():
    _admin_only()
    chain_id = request.args.get("chain_id", type=int)
    province = request.args.get("province", "").strip()
    city = request.args.get("city", "").strip()
    q = request.args.get("q", "").strip()
    status_filter = request.args.get("status", "active").strip()
    query = Staff.query.filter_by(role="staff", deleted=False)
    if status_filter == "disabled":
        query = query.filter(Staff.status == "disabled")
    else:
        query = query.filter(Staff.status != "disabled")  # active + 历史 NULL 都算在岗
        status_filter = "active"
    if chain_id:
        query = query.join(Store, Staff.store_id == Store.id).filter(Store.chain_id == chain_id)
    if province:
        query = query.filter(Staff.province == province)
    if city:
        query = query.filter(Staff.city == city)
    if q:
        query = query.filter(Staff.name.contains(q) | Staff.phone.contains(q))
    page = request.args.get("page", 1, type=int)
    pag = query.order_by(Staff.id.desc()).paginate(page=page, per_page=60, error_out=False)
    staff = pag.items
    chains = Chain.query.order_by(Chain.name).all()
    from urllib.parse import urlencode
    others = {k: v for k, v in request.args.items() if k not in ("page", "status")}
    pag_qs = urlencode({**others, **({"status": "disabled"} if status_filter == "disabled" else {})})
    active_href = urlencode(others)
    disabled_href = urlencode({**others, "status": "disabled"})
    return render_template("admin/staff_list.html", staff=staff, chains=chains,
                           chain_id=chain_id, province=province, city=city, q=q,
                           status_filter=status_filter,
                           page=page, pages=pag.pages, total=pag.total, per_page=60,
                           pag_qs=pag_qs, active_href=active_href, disabled_href=disabled_href,
                           china_region=CHINA_REGION, active="staff")


@bp.route("/staff/new", methods=["GET", "POST"])
@bp.route("/staff/<int:id>/edit", methods=["GET", "POST"])
@login_required
def staff_edit(id=None):
    _admin_only()
    s = Staff.query.get_or_404(id) if id else Staff(role="staff", status="active")
    if request.method == "POST":
        s.name = request.form.get("name", "").strip()
        s.phone = request.form.get("phone", "").strip()
        chain_id = request.form.get("chain_id", type=int)
        store_name = request.form.get("store_name", "").strip()
        s.province = request.form.get("province", "").strip()
        s.city = request.form.get("city", "").strip()
        s.position = request.form.get("position", "").strip()
        s.status = request.form.get("status", "active")
        pwd = request.form.get("password", "").strip()
        if not s.name or not s.phone:
            flash("姓名和手机号必填", "danger")
        elif not chain_id or not store_name:
            flash("请选择连锁并填写门店名称", "danger")
        elif Staff.query.filter(Staff.phone == s.phone, Staff.role == "staff",
                                Staff.id != (id or 0)).first():
            flash("该手机号已被其他店员占用", "danger")
        else:
            # 门店按 名称+连锁 匹配，不存在则新建（与店员注册一致）
            store = Store.query.filter_by(name=store_name, chain_id=chain_id).first()
            if not store:
                store = Store(name=store_name, chain_id=chain_id)
                db.session.add(store)
                db.session.flush()
            s.store_id = store.id
            if pwd:
                s.set_password(pwd)
            if not s.password_hash:  # 新建未填密码，默认手机号后6位
                s.set_password(_default_pwd(s.phone))
            if id is None:
                db.session.add(s)
            db.session.commit()
            log_action("staff_create" if id is None else "staff_edit", target_type="staff",
                       target_id=s.id, summary="店员：%s %s" % (s.name, s.phone))
            flash("已保存", "success")
            return redirect(url_for("admin.staff_list"))
    chains = Chain.query.order_by(Chain.name).all()
    return render_template("admin/staff_form.html", s=s, chains=chains,
                           china_region=CHINA_REGION, positions=STAFF_POSITIONS,
                           active="staff", is_new=(id is None))


@bp.route("/staff/<int:id>/toggle", methods=["POST"])
@login_required
def staff_toggle(id):
    _admin_only()
    s = Staff.query.get_or_404(id)
    s.status = "disabled" if s.status == "active" else "active"
    log_action("staff_toggle", target_type="staff", target_id=id,
               summary="%s %s" % (s.name, "启用" if s.status == "active" else "停用"), commit=False)
    db.session.commit()
    flash("已 %s" % ("启用" if s.status == "active" else "停用"), "success")
    return redirect(url_for("admin.staff_list"))


@bp.route("/staff/<int:id>/reset", methods=["POST"])
@login_required
def staff_reset(id):
    _admin_only()
    s = Staff.query.get_or_404(id)
    pwd = request.form.get("password", "").strip() or _default_pwd(s.phone)
    s.set_password(pwd)
    log_action("staff_reset_pwd", target_type="staff", target_id=id,
               summary="重置 %s 的密码" % s.name, commit=False)   # 不记录密码明文
    db.session.commit()
    flash("密码已重置为：%s" % pwd, "success")
    return redirect(url_for("admin.staff_list"))


@bp.route("/staff/<int:id>")
@login_required
def staff_detail(id):
    """店员详情：基本信息 + 最后登录 + 操作时间线（积分流水）。"""
    _admin_only()
    s = Staff.query.get_or_404(id)
    logs = (PointsLog.query.filter_by(staff_id=id)
            .order_by(PointsLog.created_at.desc()).limit(200).all())
    return render_template("admin/staff_detail.html", s=s, logs=logs, active="staff")


@bp.route("/staff/<int:id>/delete", methods=["POST"])
@login_required
def staff_delete(id):
    """软删除店员（仅超级管理员）。置 deleted=True，从列表隐藏，数据全保留可恢复。"""
    _super_only()
    s = Staff.query.get_or_404(id)
    if s.role != "staff":
        abort(403)
    s.deleted = True
    log_action("staff_delete", target_type="staff", target_id=id,
               summary="软删除店员：%s %s" % (s.name, s.phone), commit=False)
    db.session.commit()
    flash("已删除店员 %s（数据已保留，可恢复）" % s.name, "success")
    return redirect(url_for("admin.staff_list"))


@bp.route("/staff/delete-batch", methods=["POST"])
@login_required
def staff_delete_batch():
    """批量软删除店员（仅超级管理员）。"""
    _super_only()
    ids = [int(i) for i in request.form.getlist("ids") if (i or "").isdigit()]
    if not ids:
        flash("未选择任何店员", "danger")
        return redirect(url_for("admin.staff_list"))
    updated = (Staff.query.filter(Staff.id.in_(ids), Staff.role == "staff")
               .update({Staff.deleted: True}, synchronize_session=False))
    log_action("staff_delete_batch", target_type="staff", target_id=0,
               summary="批量软删除店员 %d 人（IDs: %s）" % (updated, ",".join(map(str, ids))[:200]),
               commit=False)
    db.session.commit()
    flash("已批量删除 %d 名店员（数据已保留）" % updated, "success")
    return redirect(url_for("admin.staff_list"))


@bp.route("/staff/trash")
@login_required
def staff_trash():
    """已删除店员列表（仅超级管理员）。"""
    _super_only()
    q = request.args.get("q", "").strip()
    query = Staff.query.filter_by(role="staff", deleted=True)
    if q:
        query = query.filter(Staff.name.contains(q) | Staff.phone.contains(q))
    staff = query.order_by(Staff.id.desc()).all()
    return render_template("admin/staff_trash.html", staff=staff, q=q, active="staff")


@bp.route("/staff/<int:id>/restore", methods=["POST"])
@login_required
def staff_restore(id):
    """恢复已删除店员（仅超级管理员）。deleted=False 后关联记录自动恢复显示。"""
    _super_only()
    s = Staff.query.get_or_404(id)
    s.deleted = False
    log_action("staff_restore", target_type="staff", target_id=id,
               summary="恢复店员：%s %s" % (s.name, s.phone), commit=False)
    db.session.commit()
    flash("已恢复店员 %s（关联记录已同步恢复显示）" % s.name, "success")
    return redirect(url_for("admin.staff_trash"))


@bp.route("/staff/restore-batch", methods=["POST"])
@login_required
def staff_restore_batch():
    """批量恢复已删除店员（仅超级管理员）。"""
    _super_only()
    ids = [int(i) for i in request.form.getlist("ids") if (i or "").isdigit()]
    if not ids:
        flash("未选择任何店员", "danger")
        return redirect(url_for("admin.staff_trash"))
    updated = (Staff.query.filter(Staff.id.in_(ids), Staff.role == "staff")
               .update({Staff.deleted: False}, synchronize_session=False))
    log_action("staff_restore_batch", target_type="staff", target_id=0,
               summary="批量恢复店员 %d 人（IDs: %s）" % (updated, ",".join(map(str, ids))[:200]),
               commit=False)
    db.session.commit()
    flash("已批量恢复 %d 名店员" % updated, "success")
    return redirect(url_for("admin.staff_trash"))


# 彻底删除时按 staff_id 级联清除的业务数据表 + participant 分配表（label, Model）
_STAFF_CASCADE = [
    ("积分流水", PointsLog), ("考试记录", ExamRecord), ("考试参与", ExamParticipant),
    ("集赞提交", LikeSubmission), ("兑换订单", ExchangeOrder), ("门店运营", StoreReport),
    ("患教投稿", CaseSubmission), ("游学发票", Invoice), ("意见反馈", Feedback),
    ("每日打卡", Checkin), ("资讯阅读", ArticleRead), ("日常管理", DailyCompliance),
    ("已读/红点状态", StaffSeen), ("首页入口分配", HomeFeatureParticipant),
    ("下载分类分配", DownloadCategoryParticipant), ("日常管理TAB分配", DailyTabParticipant),
    ("游戏屏蔽", GameExclusion), ("游戏发分", GameReward),
]


@bp.route("/staff/<int:id>/purge", methods=["GET"])
@login_required
def staff_purge(id):
    """彻底删除预览页：列出该店员所有关联数据条数，提示将整体清除（仅超级管理员，仅 deleted=True）。"""
    _super_only()
    s = Staff.query.get_or_404(id)
    if s.role != "staff":
        abort(403)
    if not s.deleted:
        flash("仅可彻底删除已软删除的店员，请先在店员列表删除", "danger")
        return redirect(url_for("admin.staff_list"))
    counts = [{"label": label, "n": Model.query.filter_by(staff_id=id).count()}
              for label, Model in _STAFF_CASCADE]
    total = sum(c["n"] for c in counts)
    return render_template("admin/staff_purge.html", s=s, counts=counts, total=total, active="staff")


@bp.route("/staff/<int:id>/purge", methods=["POST"])
@login_required
def staff_purge_exec(id):
    """执行彻底删除：NULL 该店员作为审核人/操作人/上传人的外键引用（保留这些行），
    物理删除其全部业务数据 + participant 分配 + 头像文件 + 账号本身；保留 OperationLog 审计。"""
    _super_only()
    s = Staff.query.get_or_404(id)
    if s.role != "staff":
        abort(403)
    if not s.deleted:
        flash("仅可彻底删除已软删除的店员", "danger")
        return redirect(url_for("admin.staff_list"))
    if request.form.get("confirm") != "1":
        flash("未勾选确认，未删除", "danger")
        return redirect(url_for("admin.staff_purge", id=id))
    name, phone = s.name, s.phone
    try:
        # 1) NULL 外键引用（保留这些记录，仅去掉指向该店员的外键；OperationLog 审计保留）
        for Model, fk in [(OperationLog, OperationLog.admin_id),
                          (LikeSubmission, LikeSubmission.reviewer_id),
                          (StoreReport, StoreReport.reviewer_id),
                          (CaseSubmission, CaseSubmission.reviewer_id),
                          (DailyCompliance, DailyCompliance.reviewer_id),
                          (DownloadFile, DownloadFile.uploader_id)]:
            db.session.query(Model).filter(fk == id).update({fk: None}, synchronize_session=False)
        # 2) 物理删除业务数据 + participant 分配
        deleted_rows = 0
        for label, Model in _STAFF_CASCADE:
            deleted_rows += Model.query.filter_by(staff_id=id).delete(synchronize_session=False)
        # 3) 删头像文件（仅 /uploads/avatar/ 下）
        avatar = s.avatar or ""
        if avatar.startswith("/uploads/avatar/"):
            path = os.path.join(current_app.config["UPLOAD_DIR"], avatar[len("/uploads/"):])
            if os.path.isfile(path):
                try:
                    os.remove(path)
                except OSError:
                    pass
        # 4) 删账号 + 记日志（一次性 commit）
        db.session.delete(s)
        log_action("staff_purge", target_type="staff", target_id=id,
                   summary="彻底删除店员：%s %s（级联清理 %d 行业务数据，操作日志保留）" % (
                       name, phone, deleted_rows), commit=False)
        db.session.commit()
    except Exception as e:
        db.session.rollback()
        flash("删除失败，已回滚：%s" % e, "danger")
        return redirect(url_for("admin.staff_purge", id=id))
    flash("已彻底删除 %s（关联数据已全部清除，操作日志保留）" % name, "success")
    return redirect(url_for("admin.staff_trash"))


@bp.route("/staff/import", methods=["GET", "POST"])
@login_required
def staff_import():
    _admin_only()
    if request.method == "POST":
        f = request.files.get("file")
        chain_id = request.form.get("chain_id", type=int)
        if not f or not chain_id:
            flash("请选择连锁并上传 Excel 文件", "danger")
            return redirect(url_for("admin.staff_import"))
        if not (f.filename or "").lower().endswith((".xlsx", ".xls")):
            flash("仅支持 .xlsx / .xls 文件", "danger")
            return redirect(url_for("admin.staff_import"))
        chain = Chain.query.get(chain_id)
        if not chain:
            flash("所选连锁不存在", "danger")
            return redirect(url_for("admin.staff_import"))
        from openpyxl import load_workbook
        wb = load_workbook(f, read_only=True, data_only=True)
        ws = wb.active
        rows = list(ws.iter_rows(values_only=True))
        added = skipped_dup = skipped_nostore = skipped_empty = new_stores = 0
        store_cache = {}  # (chain_id, store_name) -> Store，避免同名门店重复建/重复查
        for row in rows[1:]:  # 跳过表头，列：姓名 | 手机号 | 门店名称
            name = str(row[0]).strip() if len(row) > 0 and row[0] else ""
            phone = str(row[1]).strip() if len(row) > 1 and row[1] else ""
            store_name = str(row[2]).strip() if len(row) > 2 and row[2] else ""
            if not name or not phone:
                skipped_empty += 1
                continue
            if Staff.query.filter_by(phone=phone, role="staff").first():
                skipped_dup += 1
                continue
            if not store_name:
                skipped_nostore += 1
                continue
            key = (chain_id, store_name)
            store = store_cache.get(key)
            if not store:
                store = Store.query.filter_by(name=store_name, chain_id=chain_id).first()
                if not store:
                    store = Store(name=store_name, chain_id=chain_id)
                    db.session.add(store)
                    db.session.flush()
                    new_stores += 1
                store_cache[key] = store
            s = Staff(name=name, phone=phone, store_id=store.id,
                      role="staff", status="active")
            s.set_password(_default_pwd(phone))
            db.session.add(s)
            added += 1
        db.session.commit()
        flash("导入完成：新增 %d 人，跳过 %d 人；新建门店 %d 家。"
              "（跳过原因：手机号已存在 %d / 缺门店名 %d / 空行 %d）"
              % (added, skipped_dup + skipped_nostore + skipped_empty,
                 new_stores, skipped_dup, skipped_nostore, skipped_empty), "success")
        return redirect(url_for("admin.staff_list"))
    chains = sort_chains_for_display(Chain.query.all())
    return render_template("admin/staff_import.html", chains=chains, active="staff")


@bp.route("/staff/export")
@login_required
def staff_export():
    _admin_only()
    staff = Staff.query.filter_by(role="staff", deleted=False).all()
    out = io.StringIO()
    out.write("﻿")  # UTF-8 BOM，保证 Excel 中文不乱码
    w = csv.writer(out)
    w.writerow(["ID", "姓名", "手机号", "门店", "连锁", "积分余额", "状态"])
    for s in staff:
        w.writerow([s.id, s.name, s.phone,
                    s.store.name if s.store else "",
                    s.store.chain.name if (s.store and s.store.chain) else "",
                    s.points_balance, s.status])
    return Response(out.getvalue(), mimetype="text/csv",
                    headers={"Content-Disposition": "attachment; filename=staff_%s.csv" % datetime.date.today()})
