"""`xq` CLI — 客户端。

0.1 命令：
    xq health
    xq register <phone>
    xq verify <code> --request-id <ULID>
    xq logout
    xq me
"""

from __future__ import annotations

import os

import click
import httpx

from . import __version__, client_session, version_check

DEFAULT_ENDPOINT = os.environ.get("XIANGQIN_ENDPOINT", "http://112.124.27.213")


@click.group()
@click.version_option(__version__, prog_name="xq")
@click.option(
    "--endpoint",
    default=None,
    envvar="XIANGQIN_ENDPOINT",
    help="xiangqin 服务端 URL（默认读本地 session 或 http://112.124.27.213）",
)
@click.pass_context
def cli(ctx: click.Context, endpoint: str | None) -> None:
    """xiangqin 客户端 — 相亲 + 私信。"""
    ctx.ensure_object(dict)
    version_check.print_update_notice_if_needed()
    sess = client_session.load()
    if endpoint:
        ctx.obj["endpoint"] = endpoint.rstrip("/")
    elif sess and sess.get("endpoint"):
        ctx.obj["endpoint"] = sess["endpoint"]
    else:
        ctx.obj["endpoint"] = DEFAULT_ENDPOINT
    ctx.obj["session"] = sess


def _client(endpoint: str, token: str | None = None) -> httpx.Client:
    headers = {}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    return httpx.Client(base_url=endpoint, timeout=10.0, headers=headers, trust_env=False)


def _require_session(ctx: click.Context) -> dict[str, str]:
    sess = ctx.obj.get("session")
    if not sess or not sess.get("session_token"):
        click.echo("未登录，先 `xq register <phone>`", err=True)
        raise SystemExit(1)
    return sess


# ========== 基础 ==========


@cli.command()
@click.pass_context
def health(ctx: click.Context) -> None:
    """健康检查。"""
    with _client(ctx.obj["endpoint"]) as c:
        try:
            r = c.get("/health")
            r.raise_for_status()
            click.echo(r.text)
        except httpx.HTTPError as e:
            click.echo(f"health check failed: {e}", err=True)
            raise SystemExit(1) from e


# ========== auth ==========


@cli.command()
@click.argument("phone")
@click.pass_context
def register(ctx: click.Context, phone: str) -> None:
    """发起注册 / 登录（返回 request_id，0.1 期 mock code = 123456）。"""
    with _client(ctx.obj["endpoint"]) as c:
        r = c.post("/auth/register", json={"phone": phone})
    if r.status_code != 200:
        click.echo(f"register failed: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    data = r.json()
    # 记住 masked_phone 给下一步 verify 做上下文
    ctx.obj["__last_phone_masked"] = data["masked_phone"]
    click.echo(f"验证码已发 {data['masked_phone']}，{data['expires_in_s']}s 内有效")
    click.echo(f"request_id: {data['request_id']}")
    click.echo(f"提示：{data.get('mock_code_hint', '')}")
    # 临时把 register 产生的 masked_phone 暂存（给 verify 显示）
    os.environ["_XIANGQIN_LAST_MASKED"] = data["masked_phone"]


@cli.command()
@click.argument("code")
@click.option("--request-id", required=True, help="register 命令返回的 ULID")
@click.pass_context
def verify(ctx: click.Context, code: str, request_id: str) -> None:
    """用 request_id + 验证码换 session。"""
    with _client(ctx.obj["endpoint"]) as c:
        r = c.post("/auth/verify", json={"request_id": request_id, "code": code})
    if r.status_code != 200:
        click.echo(f"verify failed: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    data = r.json()
    masked = os.environ.get("_XIANGQIN_LAST_MASKED", "***")
    client_session.save(
        session_token=data["session_token"],
        user_id=data["user_id"],
        endpoint=ctx.obj["endpoint"],
        masked_phone=masked,
    )
    tag = "新用户" if data["is_new_user"] else "老用户"
    click.echo(f"登录成功（{tag}），user_id={data['user_id']}")
    click.echo("session 已写本机 ~/.xiangqin/session.json")


@cli.command()
@click.pass_context
def logout(ctx: click.Context) -> None:
    """登出（删本机 session + 服务端 session 行）。"""
    sess = ctx.obj.get("session")
    if sess and sess.get("session_token"):
        try:
            with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
                c.post("/auth/logout")
        except httpx.HTTPError:
            pass  # 本地清理无论如何都走
    client_session.clear()
    click.echo("已登出")


@cli.command()
@click.pass_context
def me(ctx: click.Context) -> None:
    """查当前登录态。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.get("/auth/me")
    if r.status_code != 200:
        click.echo(f"me failed: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    data = r.json()
    click.echo(f"user_id: {data['user_id']}")
    click.echo(f"phone:   {sess.get('masked_phone', '***')}")
    click.echo(f"endpoint: {ctx.obj['endpoint']}")


# ========== profile ==========


@cli.group()
def profile() -> None:
    """查 / 改个人资料。"""


@profile.command("show")
@click.pass_context
def profile_show(ctx: click.Context) -> None:
    """查看当前用户资料。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.get("/profile/me")
    if r.status_code != 200:
        click.echo(f"profile show failed: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    data = r.json()
    click.echo(f"user_id: {data['user_id']}")
    click.echo(f"gender:  {data.get('gender') or '（未填）'}")
    click.echo(f"age:     {data.get('age') or '（未填）'}")
    click.echo(f"city:    {data.get('city') or '（未填）'}")
    tags = data.get("tags") or []
    click.echo(f"tags:    {', '.join(tags) if tags else '（未填）'}")
    bio = data.get("bio")
    click.echo(f"bio:     {bio or '（未填）'}")


@profile.command("set")
@click.argument("field")
@click.argument("value")
@click.pass_context
def profile_set(ctx: click.Context, field: str, value: str) -> None:
    """改单个字段。

    字段：gender/age/city/tags/bio/height/education/agent_gateway_url/agent_hooks_token/notify_on_new_mail

    示例：
        xq profile set tags 程序,登山,做饭
        xq profile set height 175
        xq profile set education bachelor
        xq profile set agent_gateway_url https://b-xxx.example.com
        xq profile set notify_on_new_mail on
    """
    sess = _require_session(ctx)
    payload: dict[str, object] = {}
    if field == "age":
        try:
            payload["age"] = int(value)
        except ValueError as e:
            click.echo(f"age 必须是整数，收到 {value!r}", err=True)
            raise SystemExit(1) from e
    elif field == "height":
        try:
            payload["height"] = int(value)
        except ValueError as e:
            click.echo("height 必须是整数（cm）", err=True)
            raise SystemExit(1) from e
    elif field == "tags":
        payload["tags"] = [t.strip() for t in value.split(",") if t.strip()]
    elif field == "notify_on_new_mail":
        if value.lower() in {"on", "true", "1", "yes"}:
            payload["notify_on_new_mail"] = True
        elif value.lower() in {"off", "false", "0", "no"}:
            payload["notify_on_new_mail"] = False
        else:
            click.echo(f"notify_on_new_mail 请传 on/off，收到 {value!r}", err=True)
            raise SystemExit(1)
    elif field in {"gender", "city", "bio", "education", "agent_gateway_url", "agent_hooks_token"}:
        payload[field] = value
    else:
        click.echo(
            f"未知字段：{field}（允许 gender/age/city/tags/bio/height/education/"
            "agent_gateway_url/agent_hooks_token/notify_on_new_mail）",
            err=True,
        )
        raise SystemExit(1)

    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.put("/profile/me", json=payload)
    if r.status_code != 200:
        click.echo(f"profile set failed: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    click.echo(f"ok: {field} 已更新")


@profile.command("clear")
@click.argument("field", type=click.Choice(["tags", "bio"]))
@click.pass_context
def profile_clear(ctx: click.Context, field: str) -> None:
    """清空可空字段（只支持 tags / bio；gender/age/city 必填不让清）。"""
    sess = _require_session(ctx)
    payload: dict[str, object] = {field: [] if field == "tags" else None}
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.put("/profile/me", json=payload)
    if r.status_code != 200:
        click.echo(f"profile clear failed: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    click.echo(f"ok: {field} 已清空")


# ========== query ==========


@cli.command()
@click.argument("where")
@click.option("--limit", default=50, show_default=True, type=int, help="最多返回几条（1-100）")
@click.option("--json", "as_json", is_flag=True, help="原始 JSON 输出")
@click.pass_context
def query(ctx: click.Context, where: str, limit: int, as_json: bool) -> None:
    """查匹配。受限 DSL，例：xq query 'gender=f AND city=hangzhou AND age>=25'

    付费用户带 🔥 并置顶。每次命中扣 1 次他们的曝光余额。
    """
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.post("/query", json={"where": where, "limit": limit})
    if r.status_code != 200:
        click.echo(f"query failed: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    data = r.json()
    if as_json:
        import json as _json

        click.echo(_json.dumps(data, ensure_ascii=False, indent=2))
        return

    hits = data["results"]
    if not hits:
        click.echo("0 条命中")
        return

    click.echo(f"{data['total_hits']} 条命中")
    click.echo("-" * 60)
    for h in hits:
        tags_s = ", ".join(h.get("tags") or [])
        fields = [
            h.get("gender") or "?",
            str(h.get("age") or "?"),
            h.get("city") or "?",
        ]
        if h.get("height"):
            fields.append(f"{h['height']}cm")
        if h.get("education"):
            fields.append(h["education"])
        click.echo(f"{h['user_id']}  {'  '.join(fields)}")
        if tags_s:
            click.echo(f"   tags: {tags_s}")
        if h.get("bio"):
            click.echo(f"   bio:  {h['bio']}")


# ========== mail / 私信（M4）==========


def _fmt_time(ts: int | None) -> str:
    if ts is None:
        return "-"
    import datetime as _dt

    return _dt.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M")


def _fmt_status(m: dict) -> str:
    """压缩版状态展示：投递 + 通知 + 阅读 + 回应 + 处置。"""
    parts = []
    ds = m.get("delivery_status")
    if ds != "delivered":
        parts.append(f"投递={ds}")
        return " ".join(parts)
    ns = m.get("notify_status") or "-"
    parts.append(f"通知={ns}")
    parts.append(f"阅读={'已读' if m.get('read_at') else '未读'}")
    if m.get("replied_at"):
        parts.append("已回复")
    if m.get("deleted_by_recipient_at"):
        parts.append("对方已删")
    return " ".join(parts)


@cli.group()
def dm() -> None:
    """私信（发 / 查发件 / 查详情）。"""


@dm.command("send")
@click.argument("to_user_id")
@click.argument("body")
@click.pass_context
def dm_send(ctx: click.Context, to_user_id: str, body: str) -> None:
    """给对方发私信（消耗 1 个信封）。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.post("/dm/send", json={"to_user_id": to_user_id, "body": body})
    if r.status_code != 200:
        click.echo(f"发信失败: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    m = r.json()
    src = "免费额度" if m["envelope_source"] == "free_daily" else "付费信封"
    click.echo(f"✓ 已投递（{src}）。msg={m['id']}")
    if m.get("notify_status"):
        click.echo(f"  通知：{m['notify_status']}")


@dm.command("sent")
@click.option("--limit", default=20, type=int, show_default=True)
@click.pass_context
def dm_sent(ctx: click.Context, limit: int) -> None:
    """查我发出去的私信。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.get(f"/dm/sent?limit={limit}")
    if r.status_code != 200:
        click.echo(f"查询失败: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    msgs = r.json()
    if not msgs:
        click.echo("暂无发信记录")
        return
    for m in msgs:
        click.echo(f"{_fmt_time(m['sent_at'])}  → {m['to_user_id']}  {_fmt_status(m)}  {m['id']}")


@dm.command("show")
@click.argument("message_id")
@click.pass_context
def dm_show(ctx: click.Context, message_id: str) -> None:
    """查单条详情（5 维状态）。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.get(f"/dm/message/{message_id}")
    if r.status_code != 200:
        click.echo(f"查询失败: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    m = r.json()
    click.echo(f"ID:        {m['id']}")
    click.echo(f"From:      {m['from_user_id']}")
    click.echo(f"To:        {m['to_user_id']}")
    click.echo(f"Sent:      {_fmt_time(m['sent_at'])}")
    click.echo(f"Delivery:  {m['delivery_status']}")
    if m.get("delivery_failure_reason"):
        click.echo(f"  reason:  {m['delivery_failure_reason']}")
    click.echo(
        f"Notify:    {m.get('notify_status') or '-'} ({_fmt_time(m.get('notify_attempted_at'))})"
    )
    if m.get("notify_error"):
        click.echo(f"  error:   {m['notify_error']}")
    click.echo(f"Read:      {_fmt_time(m.get('read_at'))}")
    click.echo(f"Replied:   {_fmt_time(m.get('replied_at'))}")
    click.echo(f"Envelope:  {m['envelope_source']}")
    click.echo(f"Expires:   {_fmt_time(m['expires_at'])}")
    click.echo("Body:")
    click.echo(f"  {m['body']}")


@cli.group()
def inbox() -> None:
    """收件箱（查收 / 看 / 回复 / 删 / 举报）。"""


@inbox.command("list")
@click.option("--limit", default=20, type=int, show_default=True)
@click.pass_context
def inbox_list_cmd(ctx: click.Context, limit: int) -> None:
    """查收件箱（平铺，按时间倒序）。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.get(f"/inbox?limit={limit}")
    if r.status_code != 200:
        click.echo(f"查询失败: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    msgs = r.json()
    if not msgs:
        click.echo("收件箱空")
        return
    for m in msgs:
        mark = " " if m.get("read_at") else "●"
        preview = (m["body"][:40] + "...") if len(m["body"]) > 40 else m["body"]
        click.echo(f"{mark} {_fmt_time(m['sent_at'])}  ← {m['from_user_id']}  {m['id']}")
        click.echo(f"    {preview}")


@inbox.command("show")
@click.argument("message_id")
@click.pass_context
def inbox_show(ctx: click.Context, message_id: str) -> None:
    """查看单条并标为已读。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.post(f"/inbox/{message_id}/read")
    if r.status_code != 200:
        click.echo(f"查询失败: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    m = r.json()
    click.echo(f"From:    {m['from_user_id']}")
    click.echo(f"Sent:    {_fmt_time(m['sent_at'])}")
    click.echo("Body:")
    click.echo(f"  {m['body']}")
    click.echo("")
    click.echo("下一步：")
    click.echo(f'  xq inbox reply {message_id} "..."')
    click.echo(f"  xq inbox report {message_id} --reason '骚扰'")
    click.echo(f"  xq block {m['from_user_id']}")


@inbox.command("reply")
@click.argument("message_id")
@click.argument("body")
@click.pass_context
def inbox_reply_cmd(ctx: click.Context, message_id: str, body: str) -> None:
    """回信（消耗 1 个信封）。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.post(f"/inbox/{message_id}/reply", json={"body": body})
    if r.status_code != 200:
        click.echo(f"回复失败: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    m = r.json()
    click.echo(f"✓ 已回复。msg={m['id']}")


@inbox.command("delete")
@click.argument("message_id")
@click.pass_context
def inbox_delete_cmd(ctx: click.Context, message_id: str) -> None:
    """软删（服务端仍保留 30 天）。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.delete(f"/inbox/{message_id}")
    if r.status_code != 200:
        click.echo(f"删除失败: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    click.echo("已删除")


@inbox.command("report")
@click.argument("message_id")
@click.option("--reason", default=None, help="举报原因")
@click.pass_context
def inbox_report_cmd(ctx: click.Context, message_id: str, reason: str | None) -> None:
    """举报一条私信（留档取证）。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.post(f"/inbox/{message_id}/report", json={"reason": reason})
    if r.status_code != 200:
        click.echo(f"举报失败: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    d = r.json()
    click.echo(f"✓ 已举报 (report_id={d['id']})。举报人 + 被举报人已留档。")


@cli.group()
def envelope() -> None:
    """信封（查额度 / 购买）。"""


@envelope.command("state")
@click.pass_context
def envelope_state_cmd(ctx: click.Context) -> None:
    """查今日信封余额。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.get("/envelope")
    if r.status_code != 200:
        click.echo(f"查询失败: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    s = r.json()
    click.echo(f"今日免费剩余: {s['free_remaining_today']} / {s['free_per_day']}")
    click.echo(f"付费信封余量: {s['paid_remaining']}")
    click.echo(f"历史累计购买: {s['total_purchased']}")
    click.echo(f"信封单价:    ¥{s['price_cents'] / 100:.2f}")


@envelope.command("buy")
@click.option("--count", required=True, type=int, help="买几个信封（1-10000）")
@click.option("--mock", is_flag=True, help="mock 期必带；真付未接入")
@click.pass_context
def envelope_buy_cmd(ctx: click.Context, count: int, mock: bool) -> None:
    """购买信封。0.1 期仅支持 --mock（真付待接入）。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.post("/envelope/buy", json={"count": count, "mock": mock})
    if r.status_code != 200:
        click.echo(f"购买失败: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    d = r.json()
    yuan = d["amount_cents"] / 100
    click.echo(f"✓ 购买成功。order={d['id']}  买 {d['count']} 封  ¥{yuan:.2f}")


# ========== block / 拉黑 ==========


@cli.command("block")
@click.argument("user_id")
@click.pass_context
def block_cmd(ctx: click.Context, user_id: str) -> None:
    """拉黑某用户（对方后续发给我的信会被拦截）。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.post("/block", json={"user_id": user_id})
    if r.status_code != 200:
        click.echo(f"拉黑失败: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    click.echo(f"✓ 已拉黑 {user_id}")


@cli.command("unblock")
@click.argument("user_id")
@click.pass_context
def unblock_cmd(ctx: click.Context, user_id: str) -> None:
    """解除拉黑。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.delete(f"/block/{user_id}")
    if r.status_code != 200:
        click.echo(f"解除失败: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    click.echo(f"✓ 已解除拉黑 {user_id}")


@cli.command("blocklist")
@click.pass_context
def blocklist_cmd(ctx: click.Context) -> None:
    """查我的拉黑名单。"""
    sess = _require_session(ctx)
    with _client(ctx.obj["endpoint"], sess["session_token"]) as c:
        r = c.get("/blocklist")
    if r.status_code != 200:
        click.echo(f"查询失败: {r.status_code} {r.text}", err=True)
        raise SystemExit(1)
    items = r.json()
    if not items:
        click.echo("拉黑名单空")
        return
    for it in items:
        click.echo(f"{_fmt_time(it['created_at'])}  {it['blocked_id']}")
