"""Persistence для веб-чата поддержки (#tgsupport-web) — web_support_threads /
web_support_messages (см. data/sql/187_web_support_chat.sql, комментарий там же
объясняет, почему отдельные таблицы, а не `channel`-колонка в tg_support_*).

Чистые SQL-функции поверх SQLAlchemy `Session` (psycopg v3) — никакой
Telegram-логики здесь. Используется ДВУМЯ вызывающими сторонами:
  - `app.api.v1.support` (FastAPI-роутер) — создание/поиск треда, запись
    inbound-сообщения, чтение истории/unread/read.
  - `app.services.tgbot.bridge._handle_group_reply` — резолвит
    topic_message_id реплая оператора в web-тред и пишет outbound-ответ.

Тред резолвится ТОЛЬКО по username (X-Authenticated-User) — ни один метод
здесь не принимает thread_id снаружи, поэтому IDOR (чтение чужого треда)
структурно невозможен на уровне API (см. app/api/v1/support.py).
"""

from __future__ import annotations

import logging
from typing import Any

from sqlalchemy import text
from sqlalchemy.orm import Session

logger = logging.getLogger(__name__)


def find_thread_id(db: Session, username: str) -> int | None:
    """Возвращает id треда для *username*, если он уже существует, иначе None.

    Используется read-путями (GET .../messages, .../unread) — они НЕ должны
    создавать тред просто фактом обращения (иначе каждое открытие виджета
    поддержки создавало бы пустой тред для любого пользователя сайта).
    """
    row = db.execute(
        text("SELECT id FROM web_support_threads WHERE username = CAST(:username AS text)"),
        {"username": username},
    ).fetchone()
    return int(row[0]) if row is not None else None


def get_or_create_thread(db: Session, username: str) -> int:
    """Гарантирует существование треда для *username*, обновляя last_seen_at.

    Вызывается ТОЛЬКО из send-пути (POST .../messages) — отправка сообщения
    это единственное действие, которое должно "создавать" тред.
    """
    row = db.execute(
        text(
            """
            INSERT INTO web_support_threads (username, created_at, last_seen_at, last_read_at)
            VALUES (CAST(:username AS text), NOW(), NOW(), NOW())
            ON CONFLICT (username) DO UPDATE
                SET last_seen_at = NOW()
            RETURNING id
            """
        ),
        {"username": username},
    ).fetchone()
    assert row is not None  # INSERT ... RETURNING всегда отдаёт строку
    return int(row[0])


def record_inbound(
    db: Session,
    *,
    thread_id: int,
    text_body: str,
    topic_message_id: int | None,
    support_chat_id: int | None,
) -> dict[str, Any]:
    """Записывает сообщение пользователя сайта (direction='in'). `topic_message_id` —
    id зеркала (sendMessage) в support-топике, ключ маршрутизации ответа оператора.
    `support_chat_id` — TELEGRAM_SUPPORT_CHAT_ID В МОМЕНТ отправки (#tgsupport-web
    review M1): скоупит будущий резолв `find_thread_by_topic_message` к ТЕКУЩЕЙ
    support-группе — если группу когда-нибудь сменят/пересоздадут, Telegram
    message_id стартует заново с 1 в новом чате и может совпасть с числом из
    старого — без этого поля коллизия была бы ТИХОЙ (см. миграцию 187/188)."""
    row = (
        db.execute(
            text(
                """
                INSERT INTO web_support_messages
                    (thread_id, direction, text_body, topic_message_id,
                     support_chat_id, operator_tg_id, created_at)
                VALUES
                    (CAST(:thread_id AS bigint), 'in', :text_body,
                     CAST(:topic_message_id AS bigint),
                     CAST(:support_chat_id AS bigint), NULL, NOW())
                RETURNING id, direction, text_body, operator_tg_id, created_at
                """
            ),
            {
                "thread_id": thread_id,
                "text_body": text_body,
                "topic_message_id": topic_message_id,
                "support_chat_id": support_chat_id,
            },
        )
        .mappings()
        .one()
    )
    return dict(row)


def find_thread_by_topic_message(
    db: Session, topic_message_id: int, support_chat_id: int
) -> int | None:
    """Резолвит id зеркала (сообщения оператора reply_to) в thread_id — только
    среди direction='in' записей, зеркало-конвенция как в tg_support_messages (186).

    Скоупим к ТЕКУЩЕМУ `support_chat_id` (#tgsupport-web review M1): строка со
    ЧУЖИМ (не NULL и не текущим) support_chat_id — это исторический артефакт
    ротации support-группы, НЕ валидный маршрут для сегодняшнего реплая. NULL
    (легаси-строки до этой колонки, если такие есть) — лениентно матчатся как
    "любой чат", т.к. до введения этого поля был ровно один действующий чат."""
    row = db.execute(
        text(
            """
            SELECT thread_id
            FROM web_support_messages
            WHERE topic_message_id = CAST(:topic_message_id AS bigint)
              AND direction = 'in'
              AND (support_chat_id = CAST(:support_chat_id AS bigint) OR support_chat_id IS NULL)
            ORDER BY created_at DESC
            LIMIT 1
            """
        ),
        {"topic_message_id": topic_message_id, "support_chat_id": support_chat_id},
    ).fetchone()
    return int(row[0]) if row is not None else None


def record_outbound(
    db: Session, *, thread_id: int, text_body: str, operator_tg_id: int | None
) -> int | None:
    """Записывает ответ оператора (реплай на веб-зеркало) как direction='out'.
    `topic_message_id` всегда NULL — маршрутизирующий ключ живёт только на
    inbound-записи (см. tg_support_messages-конвенцию, 186)."""
    row = db.execute(
        text(
            """
            INSERT INTO web_support_messages
                (thread_id, direction, text_body, topic_message_id, operator_tg_id, created_at)
            VALUES
                (CAST(:thread_id AS bigint), 'out', :text_body, NULL,
                 CAST(:operator_tg_id AS bigint), NOW())
            RETURNING id
            """
        ),
        {
            "thread_id": thread_id,
            "text_body": text_body,
            "operator_tg_id": operator_tg_id,
        },
    ).fetchone()
    return int(row[0]) if row is not None else None


def list_messages(
    db: Session, *, thread_id: int, since_id: int, limit: int = 200
) -> list[dict[str, Any]]:
    """Сообщения треда с id > since_id, по возрастанию (обычный polling с фронта).

    `limit` (#tgsupport-web review M5): без него КАЖДОЕ монтирование виджета на
    старом треде отдавало бы ВЕСЬ лог переписки. Берём последние `limit` (ORDER
    BY id DESC + LIMIT), потом разворачиваем в хронологический порядок — так
    incremental-polling (`since_id` = последний известный id, обычно единицы
    новых строк) не страдает, а первый холодный load длинного треда получает
    последние `limit`, а не самые старые."""
    rows = (
        db.execute(
            text(
                """
                SELECT id, direction, text_body, operator_tg_id, created_at
                FROM web_support_messages
                WHERE thread_id = CAST(:thread_id AS bigint)
                  AND id > CAST(:since_id AS bigint)
                ORDER BY id DESC
                LIMIT CAST(:limit AS integer)
                """
            ),
            {"thread_id": thread_id, "since_id": since_id, "limit": limit},
        )
        .mappings()
        .all()
    )
    return [dict(r) for r in reversed(rows)]


def count_unread(db: Session, *, thread_id: int) -> int:
    """Кол-во ответов оператора (direction='out'), пришедших после last_read_at."""
    row = db.execute(
        text(
            """
            SELECT count(*)
            FROM web_support_messages m
            JOIN web_support_threads t ON t.id = m.thread_id
            WHERE m.thread_id = CAST(:thread_id AS bigint)
              AND m.direction = 'out'
              AND m.created_at > t.last_read_at
            """
        ),
        {"thread_id": thread_id},
    ).scalar()
    return int(row or 0)


def mark_read(db: Session, *, thread_id: int) -> None:
    db.execute(
        text(
            "UPDATE web_support_threads SET last_read_at = NOW() "
            "WHERE id = CAST(:thread_id AS bigint)"
        ),
        {"thread_id": thread_id},
    )
