"""Нормализация house_type → каноничный enum для listings.house_type.

Целевой канон (сверено с estimator._IMV_HOUSE_TYPE_MAP, estimator.py:146):
  panel / brick / monolith / monolith_brick / block / wood.

Зачем (#2007): estimator применяет soft-penalty по house_type — аналог с
house_type != target штрафуется. Yandex SERP отдаёт SCREAMING-значения
(MONOLIT / BRICK / PANEL / ...), которые НИКОГДА не равны каноничным
(monolith / brick / panel / ...) → ~70% yandex-аналогов получали ложный штраф
и фактически выпадали из скоринга. Нормализация на ингесте чинит это.

Важно про None: неизвестное / 'other' / '' → None, НЕ 'other'. В estimator
`house_type IS NULL` нейтрально (без штрафа), а любое не-канон значение всегда
!= target → ложный штраф. Поэтому unknown лучше схлопнуть в NULL.

Источники raw-значений:
- yandex SERP: building.buildingType — SCREAMING_SNAKE (MONOLIT, MONOLIT_BRICK, ...)
- cian SERP: building.materialType — camelCase (monolith, monolithBrick,
  gasSilicateBlock, ...) — переиспользуется в #2008 (cian.py:815).
"""

from __future__ import annotations

import logging

logger = logging.getLogger(__name__)

# Канон listings.house_type — выровнен по estimator._IMV_HOUSE_TYPE_MAP.
_CANON: frozenset[str] = frozenset(
    {"panel", "brick", "monolith", "monolith_brick", "block", "wood"}
)

# raw-токен → канон. Ключи — точные значения вокабуляров источников; сравнение
# регистронезависимое (см. _LOOKUP ниже), но строго по полному токену енума,
# без fuzzy-матчинга. Неизвестные токены сюда НЕ попадают → normalize вернёт None.
_RAW_TO_CANON: dict[str, str] = {
    # ── yandex SERP (SCREAMING_SNAKE buildingType) ───────────────────────────
    "MONOLIT": "monolith",
    "BRICK": "brick",
    "PANEL": "panel",
    "MONOLIT_BRICK": "monolith_brick",
    "BLOCK": "block",
    "WOOD": "wood",
    # ── cian SERP (camelCase materialType) — reuse в #2008 ───────────────────
    "monolith": "monolith",
    "brick": "brick",
    "panel": "panel",
    "block": "block",
    "wood": "wood",
    "monolithBrick": "monolith_brick",
    "gasSilicateBlock": "block",
    "aerocreteBlock": "block",
    "foamConcreteBlock": "block",
    "stalin": "brick",  # «сталинка» — кирпич
}

# Регистронезависимый lookup. Лоуэркейс-ключи не коллизят между вокабулярами:
# 'MONOLIT'→'monolit' и 'monolith'→'monolith' — разные ключи.
_LOOKUP: dict[str, str] = {k.lower(): v for k, v in _RAW_TO_CANON.items()}


def normalize_house_type(raw: str | None) -> str | None:
    """Преобразовать raw house_type в каноничный enum.

    Уже-каноничное значение возвращается as-is (идемпотентность — нужна при
    ре-обработке и для backfill-миграции). Неизвестное / 'other' / пустое /
    None → None (НЕ 'other': см. docstring модуля про estimator soft-penalty).

    Args:
        raw: сырое значение из парсера (e.g. «MONOLIT», «monolithBrick») или None.

    Returns:
        Одно из panel / brick / monolith / monolith_brick / block / wood, либо None.
    """
    if raw is None:
        return None
    stripped = raw.strip()
    if not stripped:
        return None
    # Pass-through: уже каноничное значение (idempotency — в т.ч. 'monolith_brick',
    # которого нет среди raw-ключей карты).
    if stripped in _CANON:
        return stripped
    # Регистронезависимый exact-token lookup по обоим вокабулярам.
    canon = _LOOKUP.get(stripped) or _LOOKUP.get(stripped.lower())
    if canon is None:
        # Неизвестное / 'other' — нормально (NULL нейтрально для estimator).
        # debug, не warning: 'other' встречается массово, warning засорил бы лог.
        logger.debug("house_type_normalizer: unmapped raw value %r — stored as NULL", raw)
    return canon
