#!/usr/bin/env python3
"""
销售三表 K–Q + S/U 订单&行课写回（Step2 触发）
触发：Cursor Step1 完成后 @小溪，或群发【执行更新】
归属：小溪 (xiaoxi)

写回列：
  K: 是否下单（是/空）
  L: 下单日期
  M: 成交渠道
  N: 产品名称
  O: GMV（元）
  P: 退款（元，全额退则清空 K/O/P/Q）
  Q: GSV（元）
  S: 行课进度（如 L1-S0-U00-L01）
  U: 累计学习时长（分钟）

规则：
  - L≥C 才 K=是
  - 同 UID 多行只写匹配手机号那行
  - 全额退(P≥O) → 清 K/O/P/Q，不进订单表
  - 只填空/更新，不覆盖已有有效数据（除非需要修正）

分工约定见 docs/bot-step2-schedule-and-orders.md
"""
import json, time, re, sys, requests, psycopg2
from datetime import datetime
from collections import defaultdict

# ── 配置 ──
APP_ID = "cli_a929ae22e0b8dcc8"
APP_SECRET = "OtFjMy7p3qE3VvLbMdcWidwgHOnGD4FJ"
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
SALES_SHEETS = {"f975f0": "吴迪", "qJF4I": "小龙", "qJF4J": "成都"}

def _get_pg_password():
    import os
    secrets_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "secrets.env")
    with open(secrets_path) as f:
        for line in f:
            if line.startswith("PG_ONLINE_PASSWORD="):
                return line.strip().split("=", 1)[1].strip('"').strip("'")
    raise RuntimeError("PG_ONLINE_PASSWORD not found in secrets.env")

PG_CONFIG = {
    "host": "bj-postgres-16pob4sg.sql.tencentcdb.com", "port": 28591,
    "user": "ai_member", "password": _get_pg_password(), "database": "vala_bi",
}

GOODS_MAP = {
    57: "瓦拉英语level1·单季", 60: "瓦拉英语level1", 63: "瓦拉英语level1·单季",
    31: "瓦拉英语年包", 32: "瓦拉英语单季度包", 33: "瓦拉英语level2", 54: "瓦拉英语季度包",
    61: "瓦拉英语level1+2",
}


def get_token():
    r = requests.post("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
                      json={"app_id": APP_ID, "app_secret": APP_SECRET}, timeout=15)
    return r.json()["tenant_access_token"]


def read_sheet(token, sheet_id, range_str):
    url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values/{sheet_id}!{range_str}?valueRenderOption=ToString"
    r = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=30)
    data = r.json()
    if data.get("code") != 0:
        print(f"Error reading {sheet_id}: {data}")
        return []
    return data["data"]["valueRange"]["values"]


def put_values(token, sheet_id, range_str, values, retries=3):
    url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values"
    body = {"valueRange": {"range": f"{sheet_id}!{range_str}", "values": values}}
    for attempt in range(retries):
        r = requests.put(url, headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json"
        }, json=body, timeout=30)
        result = r.json()
        if result.get("code") == 0:
            return True
        print(f"  Retry {attempt+1} for {range_str}: {result.get('msg','')}")
        time.sleep(1)
    print(f"  FAILED {range_str}")
    return False


def write_batch_col(token, sheet_id, col_letter, row_vals):
    """Write multiple cells in same column. row_vals = [(row, val), ...]"""
    if not row_vals:
        return
    row_vals.sort()
    i = 0
    while i < len(row_vals):
        j = i
        while j + 1 < len(row_vals) and row_vals[j + 1][0] == row_vals[j][0] + 1:
            j += 1
        start, end = row_vals[i][0], row_vals[j][0]
        vals = [[v] for _, v in row_vals[i:j + 1]]
        put_values(token, sheet_id, f"{col_letter}{start}:{col_letter}{end}", vals)
        i = j + 1
        time.sleep(0.1)


def parse_date(s):
    s = str(s).strip()
    if not s:
        return None
    m = re.match(r'(\d{4})-(\d{1,2})-(\d{1,2})', s)
    if m:
        return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
    m = re.match(r'(\d{1,2})月(\d{1,2})日', s)
    if m:
        return (2026, int(m.group(1)), int(m.group(2)))
    m = re.match(r'(\d{4})/(\d{1,2})/(\d{1,2})', s)
    if m:
        return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
    return None


def date_le(a, b):
    if a is None or b is None:
        return False
    return a <= b


def phone_match(sheet_phone, db_tel):
    if not sheet_phone or not db_tel:
        return False
    sheet_phone = str(sheet_phone).strip()
    db_tel = str(db_tel).strip()
    if sheet_phone == db_tel:
        return True
    if "****" in db_tel:
        parts = db_tel.split("****")
        if len(parts) == 2:
            return sheet_phone.startswith(parts[0]) and sheet_phone.endswith(parts[1])
    return False


def main():
    print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] Step2 K–Q + S/U 写回 启动")
    token = get_token()

    # ── Step 1: 读取销售三表 ──
    all_rows = []
    for sid, name in SALES_SHEETS.items():
        print(f"Reading {name}...")
        vals = read_sheet(token, sid, "A3:V10000")
        for i, row in enumerate(vals):
            while len(row) < 22:
                row.append("")
            b = str(row[1]).strip() if row[1] else ""
            e = str(row[4]).strip() if row[4] else ""
            h = str(row[7]).strip() if row[7] else ""
            if not b and not e and not h:
                continue
            all_rows.append({
                "sid": sid, "name": name, "row": i + 3,
                "B": b,
                "C": str(row[2]).strip() if row[2] else "",
                "E": e,
                "H": h,
                "K": str(row[10]).strip() if len(row) > 10 and row[10] else "",
                "L": str(row[11]).strip() if len(row) > 11 and row[11] else "",
                "M": str(row[12]).strip() if len(row) > 12 and row[12] else "",
                "N": str(row[13]).strip() if len(row) > 13 and row[13] else "",
                "O": str(row[14]).strip() if len(row) > 14 and row[14] else "",
                "P": str(row[15]).strip() if len(row) > 15 and row[15] else "",
                "Q": str(row[16]).strip() if len(row) > 16 and row[16] else "",
                "S": str(row[18]).strip() if len(row) > 18 and row[18] else "",
                "U": str(row[20]).strip() if len(row) > 20 and row[20] else "",
            })

    print(f"Total rows: {len(all_rows)}")

    # ── Step 2: 收集 UID ──
    all_uids = set()
    for r in all_rows:
        if r["H"].isdigit():
            all_uids.add(r["H"])
    print(f"Unique UIDs: {len(all_uids)}")

    # ── Step 3: DB 查询 ──
    uid_tel = {}
    uid_orders = {}
    uid_refunds = {}
    uid_learning = {}  # uid -> {progress, minutes}

    conn = psycopg2.connect(**PG_CONFIG)
    cur = conn.cursor()
    ul = list(all_uids)

    # 3a. 手机号
    for s in range(0, len(ul), 500):
        ch = ul[s:s + 500]
        ph = ",".join(["%s"] * len(ch))
        cur.execute(f"SELECT id, tel FROM bi_vala_app_account WHERE id IN ({ph})", ch)
        for uid, tel in cur.fetchall():
            uid_tel[str(uid)] = tel or ""
        time.sleep(0.05)

    # 3b. 订单（非测试，order_status 3/4）
    for s in range(0, len(ul), 100):
        ch = ul[s:s + 100]
        ph = ",".join(["%s"] * len(ch))
        cur.execute(f"""
            SELECT o.account_id, o.trade_no, o.goods_id, o.pay_amount_int,
                   o.pay_success_date, o.order_status, o.key_from
            FROM bi_vala_order o
            JOIN bi_vala_app_account a ON o.account_id = a.id AND a.status = 1
            WHERE o.account_id IN ({ph})
              AND o.pay_success_date IS NOT NULL
              AND o.order_status IN (3,4)
            ORDER BY o.account_id, o.pay_success_date DESC
        """, ch)
        for uid, tn, gid, amt, pd, os, kf in cur.fetchall():
            uid_orders.setdefault(str(uid), []).append({
                "tn": tn, "gid": gid, "amt": amt,
                "pd": str(pd)[:10] if pd else "",
                "os": os, "kf": kf or "",
            })
        time.sleep(0.05)

    # 3c. 退款（order_status=4 + refund status=3）
    for s in range(0, len(ul), 100):
        ch = ul[s:s + 100]
        ph = ",".join(["%s"] * len(ch))
        cur.execute(f"""
            SELECT o.account_id, COALESCE(SUM(r.refund_amount::numeric), 0)
            FROM bi_vala_order o
            JOIN bi_refund_order r ON o.trade_no = r.trade_no AND r.status = 3
            JOIN bi_vala_app_account a ON o.account_id = a.id AND a.status = 1
            WHERE o.account_id IN ({ph})
              AND o.order_status = 4
              AND o.pay_success_date IS NOT NULL
            GROUP BY o.account_id
        """, ch)
        for uid, ref in cur.fetchall():
            uid_refunds[str(uid)] = float(ref)
        time.sleep(0.05)

    # 3d. 行课进度（S/U）
    # 角色映射
    ul_int = [int(x) for x in ul]
    cur.execute("SELECT account_id, id FROM bi_vala_app_character WHERE account_id = ANY(%s) AND deleted_at IS NULL",
                (ul_int,))
    account_chars = defaultdict(list)
    char_to_account = {}
    for aid, cid in cur.fetchall():
        account_chars[str(aid)].append(cid)
        char_to_account[cid] = str(aid)
    char_ids = list(char_to_account.keys())
    print(f"  Characters: {len(char_ids)}")

    # 课程映射
    cur.execute("SELECT id, course_level, course_season, course_unit, course_lesson FROM bi_level_unit_lesson")
    chapter_map = {}
    for ch_id, cl, cs, cu, cl2 in cur.fetchall():
        chapter_map[ch_id] = (cl or "", cs or "", cu or "", cl2 or "")

    # 课时完成记录
    char_plays = defaultdict(lambda: {"latest_time": None, "latest_chapter": None, "total_ms": 0})
    for tbl_idx in range(8):
        table = f"bi_user_chapter_play_record_{tbl_idx}"
        try:
            cur.execute(
                f"SELECT user_id, chapter_id, created_at FROM {table} WHERE play_status=1 AND deleted_at IS NULL AND user_id = ANY(%s)",
                (char_ids,)
            )
            for uid, ch_id, created_at in cur.fetchall():
                ch_data = chapter_map.get(ch_id)
                if not ch_data:
                    continue
                rec = char_plays[uid]
                if rec["latest_time"] is None or created_at > rec["latest_time"]:
                    rec["latest_time"] = created_at
                    rec["latest_chapter"] = ch_data
        except Exception as e:
            print(f"  Warning {table}: {e}")

    # 学习总耗时
    for tbl_idx in range(8):
        table = f"bi_user_component_play_record_{tbl_idx}"
        try:
            cur.execute(
                f"SELECT user_id, SUM(COALESCE(interval_time,0)) FROM {table} WHERE user_id = ANY(%s) AND deleted_at IS NULL GROUP BY user_id",
                (char_ids,)
            )
            for uid, total_ms in cur.fetchall():
                if uid in char_plays:
                    char_plays[uid]["total_ms"] += (total_ms or 0)
        except Exception as e:
            print(f"  Warning {table}: {e}")

    # 汇总到 account 级别
    for uid_str in all_uids:
        chars = account_chars.get(uid_str, [])
        best_ch = None
        best_time = None
        total_ms = 0
        for cid in chars:
            play = char_plays.get(cid)
            if not play:
                continue
            if play["latest_chapter"]:
                if best_time is None or play["latest_time"] > best_time:
                    best_time = play["latest_time"]
                    best_ch = play["latest_chapter"]
            total_ms += play["total_ms"]

        if best_ch:
            cl, cs, cu, cl2 = best_ch
            uid_learning[uid_str] = {
                "progress": f"{cl}-{cs}-{cu}-{cl2}",
                "minutes": round(total_ms / 60000, 1),
            }
            if uid_learning[uid_str]["minutes"] == int(uid_learning[uid_str]["minutes"]):
                uid_learning[uid_str]["minutes"] = int(uid_learning[uid_str]["minutes"])

    cur.close()
    conn.close()
    print(f"  Orders: {len(uid_orders)}, Refunds: {len(uid_refunds)}, Learning: {len(uid_learning)}")

    # ── Step 4: DUP 检测 ──
    uid_row_groups = defaultdict(list)
    for r in all_rows:
        if r["H"].isdigit():
            uid_row_groups[r["H"]].append(r)

    uid_best_row = {}
    for uid, rows in uid_row_groups.items():
        if len(rows) == 1:
            uid_best_row[uid] = rows[0]
        else:
            db_tel = uid_tel.get(uid, "")
            best = None
            for r in rows:
                if phone_match(r["E"], db_tel):
                    best = r
                    break
            if best is None:
                for r in rows:
                    if r["E"]:
                        best = r
                        break
            if best is None:
                best = rows[0]
            uid_best_row[uid] = best

    # ── Step 5: 计算每行应写值 ──
    writes = defaultdict(lambda: defaultdict(list))  # sid -> col -> [(row, val)]

    for r in all_rows:
        uid = r["H"] if r["H"].isdigit() else ""
        row = r["row"]
        sid = r["sid"]

        # 判断是否是该 UID 的最佳行
        is_best = (uid and uid in uid_best_row and uid_best_row[uid] is r)

        if not uid or uid not in uid_orders or not is_best:
            # 无订单或非最佳行：确保 K/O/P/Q 为空
            if r["K"]:
                writes[sid]["K"].append((row, ""))
            if r["O"]:
                writes[sid]["O"].append((row, ""))
            if r["P"]:
                writes[sid]["P"].append((row, ""))
            if r["Q"]:
                writes[sid]["Q"].append((row, ""))
            if r["L"]:
                writes[sid]["L"].append((row, ""))
            if r["M"]:
                writes[sid]["M"].append((row, ""))
            if r["N"]:
                writes[sid]["N"].append((row, ""))
            # 行课数据仍然写入（独立于订单）
            if uid in uid_learning:
                learning = uid_learning[uid]
                if learning["progress"] and str(r["S"]) != learning["progress"]:
                    writes[sid]["S"].append((row, learning["progress"]))
                if learning["minutes"] and str(r["U"]) != str(learning["minutes"]):
                    writes[sid]["U"].append((row, learning["minutes"]))
            continue

        # 有订单 + 最佳行
        orders = uid_orders[uid]
        total_gmv_cents = sum(o["amt"] for o in orders)
        total_refund_cents = int(uid_refunds.get(uid, 0) * 100)
        gsv_cents = total_gmv_cents - total_refund_cents
        total_gmv = total_gmv_cents // 100
        total_refund = total_refund_cents // 100
        gsv = gsv_cents // 100
        latest = max(orders, key=lambda o: o["pd"])

        # L≥C 检查
        c_date = parse_date(r["C"])
        l_date = parse_date(latest["pd"])

        if not date_le(c_date, l_date):
            # L<C: 不清 K（可能之前已写），但也不写新数据
            continue

        # 全额退检查
        is_full_refund = (total_refund_cents > 0 and total_refund_cents >= total_gmv_cents)

        if is_full_refund:
            # 全额退 → 清 K/O/P/Q
            if r["K"]:
                writes[sid]["K"].append((row, ""))
            if r["O"]:
                writes[sid]["O"].append((row, ""))
            if r["P"]:
                writes[sid]["P"].append((row, ""))
            if r["Q"]:
                writes[sid]["Q"].append((row, ""))
            if r["L"]:
                writes[sid]["L"].append((row, ""))
            if r["M"]:
                writes[sid]["M"].append((row, ""))
            if r["N"]:
                writes[sid]["N"].append((row, ""))
        else:
            # 正常成单
            if r["K"] != "是":
                writes[sid]["K"].append((row, "是"))
            if str(r["L"]) != latest["pd"]:
                writes[sid]["L"].append((row, latest["pd"]))
            if str(r["M"]) != latest["kf"]:
                writes[sid]["M"].append((row, latest["kf"]))
            goods_name = GOODS_MAP.get(latest["gid"], f"商品{latest['gid']}")
            if str(r["N"]) != goods_name:
                writes[sid]["N"].append((row, goods_name))
            o_write = total_gmv if total_gmv > 0 else ""
            if str(r["O"]) != str(o_write):
                writes[sid]["O"].append((row, o_write))
            p_write = total_refund if total_refund > 0 else ""
            if str(r["P"]) != str(p_write):
                writes[sid]["P"].append((row, p_write))
            q_write = gsv if gsv > 0 else ""
            if str(r["Q"]) != str(q_write):
                writes[sid]["Q"].append((row, q_write))

        # 行课数据（独立于订单，所有有 UID 的用户都写）
        if uid in uid_learning:
            learning = uid_learning[uid]
            if learning["progress"] and str(r["S"]) != learning["progress"]:
                writes[sid]["S"].append((row, learning["progress"]))
            if learning["minutes"] and str(r["U"]) != str(learning["minutes"]):
                writes[sid]["U"].append((row, learning["minutes"]))

    # ── Step 6: 写入 ──
    cols_order = ["K", "L", "M", "N", "O", "P", "Q", "S", "U"]
    for sid, name in SALES_SHEETS.items():
        sid_writes = writes.get(sid, {})
        total_writes = sum(len(v) for v in sid_writes.values())
        if total_writes == 0:
            print(f"{name}: no changes")
            continue
        print(f"{name}: {total_writes} cell updates")
        for col in cols_order:
            col_writes = sid_writes.get(col, [])
            if col_writes:
                write_batch_col(token, sid, col, col_writes)

    print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] ✅ Step2 K–Q + S/U 写回完成")


if __name__ == "__main__":
    main()
