"""query 引擎 + 付费排序 + 原子扣次数。

排序规则（固定，对外公开）:
    1. 付费用户（exposure_usage.remaining > 0）优先
    2. 同优先级内按 profile.updated_at DESC（新资料优先）

每次 query 的副作用:
    被命中且付费的 user，其 exposure_usage.remaining -= 1（事务内原子）。
    扣完归 0 的人下一次 query 回到普通池。

红线:
    - 不返回手机号 / session / phone_hash 等任何 PII
    - where 不能为空（防一次性拉全库）
    - limit 范围 [1, 100]
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from .db import get_conn, now_ts
from .dsl import DSLError, parse as parse_dsl

LIMIT_DEFAULT = 50
LIMIT_MAX = 100


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


@dataclass
class QueryHit:
    user_id: str
    gender: str | None
    age: int | None
    city: str | None
    tags: list[str]
    bio: str | None
    is_promoted: bool


@dataclass
class QueryResult:
    results: list[QueryHit]
    total_hits: int
    promoted_hits: int
    exposure_consumed: int


def run(caller_user_id: str, where: str, limit: int = LIMIT_DEFAULT) -> QueryResult:
    """执行 query 并原子扣付费用户的曝光次数。"""
    if limit < 1 or limit > LIMIT_MAX:
        raise QueryError("invalid_value", f"limit 范围 [1, {LIMIT_MAX}]，收到 {limit}")

    try:
        parsed = parse_dsl(where)
    except DSLError as e:
        raise QueryError(e.code, str(e)) from e

    # 把 DSL 产出的字段名加 profiles. 前缀，避免和 users / exposure_usage 的列撞
    sql_where = _prefix_columns(parsed.sql)

    sql = f"""
        SELECT
            p.user_id, p.gender, p.age, p.city, p.tags, p.bio,
            COALESCE(eu.remaining, 0) AS remaining
        FROM profiles p
        JOIN users u ON u.id = p.user_id
        LEFT JOIN exposure_usage eu ON eu.user_id = p.user_id
        WHERE u.deleted_at IS NULL
          AND u.id != ?
          AND p.gender IS NOT NULL
          AND p.age IS NOT NULL
          AND p.city IS NOT NULL
          AND ({sql_where})
        ORDER BY
            CASE WHEN COALESCE(eu.remaining, 0) > 0 THEN 1 ELSE 0 END DESC,
            p.updated_at DESC
        LIMIT ?
    """
    params = [caller_user_id, *parsed.params, limit]

    now = now_ts()
    promoted_ids: list[str] = []
    results: list[QueryHit] = []

    with get_conn() as conn:
        for row in conn.execute(sql, params):
            is_promoted = row["remaining"] > 0
            results.append(
                QueryHit(
                    user_id=row["user_id"],
                    gender=row["gender"],
                    age=row["age"],
                    city=row["city"],
                    tags=_tags_from_db(row["tags"]),
                    bio=row["bio"],
                    is_promoted=is_promoted,
                )
            )
            if is_promoted:
                promoted_ids.append(row["user_id"])

        # 原子批量扣：对所有被命中的付费用户 remaining -= 1
        # （用 WHERE remaining > 0 守护边界，即使并发也不会扣穿）
        consumed = 0
        for uid in promoted_ids:
            r = conn.execute(
                "UPDATE exposure_usage "
                "SET remaining = remaining - 1, last_consumed_at = ? "
                "WHERE user_id = ? AND remaining > 0",
                (now, uid),
            )
            consumed += r.rowcount

    return QueryResult(
        results=results,
        total_hits=len(results),
        promoted_hits=len(promoted_ids),
        exposure_consumed=consumed,
    )


def _prefix_columns(sql: str) -> str:
    """把 DSL 生成的 `gender = ?` 变成 `p.gender = ?`（因为 SELECT 里有 JOIN）。"""
    for col in ("gender", "age", "city", "tags"):
        # 只替换单词边界，避免字段名包含子串冲突
        import re as _re

        sql = _re.sub(rf"\b{col}\b", f"p.{col}", sql)
    return sql


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