#!/usr/bin/env python3
"""
考勤排班导入脚本

[AI Agent 强制门禁] 本脚本执行前必须先阅读：
   dingtalk-workspace/references/products/attendance-schedule.md

   排班工作流、参数校验、班次校验、回显确认等约束全部在
   attendance-schedule.md，禁止凭本脚本源码或 --help 自行组装命令。

职责：
  1. 二次校验考勤组类型（必须为 TURN 排班制）
  2. 二次校验班次 ID 在可用班次列表中
  3. 回显排班内容表格，等待用户确认
  4. 调用 dws attendance schedule import 执行排班
  5. 输出执行结果摘要

用法:
    python attendance_schedule_import.py \
        --group-id 123456 \
        --schedules '[{"userId":"u001","workDate":"2026-05-19","classId":789,"isRest":"N"}]' \
        --confirm
"""

from __future__ import annotations

import argparse
import json
import sys
from datetime import datetime
from typing import Any

# 复用公共模块
from attendance_report_common import (
    run_dws,
    DwsCallError,
    extract_records,
    resolve_user_names,
    log,
    warn,
    error,
)

DATE_FMT = "%Y-%m-%d"
DATETIME_FMT = "%Y-%m-%d %H:%M:%S"


# ─────────────────────────────────────────────────────────────────────────────
# 考勤组校验
# ─────────────────────────────────────────────────────────────────────────────

def _unwrap_group_vo(result: dict) -> dict:
    """从 group get 返回结构中提取 groupVO（type/name/classIds 等字段所在层）。

    group get 返回结构：{groupVO: {type, name, classIds, ...}, ...}
    filtered-get 返回结构可能直接是扁平的 {type, name, memberUsers, ...}
    """
    if not isinstance(result, dict):
        return result
    group_vo = result.get("groupVO")
    if isinstance(group_vo, dict) and group_vo.get("type"):
        return group_vo
    # 如果顶层已经有 type 字段，说明是扁平结构，直接返回
    if result.get("type"):
        return result
    # 兜底：尝试从所有 dict 类型的值中找包含 type 字段的
    for value in result.values():
        if isinstance(value, dict) and value.get("type"):
            return value
    return result


def validate_group_is_turn(group_id: int) -> dict:
    """校验考勤组存在且类型为 TURN（排班制），返回考勤组信息（groupVO 层级）。"""
    log(f"🔍 校验考勤组 {group_id} ...")

    # 优先用 group get 获取完整信息（含绑定班次列表）
    try:
        result = run_dws([
            "attendance", "group", "get",
            "--group-id", str(group_id),
        ])
    except DwsCallError:
        # 降级使用 filtered-get
        try:
            result = run_dws([
                "attendance", "group", "filtered-get",
                "--group-id", str(group_id),
            ])
        except DwsCallError as exc:
            error(f"查询考勤组失败: {exc}")
            raise SystemExit(1) from exc

    if not result or not isinstance(result, dict):
        error(f"考勤组 {group_id} 不存在或返回数据异常")
        raise SystemExit(1)

    # 关键：从 groupVO 中提取 type/name 等字段
    group_vo = _unwrap_group_vo(result)
    group_type = group_vo.get("type", "")
    group_name = group_vo.get("name", f"ID:{group_id}")

    if not group_type:
        # 调试输出，帮助排查结构
        log(f"[debug] group get 返回顶层 keys: {list(result.keys())}")
        error(f"未能从考勤组 {group_id} 返回数据中识别出类型字段")
        raise SystemExit(1)

    if group_type != "TURN":
        type_label = {"FIXED": "固定班制", "NONE": "自由工时"}.get(group_type, group_type)
        error(f"考勤组「{group_name}」类型为 {type_label}，不是排班制（TURN），无法执行排班操作")
        raise SystemExit(1)

    log(f"✅ 考勤组「{group_name}」确认为排班制")
    return group_vo


# ─────────────────────────────────────────────────────────────────────────────
# 班次校验
# ─────────────────────────────────────────────────────────────────────────────

def extract_group_bound_classes(group_info: dict) -> set[int]:
    """从考勤组详情中提取绑定的班次 ID 集合。

    兼容多种字段结构：
      - classIds: [int]                    — 班次 ID 数组
      - classes / selectedClass: [dict]    — 班次对象数组 (含 id/classId)
      - shiftVOList: [dict]                — 排班制特有，含 shiftSetting.shiftId
      - classNameIdMap: {name: id}         — 名称到 ID 映射
    """

    def _extract_from_obj(obj: dict) -> set[int]:
        """从单个 dict 层级中提取班次 ID。"""
        ids: set[int] = set()

        # 方式1: classIds / shiftIds 数组（最常见）
        for key in ("classIds", "shiftIds", "classIdList"):
            ids_list = obj.get(key)
            if isinstance(ids_list, list):
                for item in ids_list:
                    try:
                        ids.add(int(item))
                    except (ValueError, TypeError):
                        pass

        # 方式2: classes / selectedClass 对象数组
        for key in ("classes", "selectedClass"):
            classes = obj.get(key)
            if isinstance(classes, list):
                for item in classes:
                    if isinstance(item, dict):
                        class_id = item.get("id") or item.get("classId")
                        if class_id is not None:
                            ids.add(int(class_id))
                    elif isinstance(item, (int, str)):
                        try:
                            ids.add(int(item))
                        except (ValueError, TypeError):
                            pass

        # 方式3: shiftVOList — 排班制考勤组特有字段
        shift_vo_list = obj.get("shiftVOList")
        if isinstance(shift_vo_list, list):
            for shift_vo in shift_vo_list:
                if not isinstance(shift_vo, dict):
                    continue
                # shiftSetting.shiftId
                shift_setting = shift_vo.get("shiftSetting")
                if isinstance(shift_setting, dict):
                    shift_id = shift_setting.get("shiftId") or shift_setting.get("classId")
                    if shift_id is not None:
                        ids.add(int(shift_id))
                # 直接在 shiftVO 层级的 id/shiftId/classId
                for id_key in ("id", "shiftId", "classId"):
                    val = shift_vo.get(id_key)
                    if val is not None:
                        try:
                            ids.add(int(val))
                        except (ValueError, TypeError):
                            pass

        # 方式4: classNameIdMap {name: id}
        class_map = obj.get("classNameIdMap")
        if isinstance(class_map, dict):
            for _, class_id in class_map.items():
                try:
                    ids.add(int(class_id))
                except (ValueError, TypeError):
                    pass

        return ids

    # 优先从 groupVO 提取（group get 返回结构），兼容顶层扁平结构
    bound_ids: set[int] = set()

    group_vo = group_info.get("groupVO")
    if isinstance(group_vo, dict):
        bound_ids.update(_extract_from_obj(group_vo))

    # 同时从顶层提取（兼容 filtered-get 或已解包的结构）
    bound_ids.update(_extract_from_obj(group_info))

    return bound_ids


def fetch_all_classes() -> dict[int, str]:
    """获取全局所有班次，返回 {classId: className}，用于 ID→名称映射。"""
    log("🔍 获取班次名称映射 ...")
    all_classes: dict[int, str] = {}
    page_index = 1
    page_size = 200

    while True:
        try:
            result = run_dws([
                "attendance", "class", "search",
                "--page-index", str(page_index),
                "--page-size", str(page_size),
            ])
        except DwsCallError as exc:
            error(f"查询班次列表失败: {exc}")
            raise SystemExit(1) from exc

        records = extract_records(result) if result else []
        if not records:
            break

        for record in records:
            class_id = record.get("id") or record.get("classId")
            class_name = record.get("name") or record.get("className") or str(class_id)
            if class_id is not None:
                all_classes[int(class_id)] = class_name

        if len(records) < page_size:
            break
        page_index += 1

    log(f"✅ 获取到 {len(all_classes)} 个班次名称")
    return all_classes


def validate_class_ids(
    schedules: list[dict],
    group_bound_class_ids: set[int],
    all_classes: dict[int, str],
    group_name: str,
) -> None:
    """校验排班记录中的 classId 都在该考勤组绑定的班次中。

    如果考勤组未提取到绑定班次列表（可能是接口字段差异），
    则降级为全局班次校验并输出警告。
    """
    # 如果两个来源都无法获取到班次信息，跳过校验（排班导入接口本身有服务端校验）
    no_bound = len(group_bound_class_ids) == 0
    no_global = len(all_classes) == 0

    if no_bound and no_global:
        warn(f"无法获取考勤组绑定班次和全局班次列表，跳过班次校验（将依赖服务端校验）")
        return

    use_global_fallback = no_bound
    if use_global_fallback:
        warn(f"未能从考勤组「{group_name}」详情中提取绑定班次列表，降级为全局班次校验")
        check_set = set(all_classes.keys())
    else:
        check_set = group_bound_class_ids

    invalid_class_ids: set[int] = set()

    for schedule in schedules:
        is_rest = str(schedule.get("isRest", "N")).upper()
        if is_rest == "Y":
            continue
        class_id = int(schedule.get("classId", 0))
        if class_id != 0 and class_id not in check_set:
            invalid_class_ids.add(class_id)

    if invalid_class_ids:
        invalid_names = [all_classes.get(cid, f"ID:{cid}") for cid in sorted(invalid_class_ids)]
        if use_global_fallback:
            error(f"以下班次不在可用班次列表中: {', '.join(invalid_names)}")
        else:
            error(f"以下班次不属于考勤组「{group_name}」: {', '.join(invalid_names)}")
        log(f"「{group_name}」可用班次:")
        available_ids = check_set if not use_global_fallback else set(all_classes.keys())
        for cid in sorted(available_ids):
            cname = all_classes.get(cid, f"ID:{cid}")
            log(f"  - {cname} (ID: {cid})")
        raise SystemExit(1)


# ─────────────────────────────────────────────────────────────────────────────
# 日期格式标准化
# ─────────────────────────────────────────────────────────────────────────────

def normalize_work_date(work_date: Any) -> str:
    """将 workDate 统一转换为 yyyy-MM-dd HH:mm:ss 格式。"""
    if isinstance(work_date, (int, float)):
        timestamp = work_date / 1000 if work_date > 1e12 else work_date
        return datetime.fromtimestamp(timestamp).strftime(DATETIME_FMT)

    date_str = str(work_date).strip()

    for fmt in (DATETIME_FMT, DATE_FMT):
        try:
            parsed = datetime.strptime(date_str, fmt)
            return parsed.strftime(DATETIME_FMT)
        except ValueError:
            continue

    raise ValueError(f"无法解析日期格式: {work_date!r}，请使用 YYYY-MM-DD 格式")


# ─────────────────────────────────────────────────────────────────────────────
# 回显排班内容
# ─────────────────────────────────────────────────────────────────────────────

def print_schedule_preview(
    group_name: str,
    group_id: int,
    schedules: list[dict],
    available_classes: dict[int, str],
    user_names: dict[str, str],
) -> None:
    """向 stdout 打印排班预览表格供用户确认。"""
    print("\n📋 排班确认")
    print(f"\n考勤组: {group_name} (ID: {group_id})")

    dates = sorted({s.get("workDate", "")[:10] for s in schedules})
    if dates:
        print(f"排班日期: {dates[0]} ~ {dates[-1]}")

    print(f"\n{'员工姓名':<12} {'日期':<14} {'班次':<16} {'是否排休':<8}")
    print("-" * 54)

    for schedule in sorted(schedules, key=lambda s: (s.get("userId", ""), s.get("workDate", ""))):
        user_id = schedule.get("userId", "")
        user_name = user_names.get(user_id, user_id)
        work_date = str(schedule.get("workDate", ""))[:10]
        class_id = int(schedule.get("classId", 0))
        is_rest = str(schedule.get("isRest", "N")).upper()

        if is_rest == "Y":
            class_display = "休息"
            rest_display = "是"
        else:
            class_display = available_classes.get(class_id, f"未知班次(ID:{class_id})")
            rest_display = "否"

        print(f"{user_name:<12} {work_date:<14} {class_display:<16} {rest_display:<8}")

    print(f"\n共 {len(schedules)} 条排班记录")


# ─────────────────────────────────────────────────────────────────────────────
# 执行排班
# ─────────────────────────────────────────────────────────────────────────────

def execute_schedule_import(group_id: int, schedules: list[dict]) -> None:
    """调用 dws attendance schedule import 执行排班。"""
    log(f"🚀 正在执行排班导入 ({len(schedules)} 条记录) ...")

    schedules_json = json.dumps(schedules, ensure_ascii=False)

    try:
        result = run_dws([
            "attendance", "schedule", "import",
            "--groupId", str(group_id),
            "--scheduleVOS", schedules_json,
            "--yes",
        ])
    except DwsCallError as exc:
        error(f"排班导入失败: {exc}")
        if exc.is_permission_error:
            error("提示: 当前账号可能不是考勤管理员，请确认权限")
        raise SystemExit(1) from exc

    log("✅ 排班导入完成")
    return result


# ─────────────────────────────────────────────────────────────────────────────
# 主流程
# ─────────────────────────────────────────────────────────────────────────────

def main() -> None:
    parser = argparse.ArgumentParser(
        description="考勤排班导入（含校验、回显、执行）",
        epilog="执行前必须阅读 attendance-schedule.md",
    )
    parser.add_argument(
        "--group-id", required=True, type=int,
        help="考勤组 ID（必填，必须为排班制考勤组）",
    )
    parser.add_argument(
        "--schedules", required=True,
        help="排班记录 JSON 数组（必填），每条记录包含 userId/workDate/classId/isRest",
    )
    parser.add_argument(
        "--confirm", action="store_true",
        help="用户已确认排班内容（必填，表示用户已在 Agent 回显中确认）",
    )
    parser.add_argument(
        "--dry-run", action="store_true",
        help="仅校验和回显，不实际执行排班",
    )
    args = parser.parse_args()

    # ── 解析排班记录 JSON ──
    try:
        schedules: list[dict] = json.loads(args.schedules)
    except json.JSONDecodeError as exc:
        error(f"--schedules JSON 格式错误: {exc}")
        raise SystemExit(1) from exc

    if not isinstance(schedules, list) or len(schedules) == 0:
        error("--schedules 必须是非空 JSON 数组")
        raise SystemExit(1)

    # ── 校验必填字段 ──
    required_fields = ("userId", "workDate", "classId", "isRest")
    for idx, schedule in enumerate(schedules):
        for field_name in required_fields:
            if field_name not in schedule:
                error(f"schedule[{idx}] 缺少必填字段: {field_name}")
                raise SystemExit(1)

    # ── 标准化日期格式 ──
    for idx, schedule in enumerate(schedules):
        try:
            schedule["workDate"] = normalize_work_date(schedule["workDate"])
        except ValueError as exc:
            error(f"schedule[{idx}] 日期格式错误: {exc}")
            raise SystemExit(1) from exc

    # ── 阶段 1: 校验考勤组（必须为 TURN 排班制） ──
    group_info = validate_group_is_turn(args.group_id)
    group_name = group_info.get("name", f"ID:{args.group_id}")

    # ── 阶段 2: 解析员工姓名 ──
    user_ids = list({s["userId"] for s in schedules})
    user_names = resolve_user_names(user_ids)

    # ── 阶段 3: 校验班次（必须属于该考勤组） ──
    group_bound_class_ids = extract_group_bound_classes(group_info)
    all_classes = fetch_all_classes()
    if group_bound_class_ids:
        log(f"📋 考勤组「{group_name}」绑定了 {len(group_bound_class_ids)} 个班次:")
        for cid in sorted(group_bound_class_ids):
            cname = all_classes.get(cid, f"ID:{cid}")
            log(f"   - {cname} (ID: {cid})")
    validate_class_ids(schedules, group_bound_class_ids, all_classes, group_name)
    log("✅ 班次校验通过")

    # ── 阶段 4: 回显排班内容 ──
    print_schedule_preview(group_name, args.group_id, schedules, all_classes, user_names)

    if args.dry_run:
        print("\n[dry-run] 仅校验和回显，未实际执行排班")
        return

    if not args.confirm:
        print("\n⚠️ 未传入 --confirm 参数，排班未执行")
        print("请在 Agent 回显确认后，添加 --confirm 参数重新执行")
        return

    # ── 阶段 5: 执行排班 ──
    execute_schedule_import(args.group_id, schedules)

    # ── 阶段 6: 输出摘要 ──
    print(f"\n✅ 排班导入成功！")
    print(f"   考勤组: {group_name}")
    print(f"   排班人数: {len(user_ids)}")
    print(f"   排班记录: {len(schedules)} 条")
    dates = sorted({s.get('workDate', '')[:10] for s in schedules})
    if dates:
        print(f"   日期范围: {dates[0]} ~ {dates[-1]}")

    # 展示所有排班明细
    print(f"\n{'员工姓名':<12} {'日期':<14} {'班次':<16} {'是否排休':<8}")
    print("-" * 54)
    for schedule in sorted(schedules, key=lambda s: (s.get("userId", ""), s.get("workDate", ""))):
        uid = schedule.get("userId", "")
        uname = user_names.get(uid, uid)
        wdate = str(schedule.get("workDate", ""))[:10]
        cid = int(schedule.get("classId", 0))
        is_rest = str(schedule.get("isRest", "N")).upper()
        if is_rest == "Y":
            class_display = "休息"
            rest_display = "是"
        else:
            class_display = all_classes.get(cid, f"未知班次(ID:{cid})")
            rest_display = "否"
        print(f"{uname:<12} {wdate:<14} {class_display:<16} {rest_display:<8}")


if __name__ == "__main__":
    main()
