# -*- coding: utf-8 -*-
"""
按自定义名单统计累计打球总时长（同“忠实台球类竞技客户”口径）

默认口径：
- 时间范围：2025-10-01 至今天（可传参覆盖）
- 仅统计 member_id != 0 且有时长的记录
- 台桌区域过滤：A区/B区/C区/VIP包厢/斯诺克区/TV台（可传参覆盖）
- 同一客户同一天同时开多台：按时间区间并集计时（避免重复叠加）

输入：每行 “姓名<TAB>联系方式”
输出：CSV（UTF-8-SIG，便于 Excel 打开）

运行方式（需在 etl_billiards/ 下以模块方式运行）：
  python -m reports.loyal_customers_total_hours_for_list --input reports/loyal_customers_input_2025-12-26.txt
"""

from __future__ import annotations

import argparse
import csv
import re
from dataclasses import dataclass
from datetime import datetime, time, timedelta
from pathlib import Path
from zoneinfo import ZoneInfo

from config.settings import AppConfig
from database.connection import DatabaseConnection
from reports.loyal_billiards_customers_report import DEFAULT_AREAS, SessionRow, _as_tz, _build_member_aggs, _parse_date


@dataclass(frozen=True)
class InputRow:
    idx: int
    name: str
    phone_raw: str
    phone_digits: str


def _digits(s: str) -> str:
    return re.sub(r"\D+", "", s or "")


def _normalize_name(s: str) -> str:
    s = (s or "").strip()
    s = re.sub(r"[（(].*?[）)]", "", s)
    s = re.sub(r"\s+", "", s)
    return s


def _read_input(path: Path) -> list[InputRow]:
    rows: list[InputRow] = []
    with path.open("r", encoding="utf-8") as f:
        for i, line in enumerate(f, start=1):
            line = (line or "").strip()
            if not line or line.startswith("#"):
                continue
            parts = re.split(r"\t+", line)
            if len(parts) < 2:
                parts = re.split(r"\s+", line)
            if len(parts) < 2:
                continue
            name = parts[0].strip()
            phone_raw = parts[1].strip()
            rows.append(InputRow(idx=i, name=name, phone_raw=phone_raw, phone_digits=_digits(phone_raw)))
    return rows


def _load_member_candidates_by_phone(
    conn: DatabaseConnection,
    *,
    store_id: int,
    phone_digits_list: list[str],
) -> dict[str, dict[int, str]]:
    if not phone_digits_list:
        return {}
    sql = """
    SELECT DISTINCT
      member_id,
      member_name,
      member_phone
    FROM billiards_dwd.dwd_settlement_head
    WHERE site_id = %s
      AND member_id IS NOT NULL
      AND member_id <> 0
      AND regexp_replace(COALESCE(member_phone, ''), '\\D', '', 'g') = ANY(%s)
    """
    rows = conn.query(sql, (store_id, phone_digits_list))
    out: dict[str, dict[int, str]] = {}
    for r in rows:
        mid = int(r.get("member_id") or 0)
        if mid <= 0:
            continue
        phone_digits = _digits(r.get("member_phone") or "")
        if not phone_digits:
            continue
        name = str(r.get("member_name") or "")
        out.setdefault(phone_digits, {})
        if mid not in out[phone_digits]:
            out[phone_digits][mid] = name
    return out


def _load_member_candidates_by_mobile(
    conn: DatabaseConnection,
    *,
    phone_digits_list: list[str],
) -> dict[str, dict[int, str]]:
    if not phone_digits_list:
        return {}
    sql = """
    SELECT member_id, nickname, mobile
    FROM billiards_dwd.dim_member
    WHERE scd2_is_current = 1
      AND member_id IS NOT NULL
      AND member_id <> 0
      AND regexp_replace(COALESCE(mobile, ''), '\\D', '', 'g') = ANY(%s)
    """
    rows = conn.query(sql, (phone_digits_list,))
    out: dict[str, dict[int, str]] = {}
    for r in rows:
        mid = int(r.get("member_id") or 0)
        if mid <= 0:
            continue
        phone_digits = _digits(r.get("mobile") or "")
        if not phone_digits:
            continue
        name = str(r.get("nickname") or "")
        out.setdefault(phone_digits, {})
        if mid not in out[phone_digits]:
            out[phone_digits][mid] = name
    return out


def _resolve_member_ids(
    row: InputRow,
    *,
    settle_candidates: dict[str, dict[int, str]],
    dim_candidates: dict[str, dict[int, str]],
) -> list[int]:
    phone_digits = row.phone_digits
    candidate_map: dict[int, list[str]] = {}

    for mid, name in (settle_candidates.get(phone_digits) or {}).items():
        candidate_map.setdefault(int(mid), []).append(str(name or ""))
    for mid, name in (dim_candidates.get(phone_digits) or {}).items():
        candidate_map.setdefault(int(mid), []).append(str(name or ""))

    candidate_ids = sorted(candidate_map.keys())
    if len(candidate_ids) <= 1:
        return candidate_ids

    name_clean = _normalize_name(row.name)
    if not name_clean:
        return candidate_ids

    filtered: list[int] = []
    for mid in candidate_ids:
        names = candidate_map.get(mid) or []
        if any(name_clean in _normalize_name(n) or _normalize_name(n) in name_clean for n in names if n):
            filtered.append(mid)
    return filtered or candidate_ids


def _load_sessions_for_members(
    conn: DatabaseConnection,
    *,
    store_id: int,
    tz: ZoneInfo,
    start_dt: datetime,
    end_dt: datetime,
    areas: tuple[str, ...],
    member_ids: list[int],
) -> list[SessionRow]:
    if not member_ids:
        return []
    sql = """
    SELECT
      member_id,
      start_use_time,
      ledger_end_time,
      real_table_use_seconds,
      site_table_area_name
    FROM billiards_dwd.dwd_table_fee_log
    WHERE site_id = %s
      AND member_id = ANY(%s)
      AND start_use_time >= %s
      AND start_use_time < %s
      AND site_table_area_name = ANY(%s)
    """
    rows = conn.query(sql, (store_id, member_ids, start_dt, end_dt, list(areas)))
    sessions: list[SessionRow] = []
    for r in rows:
        mid = int(r.get("member_id") or 0)
        if mid <= 0:
            continue
        start = r.get("start_use_time")
        end = r.get("ledger_end_time")
        if not isinstance(start, datetime):
            continue
        if isinstance(end, datetime):
            pass
        else:
            secs = r.get("real_table_use_seconds")
            try:
                secs = int(secs or 0)
            except Exception:
                secs = 0
            if secs > 0:
                end = start + timedelta(seconds=secs)
            else:
                continue

        start = _as_tz(start, tz)
        end = _as_tz(end, tz)
        if end <= start:
            continue

        sessions.append(SessionRow(member_id=mid, start=start, end=end, area_name=r.get("site_table_area_name")))
    return sessions


def _write_report(out_path: Path, *, tz: ZoneInfo, rows: list[dict]):
    out_path.parent.mkdir(parents=True, exist_ok=True)
    with out_path.open("w", encoding="utf-8-sig", newline="") as f:
        w = csv.writer(f)
        w.writerow(
            [
                "排名",
                "客户姓名",
                "联系方式",
                "10-12月打球总时长(小时)",
                "来店日数",
                "最后一次到店日期",
                "匹配member_id列表",
                "匹配状态",
            ]
        )
        for r in rows:
            last_visit = r.get("last_visit_at")
            last_visit_str = ""
            if isinstance(last_visit, datetime):
                last_visit_str = _as_tz(last_visit, tz).strftime("%Y-%m-%d %H:%M")
            w.writerow(
                [
                    r.get("rank", ""),
                    r.get("name", ""),
                    r.get("phone_raw", ""),
                    f"{float(r.get('total_hours') or 0.0):.2f}",
                    int(r.get("visit_days") or 0),
                    last_visit_str,
                    r.get("member_ids_str", ""),
                    r.get("status", ""),
                ]
            )


def main() -> int:
    parser = argparse.ArgumentParser(description="Total play hours for a custom customer list")
    parser.add_argument("--input", default=str(Path(__file__).with_name("loyal_customers_input_2025-12-26.txt")))
    parser.add_argument("--start-date", default="2025-10-01", help="YYYY-MM-DD")
    parser.add_argument("--end-date", default="", help="YYYY-MM-DD (default: today)")
    parser.add_argument("--areas", default=",".join(DEFAULT_AREAS), help="comma separated")
    parser.add_argument("--out", default="", help="output csv path")
    args = parser.parse_args()

    cfg = AppConfig.load({})
    tz = ZoneInfo(cfg.get("app.timezone", "Asia/Taipei"))
    store_id = int(cfg.get("app.store_id"))

    input_path = Path(args.input)
    if not input_path.is_file():
        raise SystemExit(f"input not found: {input_path}")

    start_date = _parse_date(args.start_date)
    end_date = _parse_date(args.end_date) if args.end_date else datetime.now(tz).date()
    if end_date < start_date:
        raise SystemExit("end_date must be >= start_date")

    start_dt = datetime.combine(start_date, time.min).replace(tzinfo=tz)
    end_dt = datetime.combine(end_date + timedelta(days=1), time.min).replace(tzinfo=tz)

    areas = tuple([a.strip() for a in str(args.areas or "").split(",") if a.strip()])
    if not areas:
        raise SystemExit("areas is empty")

    input_rows = _read_input(input_path)
    phone_digits_list = sorted({r.phone_digits for r in input_rows if r.phone_digits})

    conn = DatabaseConnection(dsn=cfg["db"]["dsn"], session=cfg["db"].get("session"))
    try:
        settle_candidates = _load_member_candidates_by_phone(conn, store_id=store_id, phone_digits_list=phone_digits_list)
        dim_candidates = _load_member_candidates_by_mobile(conn, phone_digits_list=phone_digits_list)

        per_input_member_ids: dict[int, list[int]] = {}
        all_member_ids: set[int] = set()
        for r in input_rows:
            mids = _resolve_member_ids(r, settle_candidates=settle_candidates, dim_candidates=dim_candidates)
            per_input_member_ids[r.idx] = mids
            all_member_ids.update(mids)

        sessions = _load_sessions_for_members(
            conn,
            store_id=store_id,
            tz=tz,
            start_dt=start_dt,
            end_dt=end_dt,
            areas=areas,
            member_ids=sorted(all_member_ids),
        )
    finally:
        conn.close()

    aggs = _build_member_aggs(sessions, tz)
    agg_by_member = {int(a.member_id): a for a in aggs}

    out_rows: list[dict] = []
    for r in input_rows:
        mids = per_input_member_ids.get(r.idx) or []
        if not mids:
            out_rows.append(
                {
                    "rank": "",
                    "name": r.name,
                    "phone_raw": r.phone_raw,
                    "total_hours": 0.0,
                    "visit_days": 0,
                    "last_visit_at": None,
                    "member_ids_str": "",
                    "status": "NOT_FOUND",
                }
            )
            continue

        total_hours = 0.0
        visit_days = 0
        last_visit_at = None
        found_any = False
        for mid in mids:
            agg = agg_by_member.get(int(mid))
            if not agg:
                continue
            found_any = True
            total_hours += float(agg.total_hours or 0.0)
            visit_days += int(agg.visit_days or 0)
            if isinstance(agg.last_visit_at, datetime):
                if last_visit_at is None or agg.last_visit_at > last_visit_at:
                    last_visit_at = agg.last_visit_at

        status = "OK"
        if len(mids) > 1:
            status = "MULTI_MEMBER_ID"
        if not found_any:
            status = "NO_SESSIONS"

        out_rows.append(
            {
                "rank": "",
                "name": r.name,
                "phone_raw": r.phone_raw,
                "total_hours": total_hours,
                "visit_days": visit_days,
                "last_visit_at": last_visit_at,
                "member_ids_str": ",".join([str(x) for x in mids]),
                "status": status,
            }
        )

    out_rows_sorted = sorted(out_rows, key=lambda x: float(x.get("total_hours") or 0.0), reverse=True)
    for i, r in enumerate(out_rows_sorted, start=1):
        r["rank"] = i

    today = datetime.now(tz).date().isoformat()
    base_dir = Path(args.out).parent if args.out else Path(__file__).parent
    out_path = Path(args.out) if args.out else base_dir / f"loyal_customers_total_hours_for_list_{today}.csv"
    _write_report(out_path, tz=tz, rows=out_rows_sorted)
    print(str(out_path))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

