"""Cian session cookie management — load/save/verify encrypted cookies.

Used by cian_valuation.py (Stage 7) to authenticate Valuation Calculator API.
Cookies stored encrypted (pgp_sym_encrypt) in cian_session_cookies table.
"""

from __future__ import annotations

import json
import logging
from typing import Any

from curl_cffi.requests import AsyncSession

# #2306: extract_state migrated to scraper_kit (byte-identical to legacy
# cian_state_parser module, deleted #2397 финальный шаг E — golden-parity против
# него уже недоступна, kit — единственный живой путь).
from scraper_kit.cian_state_parser import extract_state
from sqlalchemy import text
from sqlalchemy.orm import Session

from app.core.config import settings

logger = logging.getLogger(__name__)

# Cookies критичные для Cian auth — фильтр перед сохранением.
# Список обновлён по реальному DevTools-дампу из logged-in сессии cian.ru (2026-05-23).
# Старые записи оставлены как fallback (backward compat).
CIAN_REQUIRED_COOKIES: set[str] = {
    # --- Cian auth & session (critical) ---
    "DMIR_AUTH",  # Cian auth token (httpOnly) — основной auth-сигнал
    "_CIAN_GK",  # Cian session GUID
    # --- Yandex Metrika (обычно присутствуют, не critical) ---
    "_ym_uid",
    "_ym_d",
    "_ym_isad",
    "_ym_visorc",
    "_yasc",  # Yandex anti-spam
    # --- Cian UX state ---
    "uxfb_card_satisfaction",
    # --- Cian session IDs (fallback / legacy deployments) ---
    "_cian_visitor_session_id",
    "_cian_app_session_id",
    "_cian_uid",
    # --- Misc (legacy / optional) ---
    "_ga",  # Google Analytics
    "tlsr_id",  # legacy fingerprint
    "cf_clearance",  # Cloudflare bot check
    "session-id",  # generic session cookie
    "anti_bot",
    "csrftoken",
}

_VALUATION_TEST_URL = (
    "https://www.cian.ru/kalkulator-nedvizhimosti/"
    "?address=%D0%A1%D0%B2%D0%B5%D1%80%D0%B4%D0%BB%D0%BE%D0%B2%D1%81%D0%BA%D0%B0%D1%8F"
    "%20%D0%BE%D0%B1%D0%BB%2C%20%D0%95%D0%BA%D0%B0%D1%82%D0%B5%D1%80%D0%B8%D0%BD%D0%B1%D1%83%D1%80%D0%B3"
    "&totalArea=40&roomsCount=1&valuationType=sale"
    "&floor%5B0%5D=floorOther&repairType%5B0%5D=repairTypeCosmetic"
)

_MFE_VALUATION = "valuation-for-agent-frontend"
# #639 (2026-05): auth-сигнал (user.isAuthenticated/userId) уехал из valuation-MFE
# (там user теперь всегда anonymous) в site-header MFE `header-frontend`, который
# присутствует на любой cian-странице. Verify читает auth именно оттуда.
_MFE_AUTH = "header-frontend"

# Returned by verify_session when a TLS/bot-fingerprint ban (HTTP 403) is detected.
# Callers that check `if state is None` will NOT treat this as "expired cookies" —
# the sentinel is truthy (non-None), so cookie-refresh alerts are suppressed.
# Callers that need to distinguish ban from valid auth should check state.get("_ban").
VERIFY_BAN_SENTINEL: dict[str, Any] = {"_ban": True}

# audit-scrapers finding 4: verify_session раньше сводило 5xx / сетевой сбой /
# смену вёрстки к тому же None, что и реальный логаут (401 / isAuthenticated=false) —
# вызывающие (_cian_pre_claim, admin upload/auto-login) реагировали "куки протухли,
# перезалей" там, где куки ни при чём (Cian недоступен ИЛИ scraper_kit.cian_state_parser
# больше не находит header-frontend initialState). Два отдельных сигнала ниже НЕ
# триггерят "cookies expired" алерт у вызывающих.

# Cian источник недоступен прямо сейчас (5xx-ответ ИЛИ сетевой/транспортный сбой —
# timeout, DNS, connection reset). Cookies могут быть абсолютно валидны — просто
# нечем было их проверить. Retry позже, БЕЗ пометки session invalid.
VERIFY_SOURCE_UNAVAILABLE_SENTINEL: dict[str, Any] = {"_source_unavailable": True}

# HTTP 200 получен, но ожидаемый auth-state (header-frontend/initialState с
# user.isAuthenticated) не найден/не распарсился — Cian изменил вёрстку/MFE-схему.
# Это engineering-проблема (extract_state/_MFE_AUTH нужно обновить), НЕ протухшие
# cookies — переставлять куки здесь бесполезно.
VERIFY_MARKUP_CHANGED_SENTINEL: dict[str, Any] = {"_markup_changed": True}


def _classify_verify_response(
    status_code: int,
    html: str | None,
) -> dict[str, Any] | None:
    """Pure classifier — maps (status_code, html) to verify_session outcome.

    Returns:
        VERIFY_BAN_SENTINEL              — 403/TLS ban (cookies могут быть в порядке,
                                            блокирует сервер)
        VERIFY_SOURCE_UNAVAILABLE_SENTINEL — 5xx/иной non-200 без содержимого —
                                            источник недоступен, НЕ cookies
        VERIFY_MARKUP_CHANGED_SENTINEL   — HTTP 200, но auth-state не найден/не
                                            распарсился — вёрстка/схема изменилась
        None                              — 401 ИЛИ isAuthenticated=false — cookies
                                            ДЕЙСТВИТЕЛЬНО протухли/разлогинены
        state dict                        — authenticated successfully
    """
    if status_code == 403:
        return VERIFY_BAN_SENTINEL
    if status_code == 401:
        return None
    if status_code != 200 or html is None:
        return VERIFY_SOURCE_UNAVAILABLE_SENTINEL
    state = extract_state(html, mfe=_MFE_AUTH, key="initialState")
    if state is None:
        return VERIFY_MARKUP_CHANGED_SENTINEL
    user = state.get("user", {}) or {}
    if not user.get("isAuthenticated"):
        return None
    return state


async def verify_session(cookies: dict[str, str]) -> dict[str, Any] | None:
    """Hit Cian Valuation Calculator with cookies — return parsed state if authenticated.

    Uses curl_cffi with impersonate='chrome120' (same as prod scrapers) to avoid
    TLS-fingerprint bans that httpx would trigger.

    Returns (проверяй через `is`, НЕ `==` — это sentinel-объекты):
        state dict                         — authenticated (user.isAuthenticated + userId)
        VERIFY_BAN_SENTINEL                 — HTTP 403 TLS/bot ban; cookies могут быть
                                               валидны — НЕ триггерить cookie-refresh alert
        VERIFY_SOURCE_UNAVAILABLE_SENTINEL  — 5xx/network/timeout; источник недоступен,
                                               НЕ триггерить cookie-refresh alert, retry позже
        VERIFY_MARKUP_CHANGED_SENTINEL      — HTTP 200 но auth-state не распарсился;
                                               Cian изменил вёрстку — НЕ cookie-проблема,
                                               нужен engineering-фикс extract_state/_MFE_AUTH
        None                                 — HTTP 401 или isAuthenticated=false; cookies
                                               ДЕЙСТВИТЕЛЬНО протухли — здесь и только здесь
                                               имеет смысл просить re-upload

    Никогда не логирует сырые значения cookies.
    """
    try:
        # proxies: mobile-proxy egress (#806) — Cian блокирует datacenter-IP даже
        # при валидных DMIR_AUTH cookies. Без прокси verify всегда вернёт 403.
        # Пусто (env не задан) → прямое подключение (dev/no-op).
        _proxy_url = settings.cian_proxy_url
        _proxies = {"http": _proxy_url, "https": _proxy_url} if _proxy_url else None
        async with AsyncSession(
            impersonate="chrome120",
            cookies=cookies,
            timeout=20.0,
            proxies=_proxies,
        ) as session:
            resp = await session.get(_VALUATION_TEST_URL, allow_redirects=True)
            status = resp.status_code
            html: str | None = resp.text if status == 200 else None

            result = _classify_verify_response(status, html)

            if result is VERIFY_BAN_SENTINEL:
                logger.warning(
                    "Cian cookies verify: HTTP 403 TLS/bot ban — cookies NOT marked expired"
                )
            elif result is VERIFY_SOURCE_UNAVAILABLE_SENTINEL:
                logger.warning(
                    "Cian cookies verify: source unavailable (status=%d) — "
                    "cookies NOT marked expired, retry later",
                    status,
                )
            elif result is VERIFY_MARKUP_CHANGED_SENTINEL:
                logger.error(
                    "Cian cookies verify: HTTP 200 but auth-state not found/parseable "
                    "(mfe=%s) — markup/schema changed, cookies NOT marked expired",
                    _MFE_AUTH,
                )
            elif result is None:
                logger.warning("Cian cookies verify: expired/unauthenticated (status=%d)", status)
            else:
                user = result.get("user", {}) or {}
                logger.info("Cian cookies verified — userId=%s", user.get("userId"))

            return result
    except Exception as exc:
        # Сетевой/транспортный сбой (timeout, DNS, connection reset и т.п.) — источник
        # недоступен, НЕ признак протухших cookies (finding 4). Раньше здесь везде
        # возвращался None, конфлируя с реальным логаутом.
        logger.warning("Cian cookies verify: transport/network error — %s", exc)
        return VERIFY_SOURCE_UNAVAILABLE_SENTINEL


def save_session(
    db: Session,
    account_user_id: int,
    cookies: dict[str, str],
    ttl_days: int = 30,
) -> None:
    """Encrypt cookies via pgp_sym_encrypt and UPSERT into cian_session_cookies.

    Uses settings.cookie_encryption_key as the encryption secret.
    Никогда не логирует сырые значения cookies.
    """
    cookies_json = json.dumps(cookies)
    db.execute(
        text("""
            INSERT INTO cian_session_cookies (
                account_user_id,
                cookies_encrypted,
                expires_at_estimate,
                uploaded_at
            ) VALUES (
                CAST(:uid AS bigint),
                pgp_sym_encrypt(:cookies_json, :key),
                NOW() + (CAST(:ttl_days AS int) || ' days')::interval,
                NOW()
            )
            ON CONFLICT (account_user_id) DO UPDATE SET
                cookies_encrypted    = EXCLUDED.cookies_encrypted,
                expires_at_estimate  = EXCLUDED.expires_at_estimate,
                uploaded_at          = NOW(),
                last_invalid_at      = NULL
        """),
        {
            "uid": account_user_id,
            "cookies_json": cookies_json,
            "key": settings.cookie_encryption_key,
            "ttl_days": ttl_days,
        },
    )
    db.commit()
    logger.info(
        "Cian cookies saved for userId=%s (count=%d, ttl=%d days)",
        account_user_id,
        len(cookies),
        ttl_days,
    )


def load_session(db: Session) -> dict[str, str] | None:
    """Load most-recently-uploaded valid Cian cookies (decrypt).

    Returns dict[cookie_name, cookie_value] или None если нет валидной session.
    Выбирает только записи где expires_at_estimate > NOW() и сессия не
    была инвалидирована после последнего upload'а.
    """
    row = (
        db.execute(
            text("""
            SELECT
                account_user_id,
                pgp_sym_decrypt(cookies_encrypted, :key)::text AS cookies_json,
                expires_at_estimate
            FROM cian_session_cookies
            WHERE expires_at_estimate > NOW()
              AND (last_invalid_at IS NULL OR last_invalid_at < uploaded_at)
            ORDER BY uploaded_at DESC
            LIMIT 1
        """),
            {"key": settings.cookie_encryption_key},
        )
        .mappings()
        .first()
    )

    if row is None:
        logger.warning("No valid Cian session cookies in DB")
        return None

    cookies: dict[str, str] = json.loads(row["cookies_json"])

    # Обновляем last_used_at — не критично, игнорируем ошибки.
    try:
        db.execute(
            text(
                "UPDATE cian_session_cookies SET last_used_at = NOW()"
                " WHERE account_user_id = CAST(:uid AS bigint)"
            ),
            {"uid": row["account_user_id"]},
        )
        db.commit()
    except Exception as exc:
        logger.warning(
            "Failed to update last_used_at for userId=%s: %s", row["account_user_id"], exc
        )

    logger.info(
        "Cian cookies loaded for userId=%s (count=%d)",
        row["account_user_id"],
        len(cookies),
    )
    return cookies


def mark_session_invalid(db: Session, account_user_id: int) -> None:
    """Flag session как expired/invalid (например после 401 во время scrape)."""
    db.execute(
        text(
            "UPDATE cian_session_cookies SET last_invalid_at = NOW()"
            " WHERE account_user_id = CAST(:uid AS bigint)"
        ),
        {"uid": account_user_id},
    )
    db.commit()
    logger.warning("Cian session marked invalid for userId=%s", account_user_id)
