#!/usr/bin/env python3
"""
销售线索转化统计 v9
- 线索：以销售明细（吴迪/小龙/成都）为准
- 订单+退款：以数据库为准，通过线索 user_id 匹配
- 汇总：按线索进线月归因
- 销转总览 + 落单渠道：均拆分到个人维度
"""
import json, requests, os, re, time, psycopg2
from collections import defaultdict

CRED_DIR = "/root/.openclaw/credentials/xiaoxi"
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
SHEET_MAP = {"吴迪": "f975f0", "小龙": "qJF4I", "成都": "qJF4J"}
TARGET_MONTHS = [3, 4, 5, 6]
COSTS = {3: 243, 4: 246, 5: 241}
SALES_ORDER = ["小龙", "吴迪", "Bob", "Tom"]

PG_HOST = "bj-postgres-16pob4sg.sql.tencentcdb.com"
PG_PORT = 28591
PG_USER = "ai_member"
PG_PASSWORD = "LdfjdjL83h3h3^$&**YGG*"
PG_DB = "vala_bi"

# ── 数据库查询 ──────────────────────────────────────────

def get_orders_for_accounts(account_ids):
    if not account_ids:
        return {}, []
    conn = psycopg2.connect(host=PG_HOST, port=PG_PORT, user=PG_USER, password=PG_PASSWORD, dbname=PG_DB)
    cur = conn.cursor()
    placeholders = ','.join(['%s'] * len(account_ids))
    cur.execute(f"""
        SELECT o.account_id, o.trade_no, o.pay_success_date, o.key_from,
               o.pay_amount_int, o.order_status
        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 ({placeholders})
          AND o.pay_success_date IS NOT NULL
          AND o.order_status IN (3, 4)
        ORDER BY o.account_id, o.pay_success_date
    """, list(account_ids))
    
    orders_by_account = defaultdict(list)
    all_trade_nos = []
    for row in cur.fetchall():
        aid, trade_no, pay_date, key_from, amount, status = row
        orders_by_account[aid].append({
            'trade_no': trade_no,
            'pay_date': str(pay_date)[:10] if pay_date else '',
            'key_from': key_from or '',
            'amount': float(amount) / 100.0 if amount else 0,
            'status': status,
        })
        all_trade_nos.append(trade_no)
    cur.close()
    conn.close()
    return orders_by_account, all_trade_nos


def get_refund_for_trade_nos(trade_nos):
    if not trade_nos:
        return {}
    conn = psycopg2.connect(host=PG_HOST, port=PG_PORT, user=PG_USER, password=PG_PASSWORD, dbname=PG_DB)
    cur = conn.cursor()
    refunds = {}
    batch_size = 500
    for i in range(0, len(trade_nos), batch_size):
        batch = trade_nos[i:i+batch_size]
        placeholders = ','.join(['%s'] * len(batch))
        cur.execute(f"""
            SELECT o.trade_no, COALESCE(SUM(r.refund_amount::numeric), 0)/100.0
            FROM bi_vala_order o
            JOIN bi_refund_order r ON o.trade_no = r.trade_no
            WHERE r.status = 3 AND o.order_status = 4
              AND o.trade_no IN ({placeholders})
            GROUP BY o.trade_no
        """, batch)
        for row in cur.fetchall():
            refunds[row[0]] = float(row[1])
    cur.close()
    conn.close()
    return refunds

# ── 渠道归类 ──────────────────────────────────────────

def classify_channel(key_from):
    kf = str(key_from).strip()
    if not kf:
        return "其他"
    # 销转渠道
    if kf.startswith("sales-adp"):
        return "销转渠道"
    # 端内
    if kf in ('app-active-h5-0-0', 'app-sales-bj-qhm-0'):
        return "端内"
    if kf.startswith("miniprogram"):
        return "端内"
    # 达人渠道
    if kf.startswith("newmedia-daren") or "daren" in kf.lower():
        return "达人渠道"
    # 直购渠道
    if kf.startswith("newmedia-dianpu-xhs"):
        return "直购渠道"
    if kf.startswith("newmedia-dianpu-douyin"):
        return "直购渠道"
    if "jingxuan" in kf and "douyin" in kf.lower():
        return "直购渠道"
    if kf.startswith("stream-xhs"):
        return "直购渠道"
    if "wxxd" in kf:
        return "直购渠道"
    if kf.startswith("partner"):
        return "直购渠道"
    # 达人渠道
    if kf.startswith("newmedia-dianpu-wwxx"):
        return "达人渠道"  # 万物算达人，成本要加佣金
    # 其他新媒体（应该没有了）
    if kf.startswith("newmedia-"):
        return "直购渠道"
    return "其他"

CHANNEL_ORDER = ["销转渠道", "端内", "直购渠道", "达人渠道", "其他"]

# ── 飞书 API ──────────────────────────────────────────

BLUE = "#4472C4"
WHITE = "#FFFFFF"
LIGHT_BLUE = "#D6E4F0"
PURE_WHITE = "#FFFFFF"
YELLOW = "#FFF2CC"
YELLOW2 = "#FFF9E6"
GREEN = "#C6EFCE"
ORANGE = "#FCE4EC"
ORANGE2 = "#FFF0F3"

def get_token():
    with open(os.path.join(CRED_DIR, "config.json")) as f:
        cfg = json.load(f)
    resp = requests.post("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
        json={"app_id": cfg["apps"][0]["appId"], "app_secret": cfg["apps"][0]["appSecret"]}, timeout=15)
    return resp.json()["tenant_access_token"]

def read_sheet(token, sheet_id):
    resp = requests.get(
        f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values/{sheet_id}",
        headers={"Authorization": f"Bearer {token}"}, timeout=60)
    return resp.json()["data"]["valueRange"]["values"][2:]

def parse_month(date_str):
    m = re.match(r'(\d+)月', str(date_str).strip())
    return int(m.group(1)) if m else None

def parse_row(row):
    def get(idx, default=""):
        return str(row[idx]).strip() if idx < len(row) and row[idx] else default
    return {
        "sales": get(0), "nickname": get(1), "lead_date": get(2),
        "trial_lessons": get(3), "phone": get(4), "grade": get(5),
        "history": get(6), "user_id": get(7), "reg_date": get(8),
        "download_channel": get(9), "is_order": get(10), "order_date": get(11),
        "order_channel": get(12), "product": get(13), "gmv": get(14),
        "refund": get(15), "gsv": get(16), "activated": get(17),
        "progress": get(18), "last_study": get(19), "study_min": get(20),
        "update_time": get(21),
    }

def safe_float(val):
    try: return float(val)
    except: return 0.0

def safe_int(val):
    try: return int(float(val))
    except: return 0

def fmt_pct(val):
    if val == 0: return "0.0%"
    return f"{val*100:.1f}%"

def fmt_rmb(val):
    return f"¥{int(val):,}"

def fmt_roi(val):
    return f"{val:.2f}"

def get_or_create_sheet(token, title):
    """获取已有 sheet 或创建新 sheet，并清空旧数据"""
    # 先查已有
    resp = requests.get(
        f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/metainfo",
        headers={"Authorization": f"Bearer {token}"}, timeout=15)
    sheets = resp.json().get("data", {}).get("sheets", [])
    for s in sheets:
        if s.get("title") == title:
            sid = s["sheetId"]
            print(f"  复用已有 sheet: {title} ({sid})，清空旧数据...")
            # 清空 A1:ZZ200
            requests.put(
                f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values",
                headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
                json={"valueRange": {"range": f"{sid}!A1:ZZ200", "values": [['']]}}, timeout=30)
            return sid
    
    # 不存在则创建
    resp = requests.post(
        f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/sheets_batch_update",
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
        json={"requests": [{"addSheet": {"properties": {"title": title, "index": 10}}}]}, timeout=30)
    result = resp.json()
    if result.get("code") == 0:
        replies = result["data"]["replies"]
        if replies and "addSheet" in replies[0]:
            sid = replies[0]["addSheet"]["properties"]["sheetId"]
            print(f"  创建 sheet: {title} ({sid})")
            return sid
    print(f"  创建sheet失败: {result}")
    return None

def write_values(token, sheet_id, range_str, values):
    resp = requests.put(
        f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values",
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
        json={"valueRange": {"range": f"{sheet_id}!{range_str}", "values": values}}, timeout=30)
    code = resp.json().get("code")
    if code != 0:
        print(f"  ❌ {range_str}: {resp.json()}")
    return code == 0

def apply_style(token, sheet_id, col_start, row_start, col_end, row_end, style):
    rng = f"{sheet_id}!{col_start}{row_start}:{col_end}{row_end}"
    resp = requests.put(
        f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/style",
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
        json={"appendStyle": {"range": rng, "style": style}}, timeout=30)
    code = resp.json().get("code")
    if code != 0:
        print(f"  ❌ {rng}: {resp.json()}")
    return code == 0

def header_style():
    return {"backcolor": BLUE, "fontColor": WHITE, "bold": True, "fontSize": 10}

def total_style():
    return {"backcolor": GREEN, "fontSize": 10, "bold": True}

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

def main():
    token = get_token()
    
    # 1. 读取销售明细（线索）
    print("读取销售明细...")
    all_leads = []
    for sheet_label, sheet_id in SHEET_MAP.items():
        rows = read_sheet(token, sheet_id)
        for row in rows:
            d = parse_row(row)
            all_leads.append(d)
    print(f"  总计: {len(all_leads)} 条线索")
    
    # 按进线月分组
    by_month = defaultdict(list)
    for r in all_leads:
        m = parse_month(r["lead_date"])
        if m in TARGET_MONTHS:
            by_month[m].append(r)
    for m in TARGET_MONTHS:
        print(f"  {m}月: {len(by_month[m])} 条线索")
    
    # 2. 收集所有线索的 user_id，去数据库查订单
    all_user_ids = set()
    for r in all_leads:
        uid = safe_int(r["user_id"])
        if uid > 0:
            all_user_ids.add(uid)
    print(f"\n查询数据库订单（{len(all_user_ids)} 个用户）...")
    db_orders, all_trade_nos = get_orders_for_accounts(all_user_ids)
    db_order_users = set(db_orders.keys())
    print(f"  有订单的用户: {len(db_order_users)} 人, 订单: {sum(len(v) for v in db_orders.values())} 笔")
    
    # 3. 查退款
    print(f"查询退款（{len(all_trade_nos)} 笔订单）...")
    refund_by_trade = get_refund_for_trade_nos(all_trade_nos)
    refund_total = sum(refund_by_trade.values())
    refund_users = set()
    for trade_no in refund_by_trade:
        for aid, orders in db_orders.items():
            for o in orders:
                if o['trade_no'] == trade_no:
                    refund_users.add(aid)
    print(f"  退款: {len(refund_by_trade)} 笔, ¥{refund_total:,.0f}, {len(refund_users)} 人")
    
    # 4. 为每条线索匹配数据库订单
    for r in all_leads:
        uid = safe_int(r["user_id"])
        r["_db_orders"] = db_orders.get(uid, [])
        r["_has_order"] = len(r["_db_orders"]) > 0
    
    # 5. 计算每条线索的 GMV / 退款 / GSV（来自数据库）
    for r in all_leads:
        orders = r["_db_orders"]
        r["_db_gmv"] = sum(o['amount'] for o in orders)
        r["_db_refund"] = sum(refund_by_trade.get(o['trade_no'], 0) for o in orders)
        r["_db_gsv"] = r["_db_gmv"] - r["_db_refund"]
    
    # 6. 构建数据（供公式和底表使用）
    funnel_rows = []
    for m in TARGET_MONTHS:
        for sales in SALES_ORDER:
            leads = [r for r in by_month[m] if r["sales"] == sales]
            if not leads:
                continue
            lead_count = len(leads)
            has_phone = len([r for r in leads if r["phone"] and r["phone"] != "未注册"])
            registered = len([r for r in leads if r["user_id"] and r["user_id"] != "未注册"])
            trial = [safe_int(r["trial_lessons"]) for r in leads]
            ordered = [r for r in leads if r["_has_order"]]
            order_count = len(ordered)
            gmv = sum(r["_db_gmv"] for r in ordered)
            refund = sum(r["_db_refund"] for r in ordered)
            gsv = gmv - refund
            
            phone_rate = has_phone / lead_count if lead_count else 0
            reg_rate = registered / lead_count if lead_count else 0
            lesson_data = {}
            for n in range(1, 6):
                cnt = len([t for t in trial if t >= n])
                ord_n = len([r for r in leads if safe_int(r["trial_lessons"]) >= n and r["_has_order"]])
                lesson_data[n] = (cnt, ord_n)
            rates = {}
            for n in range(1, 6):
                cnt, ord_n = lesson_data[n]
                rates[n] = (cnt / lead_count if lead_count else 0, ord_n / cnt if cnt else 0)
            order_conv = order_count / lead_count if lead_count else 0
            
            funnel_rows.append([
                f"{m}月", sales, lead_count,
                has_phone, fmt_pct(phone_rate),
                registered, fmt_pct(reg_rate),
                lesson_data[1][0], fmt_pct(rates[1][0]), fmt_pct(rates[1][1]),
                lesson_data[2][0], fmt_pct(rates[2][0]), fmt_pct(rates[2][1]),
                lesson_data[3][0], fmt_pct(rates[3][0]), fmt_pct(rates[3][1]),
                lesson_data[4][0], fmt_pct(rates[4][0]), fmt_pct(rates[4][1]),
                lesson_data[5][0], fmt_pct(rates[5][0]), fmt_pct(rates[5][1]),
                order_count, fmt_pct(order_conv),
                gmv, refund, gsv,
            ])
    
    # 渠道底表数据
    channel_rows = []
    for m in TARGET_MONTHS:
        for sales in SALES_ORDER:
            if sum(1 for r in by_month[m] if r["sales"] == sales) == 0:
                continue
            for cat in CHANNEL_ORDER:
                o = 0; g = 0.0
                for r in by_month[m]:
                    if r["sales"] != sales: continue
                    for odb in r["_db_orders"]:
                        if classify_channel(odb['key_from']) == cat:
                            o += 1; g += odb['amount']
                if o > 0 or g > 0:
                    channel_rows.append([f"{m}月", sales, cat, o, g])
        # 月合计渠道行
        for cat in CHANNEL_ORDER:
            o = 0; g = 0.0
            for r in by_month[m]:
                for odb in r["_db_orders"]:
                    if classify_channel(odb['key_from']) == cat:
                        o += 1; g += odb['amount']
            if o > 0 or g > 0:
                channel_rows.append([f"{m}月", "合计", cat, o, g])
    
    # 渠道统计（月 × 销售 × 渠道）供落单渠道分布使用
    ms_channel = defaultdict(lambda: defaultdict(lambda: {"orders": 0, "gmv": 0.0}))
    for m in TARGET_MONTHS:
        for r in by_month[m]:
            sales = r["sales"]
            for o in r["_db_orders"]:
                cat = classify_channel(o['key_from'])
                ms_channel[(m, sales)][cat]["orders"] += 1
                ms_channel[(m, sales)][cat]["gmv"] += o['amount']
    
    # 7. 过程数据（唯一的数据底表）
    print("\n=== 1. 过程数据（底表） ===")
    sid_funnel = get_or_create_sheet(token, "📊 过程数据")
    
    # 清除旧数据
    meta = requests.get(
        f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/metainfo",
        headers={"Authorization": f"Bearer {token}"}, timeout=10).json()
    sheets_info = meta.get('data', {}).get('sheets', [])
    max_rows = 200
    for s in sheets_info:
        if s.get('sheetId') == sid_funnel:
            max_rows = s.get('row_count', 200)
            break
    if max_rows > 0:
        requests.delete(
            f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/sheets/{sid_funnel}/range?dimension=ROWS&start_index=1&end_index={max_rows}",
            headers={"Authorization": f"Bearer {token}"}, timeout=15)
        time.sleep(0.5)
    
    headers3 = [["月份", "销售", "线索总数",
        "拿手机号数", "拿手机号率", "注册数", "注册率",
        "首课人数", "首课率", "一节课转化率",
        "二次课人数", "二次课率", "二节课转化率",
        "三次课人数", "三次课率", "三节课转化率",
        "四次课人数", "四次课率", "四节课转化率",
        "五次课人数", "五次课率", "五节课转化率",
        "订单数", "转化率", "GMV", "退款金额", "GSV",
    ]]
    # 批量写入（一次性API调用，避免限流）
    channel_start = 16
    cost_ref_data = [["月份", "月投放成本"]]
    for m in TARGET_MONTHS:
        c = COSTS.get(m, 0)
        if c > 0:
            cost_ref_data.append([f"{m}月", c])
    
    batch_data = [
        {"range": f"{sid_funnel}!A1:AA1", "values": headers3},
        {"range": f"{sid_funnel}!A2:AA{1+len(funnel_rows)}", "values": funnel_rows},
        {"range": f"{sid_funnel}!A{channel_start}:E{channel_start}", "values": [["月份", "销售", "渠道", "订单数", "GMV"]]},
        {"range": f"{sid_funnel}!A{channel_start+1}:E{channel_start+len(channel_rows)}", "values": channel_rows},
        {"range": f"{sid_funnel}!G{channel_start}:H{channel_start+len(cost_ref_data)-1}", "values": cost_ref_data},
    ]
    resp = requests.post(
        f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values_batch_update",
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
        json={"valueInputOption": "USER", "valueRanges": batch_data},
        timeout=60)
    code = resp.json().get("code")
    if code != 0:
        print(f"  ❌ 批量写入失败: {resp.json()}")
    time.sleep(1)

    print("  ✅ 数据写入完成")


if __name__ == "__main__":
    main()
