"""Profile CRUD + 字段校验。

Profile 字段:
    gender  f / m / nb                          （唯一"不可空"的字段）
    age     18-99 整数                          （唯一"不可空"的字段）
    city    ≤ 30 字符，STR_RE 允许             （唯一"不可空"的字段）
    tags    ≤ 20 个，每个 ≤ 20 字符 STR_RE      （可空）
    bio     ≤ 500 字符                          （可空）

tags 在 DB 以 `|a|b|c|` 形式存（便于 LIKE `%|tag|%`）；业务层 <-> list[str]。
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Any

from .db import get_conn, now_ts

TAG_MAX_LEN = 20
TAGS_MAX_COUNT = 20
CITY_MAX_LEN = 30
BIO_MAX_LEN = 500
GATEWAY_URL_MAX_LEN = 500
HOOKS_TOKEN_MAX_LEN = 500
STR_RE = re.compile(r"^[a-zA-Z0-9_一-龥-]+$")

ALLOWED_GENDERS = {"f", "m", "nb"}
ALLOWED_EDUCATION = {"highschool", "associate", "bachelor", "master", "phd", "other"}


class ProfileError(Exception):
    def __init__(self, code: str, msg: str):
        super().__init__(msg)
        self.code = code


@dataclass
class Profile:
    user_id: str
    gender: str | None
    age: int | None
    city: str | None
    tags: list[str]
    bio: str | None
    height: int | None
    education: str | None
    agent_gateway_url: str | None
    agent_hooks_token_masked: str | None
    notify_on_new_mail: bool
    updated_at: int


_COLUMNS = (
    "user_id, gender, age, city, tags, bio, height, education, "
    "agent_gateway_url, agent_hooks_token, notify_on_new_mail, updated_at"
)


def get(user_id: str) -> Profile:
    with get_conn() as conn:
        row = conn.execute(
            f"SELECT {_COLUMNS} FROM profiles WHERE user_id = ?",
            (user_id,),
        ).fetchone()
    if row is None:
        raise ProfileError("profile_missing", "profile 未初始化（请先 register/verify）")
    return _row_to_profile(row)


def update(user_id: str, fields: dict[str, Any]) -> Profile:
    """部分更新。fields 里允许：gender / age / city / tags / bio。"""
    normalized: dict[str, Any] = {}

    if "gender" in fields:
        g = fields["gender"]
        if g is not None:
            if not isinstance(g, str) or g not in ALLOWED_GENDERS:
                raise ProfileError("invalid_value", f"gender 必须 f/m/nb，收到 {g!r}")
        normalized["gender"] = g

    if "age" in fields:
        a = fields["age"]
        if a is not None:
            if not isinstance(a, int) or a < 18 or a > 99:
                raise ProfileError("invalid_value", f"age 范围 [18, 99]，收到 {a!r}")
        normalized["age"] = a

    if "city" in fields:
        c = fields["city"]
        if c is not None:
            if not isinstance(c, str) or len(c) > CITY_MAX_LEN:
                raise ProfileError("invalid_value", f"city 长度 ≤ {CITY_MAX_LEN}")
            if not STR_RE.match(c):
                raise ProfileError("invalid_value", f"city 包含非法字符：{c!r}")
        normalized["city"] = c

    if "tags" in fields:
        t = fields["tags"]
        if t is None:
            normalized["tags"] = None
        else:
            if not isinstance(t, list):
                raise ProfileError("invalid_value", "tags 必须是列表")
            if len(t) > TAGS_MAX_COUNT:
                raise ProfileError("invalid_value", f"tags 个数 ≤ {TAGS_MAX_COUNT}")
            cleaned: list[str] = []
            for tag in t:
                if not isinstance(tag, str):
                    raise ProfileError("invalid_value", f"tag 必须是字符串：{tag!r}")
                tag = tag.strip()
                if not tag:
                    continue
                if len(tag) > TAG_MAX_LEN:
                    raise ProfileError("invalid_value", f"tag 长度 ≤ {TAG_MAX_LEN}：{tag!r}")
                if not STR_RE.match(tag):
                    raise ProfileError("invalid_value", f"tag 非法字符：{tag!r}")
                if tag not in cleaned:
                    cleaned.append(tag)
            normalized["tags"] = _tags_to_db(cleaned)

    if "bio" in fields:
        b = fields["bio"]
        if b is not None:
            if not isinstance(b, str) or len(b) > BIO_MAX_LEN:
                raise ProfileError("invalid_value", f"bio 长度 ≤ {BIO_MAX_LEN}")
        normalized["bio"] = b

    if "height" in fields:
        h = fields["height"]
        if h is not None:
            if not isinstance(h, int) or h < 140 or h > 220:
                raise ProfileError("invalid_value", f"height 范围 [140, 220]，收到 {h!r}")
        normalized["height"] = h

    if "education" in fields:
        e = fields["education"]
        if e is not None:
            if not isinstance(e, str) or e not in ALLOWED_EDUCATION:
                raise ProfileError(
                    "invalid_value",
                    f"education 必须是 {sorted(ALLOWED_EDUCATION)} 之一，收到 {e!r}",
                )
        normalized["education"] = e

    if "agent_gateway_url" in fields:
        u = fields["agent_gateway_url"]
        if u is not None:
            if not isinstance(u, str) or len(u) > GATEWAY_URL_MAX_LEN:
                raise ProfileError(
                    "invalid_value", f"agent_gateway_url 长度 ≤ {GATEWAY_URL_MAX_LEN}"
                )
            if u and not (u.startswith("http://") or u.startswith("https://")):
                raise ProfileError(
                    "invalid_value", "agent_gateway_url 必须 http:// 或 https:// 开头"
                )
        normalized["agent_gateway_url"] = u

    if "agent_hooks_token" in fields:
        t = fields["agent_hooks_token"]
        if t is not None:
            if not isinstance(t, str) or len(t) > HOOKS_TOKEN_MAX_LEN:
                raise ProfileError(
                    "invalid_value", f"agent_hooks_token 长度 ≤ {HOOKS_TOKEN_MAX_LEN}"
                )
        normalized["agent_hooks_token"] = t

    if "notify_on_new_mail" in fields:
        n = fields["notify_on_new_mail"]
        if not isinstance(n, bool):
            raise ProfileError("invalid_value", "notify_on_new_mail 必须是 bool")
        normalized["notify_on_new_mail"] = 1 if n else 0

    if not normalized:
        raise ProfileError("invalid_value", "没有任何可更新字段")

    normalized["updated_at"] = now_ts()

    set_clause = ", ".join(f"{k} = ?" for k in normalized)
    params = list(normalized.values()) + [user_id]
    with get_conn() as conn:
        conn.execute(
            f"UPDATE profiles SET {set_clause} WHERE user_id = ?",
            params,
        )
        row = conn.execute(
            f"SELECT {_COLUMNS} FROM profiles WHERE user_id = ?",
            (user_id,),
        ).fetchone()
    return _row_to_profile(row)


def _row_to_profile(row) -> Profile:
    token = row["agent_hooks_token"]
    return Profile(
        user_id=row["user_id"],
        gender=row["gender"],
        age=row["age"],
        city=row["city"],
        tags=_tags_from_db(row["tags"]),
        bio=row["bio"],
        height=row["height"],
        education=row["education"],
        agent_gateway_url=row["agent_gateway_url"],
        agent_hooks_token_masked=_mask_token(token) if token else None,
        notify_on_new_mail=bool(row["notify_on_new_mail"]),
        updated_at=row["updated_at"],
    )


def _mask_token(token: str) -> str:
    """token 只露前 4 后 4，防 CLI 泄漏。"""
    if len(token) <= 8:
        return "***"
    return f"{token[:4]}***{token[-4:]}"


def _tags_to_db(tags: list[str]) -> str | None:
    if not tags:
        return None
    return "|" + "|".join(tags) + "|"


def _tags_from_db(raw: str | None) -> list[str]:
    if not raw:
        return []
    # "|a|b|c|" → ["a","b","c"]
    return [t for t in raw.strip("|").split("|") if t]
