#!/usr/bin/env python3
"""
手机号 → 账号ID → 课时完成记录查询脚本

用法:
    python3 scripts/phone_chapter_query.py <input_file> [--output <output_path>]

参数:
    input_file    包含手机号的 Excel(.xlsx) 或 CSV(.csv) 文件路径
    --output      输出 xlsx 文件路径（可选，默认 output/phone_chapter_query_时间戳.xlsx）

流程:
    1. 从文件中提取所有11位手机号（自动识别列）
    2. 脱敏后匹配 bi_vala_app_account（status=1 且未删除）获取 account_id
    3. 用 account_id 关联角色表，查询指定课时的首次完成记录（含耗时）
    4. 输出带手机号明文列的 xlsx 文件，未匹配手机号追加在末尾
"""

import argparse
import csv
import io
import json
import os
import re
import subprocess
import sys
import urllib.request
from datetime import datetime

try:
    import openpyxl
    from openpyxl.styles import Font, Alignment, PatternFill
except ImportError:
    print("ERROR: 需要 openpyxl 库，请运行: pip3 install openpyxl")
    sys.exit(1)


# ─── 配置 ───────────────────────────────────────────────
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# 工作区根目录：scripts/ -> phone-chapter-query/ -> skills/ -> workspace/
WORKSPACE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(SCRIPT_DIR)))
SECRETS_FILE = os.path.join(WORKSPACE_DIR, "secrets.env")
OUTPUT_DIR = os.path.join(WORKSPACE_DIR, "output")

DB_HOST = "bj-postgres-16pob4sg.sql.tencentcdb.com"
DB_PORT = "28591"
DB_USER = "ai_member"
DB_NAME = "vala_bi"

# 要查询的课时 ID，按需修改
CHAPTER_IDS = "55,56,57,58,59,343,344,345,346,348"
# 分表数量（0-7）
SHARD_COUNT = 8

# 飞书发送配置
LARK_CREDENTIALS = "/root/.openclaw/credentials/xiaoxi/config.json"
LARK_TARGET_CHAT_ID = "oc_616882799f1910b85a248b74f44c3be5"


def load_pg_password():
    """从 secrets.env 读取 PostgreSQL 密码"""
    with open(SECRETS_FILE, "r") as f:
        for line in f:
            if line.startswith("PG_ONLINE_PASSWORD="):
                return line.split("=", 1)[1].strip().strip("'\"")
    raise RuntimeError("secrets.env 中未找到 PG_ONLINE_PASSWORD")


def extract_phones(file_path):
    """从 xlsx 或 csv 文件中提取所有11位手机号"""
    phones = []
    ext = os.path.splitext(file_path)[1].lower()

    if ext in (".xlsx", ".xls"):
        wb = openpyxl.load_workbook(file_path, read_only=True)
        for ws in wb:
            for row in ws.iter_rows(values_only=True):
                for cell in row:
                    if cell is not None:
                        val = str(int(cell)) if isinstance(cell, float) else str(cell)
                        val = val.strip()
                        if re.match(r"^1\d{10}$", val):
                            phones.append(val)
    elif ext == ".csv":
        with open(file_path, "r", encoding="utf-8-sig") as f:
            reader = csv.reader(f)
            for row in reader:
                for cell in row:
                    val = cell.strip()
                    if re.match(r"^1\d{10}$", val):
                        phones.append(val)
    else:
        raise ValueError(f"不支持的文件格式: {ext}，仅支持 .xlsx 和 .csv")

    # 去重并保持顺序
    seen = set()
    unique = []
    for p in phones:
        if p not in seen:
            seen.add(p)
            unique.append(p)
    return unique


def mask_phone(phone):
    """手机号脱敏：前3 + **** + 后4"""
    return f"{phone[:3]}****{phone[-4:]}"


def run_sql(sql, pg_password):
    """执行 SQL 并返回 CSV 格式的结果"""
    env = os.environ.copy()
    env["PGPASSWORD"] = pg_password
    result = subprocess.run(
        ["psql", "-h", DB_HOST, "-p", DB_PORT, "-U", DB_USER, "-d", DB_NAME, "--csv", "-c", sql],
        capture_output=True, text=True, env=env, timeout=300,
    )
    if result.returncode != 0:
        raise RuntimeError(f"SQL 执行失败:\n{result.stderr}")
    return result.stdout


def step1_match_accounts(phones, pg_password):
    """步骤1: 手机号脱敏后匹配 account_id，返回匹配结果和映射表"""
    # masked -> [original1, original2, ...] （不同明文可能脱敏后相同）
    masked_to_originals = {}
    for p in phones:
        m = mask_phone(p)
        masked_to_originals.setdefault(m, []).append(p)

    conditions = ", ".join(f"'{m}'" for m in masked_to_originals)
    sql = f"""
    SELECT id AS account_id, tel AS masked_phone
    FROM bi_vala_app_account
    WHERE tel IN ({conditions})
      AND status = 1
      AND deleted_at IS NULL
    ORDER BY id;
    """
    output = run_sql(sql, pg_password)

    # 解析结果，构建 account_id -> 明文手机号 映射
    account_to_phone = {}  # account_id -> original_phone
    matched_masks = set()
    reader = csv.DictReader(io.StringIO(output))
    for row in reader:
        aid = row["account_id"]
        masked = row["masked_phone"]
        matched_masks.add(masked)
        # 取该脱敏格式对应的第一个明文
        if masked in masked_to_originals:
            account_to_phone[aid] = masked_to_originals[masked][0]

    account_ids = list(account_to_phone.keys())

    # 未匹配的手机号（明文）
    unmatched = []
    for m, originals in masked_to_originals.items():
        if m not in matched_masks:
            unmatched.extend(originals)

    return account_ids, account_to_phone, unmatched


def build_union_sql(table_prefix, columns, where_clause, shard_count):
    """生成分表 UNION ALL SQL"""
    parts = []
    for i in range(shard_count):
        parts.append(f"SELECT {columns} FROM {table_prefix}_{i} {where_clause}")
    return " UNION ALL ".join(parts)


def step2_query_chapter_play(account_ids, pg_password):
    """步骤2: 用 account_id 查询课时完成记录"""
    if not account_ids:
        return ""

    aid_list = ", ".join(account_ids)

    chapter_union = build_union_sql(
        "bi_user_chapter_play_record",
        "user_id, chapter_id, chapter_unique_id, date(updated_at) AS finish_date",
        f"WHERE chapter_id IN ({CHAPTER_IDS}) AND play_status = 1",
        SHARD_COUNT,
    )

    # 为每个分表生成带过滤的 component_play_record 查询
    comp_parts = []
    for i in range(SHARD_COUNT):
        comp_parts.append(
            f"SELECT chapter_unique_id, SUM(interval_time) AS total_interval "
            f"FROM bi_user_component_play_record_{i} "
            f"WHERE chapter_unique_id IN (SELECT chapter_unique_id FROM cp_unique_ids) "
            f"GROUP BY chapter_unique_id"
        )
    comp_filtered_union = " UNION ALL ".join(comp_parts)

    sql = f"""
WITH chapter_play AS (
    {chapter_union}
),
filtered_play AS (
    SELECT cp.user_id, cp.chapter_id, cp.chapter_unique_id, cp.finish_date,
        ROW_NUMBER() OVER (PARTITION BY cp.user_id, cp.chapter_id ORDER BY cp.finish_date) AS rn
    FROM chapter_play cp
    JOIN bi_vala_app_character c ON cp.user_id = c.id
    JOIN bi_vala_app_account a ON c.account_id = a.id
    WHERE a.id IN ({aid_list})
),
cp_unique_ids AS (
    SELECT DISTINCT chapter_unique_id FROM filtered_play WHERE rn = 1
),
comp_time AS (
    SELECT chapter_unique_id, SUM(total_interval) AS total_interval
    FROM ({comp_filtered_union}) t
    GROUP BY chapter_unique_id
),
course_detail AS (
    SELECT
        fp.user_id,
        fp.chapter_id,
        FORMAT('%s-%s-%s-%s', l.course_level, l.course_season, l.course_unit, l.course_lesson) AS course_id,
        fp.finish_date,
        FORMAT('%s:%s',
            FLOOR(ct.total_interval / 1000 / 60),
            LPAD(CAST(MOD(ct.total_interval / 1000, 60) AS TEXT), 2, '0')
        ) AS finish_time
    FROM filtered_play fp
    LEFT JOIN bi_level_unit_lesson l ON fp.chapter_id = l.id
    LEFT JOIN comp_time ct ON fp.chapter_unique_id = ct.chapter_unique_id
    WHERE fp.rn = 1
)

SELECT
    a.account_id              AS "账号ID",
    CAST(a.account_id AS TEXT) AS "account_id_str",
    a.id                      AS "角色ID",
    DATE(a.created_at)        AS "注册日期",
    DATE(TO_TIMESTAMP(b.latest_active_time)) AS "最近登录时间",
    c.course_id               AS "课程名称",
    c.finish_date             AS "课程完成时间",
    c.finish_time             AS "课程耗时"
FROM bi_vala_app_character a
JOIN bi_vala_app_account acct ON a.account_id = acct.id
LEFT JOIN user_detail_info b ON a.id = b.user_id
LEFT JOIN course_detail c ON a.id = c.user_id
WHERE a.status = 1
  AND acct.id IN ({aid_list})
ORDER BY a.id, c.course_id;
"""
    return run_sql(sql, pg_password)


def write_xlsx(csv_data, account_to_phone, unmatched_phones, output_path):
    """将查询结果写入 xlsx，用明文手机号替换脱敏手机号"""
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "课时完成记录"

    # 表头
    ws.append(["账号ID", "手机号", "角色ID", "注册日期", "最近登录时间", "课程名称", "课程完成时间", "课程耗时"])

    if csv_data:
        reader = csv.DictReader(io.StringIO(csv_data))
        for row in reader:
            aid = row["账号ID"]
            phone = account_to_phone.get(aid, "")
            ws.append([
                aid,
                phone,
                row["角色ID"],
                row["注册日期"],
                row["最近登录时间"],
                row["课程名称"],
                row["课程完成时间"],
                row["课程耗时"],
            ])

    # 追加未匹配手机号（明文）
    for p in sorted(unmatched_phones):
        ws.append(["", p, "", "", "", "", "", ""])

    # 表头样式
    hf = Font(bold=True, color="FFFFFF", size=11)
    hfill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
    ha = Alignment(horizontal="center", vertical="center")
    for cell in ws[1]:
        cell.font = hf
        cell.fill = hfill
        cell.alignment = ha

    # 自动列宽
    for col in ws.columns:
        max_w = 0
        cl = col[0].column_letter
        for cell in col:
            v = str(cell.value) if cell.value else ""
            w = sum(2 if ord(c) > 127 else 1 for c in v)
            if w > max_w:
                max_w = w
        ws.column_dimensions[cl].width = min(max_w + 3, 30)

    ws.freeze_panes = "A2"
    wb.save(output_path)


def get_lark_token():
    """获取飞书 tenant_access_token"""
    with open(LARK_CREDENTIALS, "r") as f:
        config = json.load(f)
    app_id = config["apps"][0]["appId"]
    app_secret = config["apps"][0]["appSecret"]

    data = json.dumps({"app_id": app_id, "app_secret": app_secret}).encode()
    req = urllib.request.Request(
        "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
        data=data,
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=10) as resp:
        result = json.loads(resp.read())
    return result["tenant_access_token"]


def send_file_to_chat(file_path, file_name, chat_id):
    """上传文件并发送到飞书群"""
    token = get_lark_token()

    # 上传文件
    upload_cmd = [
        "curl", "-s", "-X", "POST",
        "https://open.feishu.cn/open-apis/im/v1/files",
        "-H", f"Authorization: Bearer {token}",
        "-F", "file_type=xls",
        "-F", f"file_name={file_name}",
        "-F", f"file=@{file_path}",
    ]
    result = subprocess.run(upload_cmd, capture_output=True, text=True, timeout=60)
    upload_resp = json.loads(result.stdout)
    if upload_resp.get("code") != 0:
        raise RuntimeError(f"文件上传失败: {upload_resp}")
    file_key = upload_resp["data"]["file_key"]

    # 发送文件消息
    send_data = json.dumps({
        "receive_id": chat_id,
        "msg_type": "file",
        "content": json.dumps({"file_key": file_key}),
    })
    req = urllib.request.Request(
        f"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id",
        data=send_data.encode(),
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        },
    )
    with urllib.request.urlopen(req, timeout=10) as resp:
        send_resp = json.loads(resp.read())
    if send_resp.get("code") != 0:
        raise RuntimeError(f"消息发送失败: {send_resp}")
    return send_resp["data"]["message_id"]


def main():
    parser = argparse.ArgumentParser(description="手机号 → 课时完成记录查询")
    parser.add_argument("input_file", help="包含手机号的 Excel 或 CSV 文件")
    parser.add_argument("--output", "-o", help="输出 xlsx 文件路径")
    parser.add_argument("--no-send", action="store_true", help="不发送到飞书群（仅生成文件）")
    args = parser.parse_args()

    os.makedirs(OUTPUT_DIR, exist_ok=True)
    if args.output:
        output_path = args.output
    else:
        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
        output_path = os.path.join(OUTPUT_DIR, f"phone_chapter_query_{ts}.xlsx")

    # 加载密码
    print("读取数据库配置...")
    pg_password = load_pg_password()

    # 提取手机号
    print(f"读取文件: {args.input_file}")
    phones = extract_phones(args.input_file)
    print(f"提取到 {len(phones)} 个手机号（去重后）")
    if not phones:
        print("ERROR: 未提取到任何手机号")
        sys.exit(1)

    # 匹配 account_id
    print("匹配账号...")
    account_ids, account_to_phone, unmatched = step1_match_accounts(phones, pg_password)
    print(f"已匹配: {len(account_ids)} 个账号，未匹配: {len(unmatched)} 个手机号")

    # 查询课时完成记录
    if account_ids:
        print(f"查询课时完成记录（{len(account_ids)} 个账号）...")
        csv_data = step2_query_chapter_play(account_ids, pg_password)
        row_count = csv_data.strip().count("\n")
        print(f"查询到 {row_count} 条记录")
    else:
        csv_data = ""
        print("无匹配账号，跳过课时查询")

    # 生成 xlsx
    print("生成 xlsx...")
    write_xlsx(csv_data, account_to_phone, unmatched, output_path)
    print(f"文件保存至: {output_path}")

    # 发送到飞书群
    if not args.no_send:
        print(f"发送到飞书群 {LARK_TARGET_CHAT_ID}...")
        file_name = os.path.basename(output_path)
        msg_id = send_file_to_chat(output_path, file_name, LARK_TARGET_CHAT_ID)
        print(f"发送成功！message_id: {msg_id}")
    else:
        print("跳过飞书发送（--no-send）")

    print("全部完成！")


if __name__ == "__main__":
    main()
