"""Минимальный синхронный DaData-клиент — /clean/address → (lat, lon).

Нужен ТОЛЬКО для geo-pass в objective_backfill (#2177): адрес objective-проекта
→ координаты для haversine-сверки с domrf-точкой. Не тащим tradein-модуль
(`tradein-mvp/.../dadata.py`) импортом — это другое приложение с другим settings.

Клиент СИНХРОННЫЙ (httpx.Client): geo-pass гоняется в CLI / sync-Session, без
event-loop. Авторизация /clean/address требует ОБА: token + secret (в отличие от
/suggest, которому хватает токена). Оба берутся из settings (env DADATA_API_TOKEN /
DADATA_API_SECRET).

Возвращает None (не raise) на любой сбой — нет credentials, пустой/короткий адрес,
сетевая/HTTP-ошибка, 429 quota, невалидный JSON, адрес не распознан. Вызывающая
сторона (geo-pass) трактует None как «нет геокода» → reject-кандидат, проход не
падает.

Прод-инцидент 2026-07-03: у токена может быть ОТКЛЮЧЕНА фича CLEAN («Feature
'CLEAN' disabled for token» — стандартизация платная, баланс исчерпан), при этом
БЕСПЛАТНЫЙ suggestions API (10k/день, только Token без X-Secret) работает и тоже
отдаёт geo_lat/geo_lon. Поэтому: cleaner → при 401/403 однократный фолбэк на
suggest/address (первая саджеста). Точность саджесты чуть ниже клинера — для
наших целей (точка на карте + bbox-гейт) достаточно.

Docs: https://dadata.ru/api/clean/address/ · https://dadata.ru/api/suggest/address/
"""

from __future__ import annotations

import logging
from typing import Any

import httpx

from app.core.config import settings

logger = logging.getLogger(__name__)

DADATA_CLEAN_URL = "https://cleaner.dadata.ru/api/v1/clean/address"
DADATA_SUGGEST_URL = "https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address"


def _credentials() -> tuple[str, str] | None:
    """(token, secret) если ОБА заданы непусто, иначе None (graceful disable)."""
    token = (settings.dadata_api_token or "").strip()
    secret = (settings.dadata_api_secret or "").strip()
    if not token or not secret:
        return None
    return token, secret


def _coerce_float(value: Any) -> float | None:
    """DaData отдаёт geo_lat/geo_lon строкой — coerce в float безопасно."""
    if value is None:
        return None
    try:
        return float(value)
    except (TypeError, ValueError):
        return None


def clean_address(address: str) -> tuple[float, float] | None:
    """Геокодировать один адрес через DaData /clean/address → (lat, lon).

    Args:
        address: свободный текст адреса (objective_lots.address).

    Returns:
        (geo_lat, geo_lon) как float-пара, либо None если:
          - credentials не заданы в ENV (graceful disable);
          - адрес пустой / короче 3 символов;
          - сетевая / HTTP-ошибка (timeout, 5xx, 429 quota, 401/403 auth, 4xx);
          - невалидный JSON / пустой массив / не dict;
          - DaData не вернул geo_lat/geo_lon (адрес не геокодирован).
    """
    if not address or len(address.strip()) < 3:
        return None

    creds = _credentials()
    if creds is None:
        logger.debug("dadata_client: credentials не заданы — skip геокод")
        return None

    token, secret = creds
    headers = {
        "Authorization": f"Token {token}",
        "X-Secret": secret,
        "Content-Type": "application/json",
        "Accept": "application/json",
    }
    body = [address.strip()]

    try:
        with httpx.Client(timeout=settings.dadata_timeout_s) as client:
            response = client.post(DADATA_CLEAN_URL, headers=headers, json=body)
    except (httpx.TimeoutException, httpx.NetworkError) as exc:
        logger.warning("dadata_client: network error для %r: %s", address[:60], exc)
        return None
    except Exception as exc:
        # defensive: любой прочий httpx/клиент-сбой → None, geo-pass reject'ит,
        # НЕ роняем весь проход. Логируем на warning (не глотаем молча).
        logger.warning("dadata_client: unexpected client error для %r: %s", address[:60], exc)
        return None

    status = response.status_code
    if status == 429:
        logger.warning("dadata_client: HTTP 429 — quota exceeded")
        return None
    if status in (401, 403):
        # Фича CLEAN у токена может быть отключена (баланс стандартизации) —
        # фолбэк на бесплатный suggestions (тот же token, X-Secret не нужен).
        logger.warning("dadata_client: cleaner HTTP %d — фолбэк на suggest/address", status)
        return _suggest_geocode(address.strip(), token)
    if status >= 400:
        logger.warning("dadata_client: HTTP %d для %r", status, address[:60])
        return None

    try:
        payload = response.json()
    except ValueError as exc:
        logger.warning("dadata_client: невалидный JSON: %s", exc)
        return None

    if not isinstance(payload, list) or not payload:
        logger.info("dadata_client: пустой ответ для %r", address[:60])
        return None

    item = payload[0]
    if not isinstance(item, dict):
        logger.warning("dadata_client: первый элемент не dict (%s)", type(item).__name__)
        return None

    lat = _coerce_float(item.get("geo_lat"))
    lon = _coerce_float(item.get("geo_lon"))
    if lat is None or lon is None:
        logger.info(
            "dadata_client: нет координат (qc_geo=%s) для %r",
            item.get("qc_geo"),
            address[:60],
        )
        return None

    return (lat, lon)


def _suggest_geocode(address: str, token: str) -> tuple[float, float] | None:
    """Фолбэк-геокод через БЕСПЛАТНЫЙ suggest/address (первая саджеста).

    Возвращает (lat, lon) из suggestions[0].data.geo_lat/geo_lon либо None.
    Та же graceful-семантика, что у clean_address.
    """
    headers = {
        "Authorization": f"Token {token}",
        "Content-Type": "application/json",
        "Accept": "application/json",
    }
    try:
        with httpx.Client(timeout=settings.dadata_timeout_s) as client:
            response = client.post(
                DADATA_SUGGEST_URL,
                headers=headers,
                json={"query": address, "count": 1},
            )
    except (httpx.TimeoutException, httpx.NetworkError) as exc:
        logger.warning("dadata_client: suggest network error для %r: %s", address[:60], exc)
        return None
    except Exception as exc:
        logger.warning("dadata_client: suggest unexpected error для %r: %s", address[:60], exc)
        return None

    if response.status_code != 200:
        logger.warning("dadata_client: suggest HTTP %d для %r", response.status_code, address[:60])
        return None

    try:
        payload = response.json()
    except ValueError as exc:
        logger.warning("dadata_client: suggest невалидный JSON: %s", exc)
        return None

    suggestions = payload.get("suggestions") if isinstance(payload, dict) else None
    if not suggestions or not isinstance(suggestions[0], dict):
        logger.info("dadata_client: suggest пусто для %r", address[:60])
        return None

    data = suggestions[0].get("data") or {}
    lat = _coerce_float(data.get("geo_lat"))
    lon = _coerce_float(data.get("geo_lon"))
    if lat is None or lon is None:
        logger.info(
            "dadata_client: suggest без координат (qc_geo=%s) для %r",
            data.get("qc_geo"),
            address[:60],
        )
        return None
    return (lat, lon)
