"""Insurance customer & policy CLI.

Subcommands cover customer / policy / followup CRUD, gap-analysis,
customer-segment, dashboard, and internal helpers used by sibling skills.
All commands print JSON to stdout: {"ok": true|false, ...}.
"""
from __future__ import annotations

import argparse
import json
import re
import sqlite3
import sys
from datetime import datetime, timedelta
from typing import Any

from insurance_customer_cli import db

try:
    sys.stdout.reconfigure(encoding="utf-8")
    sys.stderr.reconfigure(encoding="utf-8")
except (AttributeError, OSError):
    pass

DATE_FMT = "%Y-%m-%d"


def print_json(obj: Any) -> None:
    print(json.dumps(obj, ensure_ascii=False, indent=2))


def next_id(conn: sqlite3.Connection, prefix: str, table: str) -> str:
    rows = conn.execute(
        f"SELECT id FROM {table} WHERE id GLOB ?", (f"{prefix}[0-9]*",)
    ).fetchall()
    max_n = 0
    for r in rows:
        m = re.match(rf"^{prefix}(\d+)$", r["id"])
        if m:
            max_n = max(max_n, int(m.group(1)))
    return f"{prefix}{max_n + 1:03d}"


def parse_date(s: str | None) -> datetime | None:
    if not s:
        return None
    return datetime.strptime(s.strip()[:10], DATE_FMT)


def add_years(d: datetime, years: int) -> datetime:
    try:
        return d.replace(year=d.year + years)
    except ValueError:
        return d.replace(year=d.year + years, day=28)


def add_months(d: datetime, months: int) -> datetime:
    m = d.month - 1 + months
    y = d.year + m // 12
    nm = m % 12 + 1
    day = min(d.day, [31, 29 if y % 4 == 0 and (y % 100 != 0 or y % 400 == 0) else 28,
                      31, 30, 31, 30, 31, 31, 30, 31, 30, 31][nm - 1])
    return d.replace(year=y, month=nm, day=day)


def derive_dates(effective_date: str, pay_period: str | None) -> tuple[str | None, str | None]:
    """Compute (expire_date, next_pay_date) from effective_date + pay_period.

    Heuristics:
    - "趸交" / "1年" : next_pay_date = effective + 1y, expire = None (开放期/终身保单常见)
    - "N年缴" / "N年" : next_pay_date = effective + 1y; expire = effective + Ny
    - 其他 / 留空 : 保守地把 next_pay_date 设为 effective + 1y。
    """
    eff = parse_date(effective_date)
    if eff is None:
        return None, None
    next_pay = add_years(eff, 1).strftime(DATE_FMT)
    expire: str | None = None
    if pay_period:
        m = re.match(r"^\s*(\d+)\s*年", pay_period)
        if m:
            expire = add_years(eff, int(m.group(1))).strftime(DATE_FMT)
        elif "趸" in pay_period:
            expire = None
    return expire, next_pay


# ---------- customer commands ----------

def cmd_customer_add(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    cid = args.customer_id or next_id(conn, "C", "customers")
    household = args.household or "{}"
    tags = args.tags or "[]"
    fields = args.fields or "{}"
    try:
        json.loads(household)
        json.loads(tags)
        json.loads(fields)
    except json.JSONDecodeError as e:
        print_json({"ok": False, "error": "bad_json", "hint": str(e)})
        return 1
    conn.execute(
        """INSERT INTO customers
           (id, name, phone, age, gender, income, household_json, occupation,
            birthday, tags_json, fields_json)
           VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
        (
            cid,
            args.name,
            args.phone or "",
            args.age,
            args.gender or "",
            args.income,
            household,
            args.occupation or "",
            args.birthday or "",
            tags,
            fields,
        ),
    )
    conn.commit()
    print_json({"ok": True, "id": cid, "name": args.name})
    return 0


def _row_customer(row: sqlite3.Row, conn: sqlite3.Connection | None = None) -> dict:
    d = dict(row)
    for jk in ("household_json", "tags_json", "fields_json"):
        if jk in d and isinstance(d[jk], str):
            try:
                d[jk[: -len("_json")]] = json.loads(d[jk])
            except json.JSONDecodeError:
                d[jk[: -len("_json")]] = None
    if conn is not None:
        cnt = conn.execute(
            "SELECT COUNT(*) AS n FROM policies WHERE customer_id=? AND status='active'",
            (d["id"],),
        ).fetchone()
        d["policy_count"] = int(cnt["n"]) if cnt else 0
        last = conn.execute(
            "SELECT created_at FROM followups WHERE customer_id=? ORDER BY created_at DESC LIMIT 1",
            (d["id"],),
        ).fetchone()
        d["last_followup_at"] = last["created_at"] if last else None
    return d


def cmd_customer_query(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    cur = conn.cursor()
    rows: list[sqlite3.Row]
    if args.customer_id:
        rows = list(
            cur.execute(
                "SELECT * FROM customers WHERE id=? AND status='active'",
                (args.customer_id,),
            )
        )
    elif args.keyword:
        kw = f"%{args.keyword}%"
        rows = list(
            cur.execute(
                "SELECT * FROM customers WHERE status='active' AND (name LIKE ? OR phone LIKE ?) ORDER BY created_at DESC",
                (kw, kw),
            )
        )
    else:
        rows = list(cur.execute("SELECT * FROM customers WHERE status='active' ORDER BY created_at DESC"))
    out = [_row_customer(r, conn) for r in rows]
    print_json({"ok": True, "customers": out, "count": len(out)})
    return 0


_CUSTOMER_UPDATE_FIELDS = {
    "name": "name",
    "phone": "phone",
    "age": "age",
    "gender": "gender",
    "income": "income",
    "occupation": "occupation",
    "birthday": "birthday",
    "household": "household_json",
    "tags": "tags_json",
    "fields": "fields_json",
    "children": None,
    "spouse": None,
    "elderly": None,
}


def cmd_customer_update(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    try:
        payload = json.loads(args.set)
    except json.JSONDecodeError as e:
        print_json({"ok": False, "error": "bad_json", "hint": str(e)})
        return 1
    row = conn.execute(
        "SELECT household_json FROM customers WHERE id=? AND status='active'",
        (args.customer_id,),
    ).fetchone()
    if not row:
        print_json({"ok": False, "error": "customer_not_found"})
        return 1
    sets: list[str] = []
    vals: list[Any] = []
    household_now = json.loads(row["household_json"] or "{}")
    household_dirty = False
    for k, v in payload.items():
        if k in ("children", "spouse", "elderly"):
            household_now[k] = v
            household_dirty = True
            continue
        col = _CUSTOMER_UPDATE_FIELDS.get(k)
        if col is None:
            continue
        if col.endswith("_json"):
            sets.append(f"{col}=?")
            vals.append(json.dumps(v, ensure_ascii=False) if not isinstance(v, str) else v)
        else:
            sets.append(f"{col}=?")
            vals.append(v)
    if household_dirty:
        sets.append("household_json=?")
        vals.append(json.dumps(household_now, ensure_ascii=False))
    if sets:
        sets.append("updated_at=datetime('now','localtime')")
        vals.append(args.customer_id)
        conn.execute(f"UPDATE customers SET {', '.join(sets)} WHERE id=?", vals)
        conn.commit()
    print_json({"ok": True, "id": args.customer_id})
    return 0


def cmd_customer_delete(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    row = conn.execute(
        "SELECT id FROM customers WHERE id=? AND status='active'",
        (args.customer_id,),
    ).fetchone()
    if not row:
        print_json({"ok": False, "error": "customer_not_found"})
        return 1
    cnt = conn.execute(
        "SELECT COUNT(*) AS n FROM policies WHERE customer_id=? AND status='active'",
        (args.customer_id,),
    ).fetchone()
    if int(cnt["n"]) > 0:
        print_json({"ok": False, "error": "has_policies", "policy_count": int(cnt["n"])})
        return 1
    if not args.yes:
        print_json({"ok": False, "error": "need_confirm", "hint": "请加 --yes 确认删除"})
        return 1
    conn.execute(
        "UPDATE customers SET status='deleted', updated_at=datetime('now','localtime') WHERE id=?",
        (args.customer_id,),
    )
    conn.commit()
    print_json({"ok": True})
    return 0


def cmd_customer_exists(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    row = conn.execute(
        "SELECT id FROM customers WHERE id=? AND status='active'",
        (args.customer_id,),
    ).fetchone()
    print_json({"ok": True, "exists": bool(row)})
    return 0


# ---------- policy commands ----------

def cmd_policy_add(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    cust = conn.execute(
        "SELECT id FROM customers WHERE id=? AND status='active'", (args.customer_id,)
    ).fetchone()
    if not cust:
        print_json({"ok": False, "error": "customer_not_found"})
        return 1
    pid = args.policy_id or next_id(conn, "P", "policies")
    expire = args.expire_date
    next_pay = args.next_pay_date
    if not expire or not next_pay:
        de, dn = derive_dates(args.effective_date, args.pay_period)
        expire = expire or de
        next_pay = next_pay or dn
    conn.execute(
        """INSERT INTO policies
           (id, customer_id, company, product_name, product_type, coverage, premium,
            pay_period, effective_date, expire_date, next_pay_date,
            insured_name, beneficiary, notes)
           VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
        (
            pid,
            args.customer_id,
            args.company,
            args.product,
            args.product_type,
            float(args.coverage),
            float(args.premium),
            args.pay_period or "",
            args.effective_date,
            expire or "",
            next_pay or "",
            args.insured or "",
            args.beneficiary or "",
            args.notes or "",
        ),
    )
    conn.commit()
    print_json(
        {
            "ok": True,
            "id": pid,
            "customer_id": args.customer_id,
            "expire_date": expire,
            "next_pay_date": next_pay,
        }
    )
    return 0


def cmd_policy_query(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    cur = conn.cursor()
    rows: list[sqlite3.Row]
    if args.policy_id:
        rows = list(cur.execute("SELECT * FROM policies WHERE id=? AND status='active'", (args.policy_id,)))
    elif args.customer_id:
        rows = list(
            cur.execute(
                "SELECT * FROM policies WHERE customer_id=? AND status='active' ORDER BY effective_date DESC",
                (args.customer_id,),
            )
        )
    elif args.expire_before:
        rows = list(
            cur.execute(
                "SELECT * FROM policies WHERE status='active' AND expire_date != '' AND expire_date <= ? ORDER BY expire_date",
                (args.expire_before,),
            )
        )
    elif args.next_pay_before:
        rows = list(
            cur.execute(
                "SELECT * FROM policies WHERE status='active' AND next_pay_date != '' AND next_pay_date <= ? ORDER BY next_pay_date",
                (args.next_pay_before,),
            )
        )
    elif args.product_type:
        rows = list(
            cur.execute(
                "SELECT * FROM policies WHERE status='active' AND product_type=? ORDER BY effective_date DESC",
                (args.product_type,),
            )
        )
    else:
        rows = list(cur.execute("SELECT * FROM policies WHERE status='active' ORDER BY effective_date DESC"))
    print_json({"ok": True, "policies": [dict(r) for r in rows], "count": len(rows)})
    return 0


_POLICY_UPDATE_FIELDS = {
    "company": "company",
    "product_name": "product_name",
    "product_type": "product_type",
    "coverage": "coverage",
    "premium": "premium",
    "pay_period": "pay_period",
    "effective_date": "effective_date",
    "expire_date": "expire_date",
    "next_pay_date": "next_pay_date",
    "insured_name": "insured_name",
    "beneficiary": "beneficiary",
    "notes": "notes",
}


def cmd_policy_update(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    try:
        payload = json.loads(args.set)
    except json.JSONDecodeError as e:
        print_json({"ok": False, "error": "bad_json", "hint": str(e)})
        return 1
    row = conn.execute("SELECT id FROM policies WHERE id=? AND status='active'", (args.policy_id,)).fetchone()
    if not row:
        print_json({"ok": False, "error": "policy_not_found"})
        return 1
    sets: list[str] = []
    vals: list[Any] = []
    for k, v in payload.items():
        col = _POLICY_UPDATE_FIELDS.get(k)
        if col is None:
            continue
        sets.append(f"{col}=?")
        vals.append(v)
    if sets:
        sets.append("updated_at=datetime('now','localtime')")
        vals.append(args.policy_id)
        conn.execute(f"UPDATE policies SET {', '.join(sets)} WHERE id=?", vals)
        conn.commit()
    print_json({"ok": True, "id": args.policy_id})
    return 0


def cmd_policy_delete(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    row = conn.execute("SELECT id FROM policies WHERE id=? AND status='active'", (args.policy_id,)).fetchone()
    if not row:
        print_json({"ok": False, "error": "policy_not_found"})
        return 1
    if not args.yes:
        print_json({"ok": False, "error": "need_confirm", "hint": "请加 --yes 确认删除"})
        return 1
    conn.execute(
        "UPDATE policies SET status='cancelled', updated_at=datetime('now','localtime') WHERE id=?",
        (args.policy_id,),
    )
    conn.commit()
    print_json({"ok": True})
    return 0


def cmd_policy_count_by_customer(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    row = conn.execute(
        "SELECT COUNT(*) AS n FROM policies WHERE customer_id=? AND status='active'",
        (args.customer_id,),
    ).fetchone()
    print_json({"ok": True, "customer_id": args.customer_id, "count": int(row["n"])})
    return 0


# ---------- followup commands ----------

def cmd_followup_add(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    cust = conn.execute(
        "SELECT id FROM customers WHERE id=? AND status='active'", (args.customer_id,)
    ).fetchone()
    if not cust:
        print_json({"ok": False, "error": "customer_not_found"})
        return 1
    cur = conn.execute(
        """INSERT INTO followups (customer_id, content, next_step, next_date, channel)
           VALUES (?,?,?,?,?)""",
        (
            args.customer_id,
            args.content,
            args.next_step or "",
            args.next_date or "",
            args.channel or "",
        ),
    )
    conn.commit()
    print_json({"ok": True, "id": cur.lastrowid, "customer_id": args.customer_id})
    return 0


def cmd_followup_query(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    cur = conn.cursor()
    rows: list[sqlite3.Row]
    if args.customer_id:
        rows = list(
            cur.execute(
                "SELECT * FROM followups WHERE customer_id=? ORDER BY created_at DESC",
                (args.customer_id,),
            )
        )
    elif args.days is not None:
        threshold = (datetime.now() - timedelta(days=int(args.days))).strftime("%Y-%m-%d %H:%M:%S")
        rows = list(
            cur.execute(
                "SELECT * FROM followups WHERE created_at >= ? ORDER BY created_at DESC",
                (threshold,),
            )
        )
    else:
        rows = list(cur.execute("SELECT * FROM followups ORDER BY created_at DESC LIMIT 100"))
    print_json({"ok": True, "followups": [dict(r) for r in rows], "count": len(rows)})
    return 0


# ---------- gap-analysis ----------

GAP_TYPES = ["重疾险", "医疗险", "意外险", "寿险", "养老险", "教育金"]


def _gap_for_customer(conn: sqlite3.Connection, customer_id: str) -> dict:
    cust = conn.execute(
        "SELECT id, name, household_json, age, income FROM customers WHERE id=? AND status='active'",
        (customer_id,),
    ).fetchone()
    if not cust:
        return {"customer_id": customer_id, "found": False}
    rows = conn.execute(
        "SELECT product_type FROM policies WHERE customer_id=? AND status='active'",
        (customer_id,),
    ).fetchall()
    have = {r["product_type"] for r in rows}
    has_children = False
    try:
        hh = json.loads(cust["household_json"] or "{}")
        kids = hh.get("children")
        has_children = bool(kids) and (kids is True or (isinstance(kids, (int, float)) and kids > 0))
    except json.JSONDecodeError:
        pass
    coverage = {}
    gaps: list[str] = []
    for t in GAP_TYPES:
        present = t in have
        coverage[t] = present
        if not present:
            if t == "教育金" and not has_children:
                continue
            gaps.append(t)
    return {
        "customer_id": cust["id"],
        "name": cust["name"],
        "found": True,
        "coverage": coverage,
        "gaps": gaps,
        "has_children": has_children,
    }


def cmd_gap_analysis(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    if args.customer_id:
        result = _gap_for_customer(conn, args.customer_id)
        if not result.get("found"):
            print_json({"ok": False, "error": "customer_not_found"})
            return 1
        print_json({"ok": True, "result": result})
        return 0
    rows = conn.execute("SELECT id FROM customers WHERE status='active'").fetchall()
    results = [_gap_for_customer(conn, r["id"]) for r in rows]
    if args.gap_type:
        results = [r for r in results if r.get("found") and args.gap_type in r["gaps"]]
    print_json({"ok": True, "results": results, "count": len(results)})
    return 0


# ---------- customer-segment ----------

def _premium_total(conn: sqlite3.Connection, customer_id: str) -> float:
    row = conn.execute(
        "SELECT COALESCE(SUM(premium),0) AS s FROM policies WHERE customer_id=? AND status='active'",
        (customer_id,),
    ).fetchone()
    return float(row["s"] or 0)


def _last_followup_at(conn: sqlite3.Connection, customer_id: str) -> str | None:
    row = conn.execute(
        "SELECT created_at FROM followups WHERE customer_id=? ORDER BY created_at DESC LIMIT 1",
        (customer_id,),
    ).fetchone()
    return row["created_at"] if row else None


def cmd_customer_segment(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    now = datetime.now()
    rows = conn.execute("SELECT id, name, created_at FROM customers WHERE status='active'").fetchall()
    hi_value: list[dict] = []
    active: list[dict] = []
    dormant: list[dict] = []
    new_clients: list[dict] = []
    for r in rows:
        cid = r["id"]
        name = r["name"]
        premium = _premium_total(conn, cid)
        last = _last_followup_at(conn, cid)
        last_dt = None
        if last:
            try:
                last_dt = datetime.strptime(last[:19], "%Y-%m-%d %H:%M:%S")
            except ValueError:
                last_dt = None
        days_since = (now - last_dt).days if last_dt else None
        try:
            created_dt = datetime.strptime(r["created_at"][:19], "%Y-%m-%d %H:%M:%S")
        except (ValueError, TypeError):
            created_dt = None
        item = {
            "customer_id": cid,
            "name": name,
            "premium_total": premium,
            "last_followup_at": last,
            "days_since_followup": days_since,
        }
        if premium > 50000:
            hi_value.append(item)
        if days_since is not None and days_since <= 30:
            active.append(item)
        if days_since is not None and days_since > 60:
            dormant.append(item)
        elif days_since is None and created_dt and (now - created_dt).days > 60:
            dormant.append(item)
        if created_dt and (now - created_dt).days <= 30:
            new_clients.append(item)
    print_json(
        {
            "ok": True,
            "hi_value": hi_value,
            "active": active,
            "dormant": dormant,
            "new": new_clients,
            "summary": {
                "total": len(rows),
                "hi_value": len(hi_value),
                "active": len(active),
                "dormant": len(dormant),
                "new": len(new_clients),
            },
        }
    )
    return 0


# ---------- dashboard ----------

def cmd_dashboard(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    cust_total = int(conn.execute("SELECT COUNT(*) AS n FROM customers WHERE status='active'").fetchone()["n"])
    pol_row = conn.execute(
        "SELECT COUNT(*) AS n, COALESCE(SUM(premium),0) AS s FROM policies WHERE status='active'"
    ).fetchone()
    pol_total = int(pol_row["n"])
    premium_total = float(pol_row["s"] or 0)
    now = datetime.now()
    seg_rows = conn.execute("SELECT id, created_at FROM customers WHERE status='active'").fetchall()
    hi = active = dormant = new_n = 0
    for r in seg_rows:
        cid = r["id"]
        premium = _premium_total(conn, cid)
        last = _last_followup_at(conn, cid)
        last_dt = None
        if last:
            try:
                last_dt = datetime.strptime(last[:19], "%Y-%m-%d %H:%M:%S")
            except ValueError:
                last_dt = None
        days_since = (now - last_dt).days if last_dt else None
        try:
            created_dt = datetime.strptime(r["created_at"][:19], "%Y-%m-%d %H:%M:%S")
        except (ValueError, TypeError):
            created_dt = None
        if premium > 50000:
            hi += 1
        if days_since is not None and days_since <= 30:
            active += 1
        if (days_since is not None and days_since > 60) or (
            days_since is None and created_dt and (now - created_dt).days > 60
        ):
            dormant += 1
        if created_dt and (now - created_dt).days <= 30:
            new_n += 1
    print_json(
        {
            "ok": True,
            "skill": "insurance-customer-policy",
            "customers_total": cust_total,
            "hi_value": hi,
            "active": active,
            "dormant": dormant,
            "new": new_n,
            "policies_total": pol_total,
            "total_premium": premium_total,
        }
    )
    return 0


# ---------- custom field commands ----------

def cmd_customer_field_add(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    conn.execute(
        "INSERT OR REPLACE INTO customer_fields (name, type) VALUES (?,?)",
        (args.name, args.type or "text"),
    )
    conn.commit()
    print_json({"ok": True, "name": args.name, "type": args.type or "text"})
    return 0


def cmd_customer_field_query(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
    rows = conn.execute("SELECT name, type FROM customer_fields ORDER BY name").fetchall()
    print_json({"ok": True, "fields": [dict(r) for r in rows]})
    return 0


# ---------- argparse ----------

def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(prog="insurance_customer_cli")
    sub = p.add_subparsers(dest="cmd", required=True)

    a = sub.add_parser("customer-add")
    a.add_argument("--customer-id")
    a.add_argument("--name", required=True)
    a.add_argument("--phone")
    a.add_argument("--age", type=int)
    a.add_argument("--gender")
    a.add_argument("--income", type=float)
    a.add_argument("--household")
    a.add_argument("--occupation")
    a.add_argument("--birthday")
    a.add_argument("--tags")
    a.add_argument("--fields")
    a.set_defaults(func=cmd_customer_add)

    a = sub.add_parser("customer-query")
    a.add_argument("--customer-id")
    a.add_argument("--keyword")
    a.set_defaults(func=cmd_customer_query)

    a = sub.add_parser("customer-update")
    a.add_argument("--customer-id", required=True)
    a.add_argument("--set", required=True)
    a.set_defaults(func=cmd_customer_update)

    a = sub.add_parser("customer-delete")
    a.add_argument("--customer-id", required=True)
    a.add_argument("--yes", action="store_true")
    a.set_defaults(func=cmd_customer_delete)

    a = sub.add_parser("customer-exists")
    a.add_argument("--customer-id", required=True)
    a.add_argument("--json", action="store_true")
    a.set_defaults(func=cmd_customer_exists)

    a = sub.add_parser("policy-add")
    a.add_argument("--customer-id", required=True)
    a.add_argument("--policy-id")
    a.add_argument("--company", required=True)
    a.add_argument("--product", required=True)
    a.add_argument("--product-type", "--type", dest="product_type", required=True)
    a.add_argument("--coverage", required=True)
    a.add_argument("--premium", required=True)
    a.add_argument("--pay-period")
    a.add_argument("--effective-date", required=True)
    a.add_argument("--expire-date")
    a.add_argument("--next-pay-date")
    a.add_argument("--insured")
    a.add_argument("--beneficiary")
    a.add_argument("--notes")
    a.set_defaults(func=cmd_policy_add)

    a = sub.add_parser("policy-query")
    a.add_argument("--policy-id")
    a.add_argument("--customer-id")
    a.add_argument("--expire-before")
    a.add_argument("--next-pay-before")
    a.add_argument("--product-type")
    a.set_defaults(func=cmd_policy_query)

    a = sub.add_parser("policy-update")
    a.add_argument("--policy-id", required=True)
    a.add_argument("--set", required=True)
    a.set_defaults(func=cmd_policy_update)

    a = sub.add_parser("policy-delete")
    a.add_argument("--policy-id", required=True)
    a.add_argument("--yes", action="store_true")
    a.set_defaults(func=cmd_policy_delete)

    a = sub.add_parser("policy-count-by-customer")
    a.add_argument("--customer-id", required=True)
    a.add_argument("--json", action="store_true")
    a.set_defaults(func=cmd_policy_count_by_customer)

    a = sub.add_parser("followup-add")
    a.add_argument("--customer-id", required=True)
    a.add_argument("--content", required=True)
    a.add_argument("--next-step")
    a.add_argument("--next-date")
    a.add_argument("--channel")
    a.set_defaults(func=cmd_followup_add)

    a = sub.add_parser("followup-query")
    a.add_argument("--customer-id")
    a.add_argument("--days", type=int)
    a.set_defaults(func=cmd_followup_query)

    a = sub.add_parser("gap-analysis")
    a.add_argument("--customer-id")
    a.add_argument("--all", action="store_true")
    a.add_argument("--gap-type")
    a.set_defaults(func=cmd_gap_analysis)

    a = sub.add_parser("customer-segment")
    a.set_defaults(func=cmd_customer_segment)

    a = sub.add_parser("dashboard")
    a.add_argument("--json", action="store_true")
    a.set_defaults(func=cmd_dashboard)

    a = sub.add_parser("customer-field-add")
    a.add_argument("--name", required=True)
    a.add_argument("--type")
    a.set_defaults(func=cmd_customer_field_add)

    a = sub.add_parser("customer-field-query")
    a.set_defaults(func=cmd_customer_field_query)

    return p


def main(argv: list[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)
    conn = db.connect()
    try:
        return int(args.func(conn, args) or 0)
    except sqlite3.Error as e:
        print_json({"ok": False, "error": "sqlite_error", "hint": str(e)})
        return 1
    finally:
        conn.close()


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