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

Целевой enum: needs_repair / standard / good / excellent.
Используется во всех инgest-путях: avito_detail, cian SERP, cian_detail.

Источники raw-значений:
- Avito HTML: «Ремонт: евро» → avito_detail._REPAIR_MAP → «euro» → good
- Cian SERP JSON: offer.decoration → «euro» / «cosmetic» / «without» / ...
- Cian Detail JSON: offer.repairType → «cosmetic» / «design» / «no» / «euro» / ...

Маппинг основан на _IMV_REPAIR_MAP из estimator.py (обратная форма):
  needs_repair→«required», standard→«cosmetic», good→«euro», excellent→«designer».

Связь с estimator._REPAIR_COEF:
  needs_repair=0.94 / standard=1.00 / good=1.05 / excellent=1.10
"""

from __future__ import annotations

import logging
import re

logger = logging.getLogger(__name__)

# Все известные raw-значения с источником в комментарии
_RAW_TO_ENUM: dict[str, str] = {
    # ── needs_repair ────────────────────────────────────────────────────────────
    "required": "needs_repair",  # Avito: «требуется» / «без ремонта» (через _REPAIR_MAP)
    "without": "needs_repair",  # Cian: без ремонта
    "no": "needs_repair",  # Cian: нет ремонта (синоним without)
    "rough": "needs_repair",  # Cian/общий: черновая отделка
    "prefine": "needs_repair",  # Cian detail: предчистовая/whitebox (offer.decoration, #1791)
    # ── standard ────────────────────────────────────────────────────────────────
    "cosmetic": "standard",  # Avito/Cian: косметический
    # ── good ────────────────────────────────────────────────────────────────────
    "euro": "good",  # Avito/Cian: евро
    "fine": "good",  # Cian: хорошая отделка (предчистовая / whitebox)
    # ── excellent ───────────────────────────────────────────────────────────────
    "designer": "excellent",  # Avito: дизайнерский
    "design": "excellent",  # Cian: дизайнерский (camelCase вариант)
}

# Допустимые значения целевого enum — для pass-through защиты
_VALID_ENUM: frozenset[str] = frozenset({"needs_repair", "standard", "good", "excellent"})


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

    Если raw уже является корректным enum-значением — вернуть as-is (идемпотентно).
    Если raw не распознан — вернуть None + залогировать WARNING один раз.

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

    Returns:
        Одно из needs_repair / standard / good / excellent, либо None.
    """
    if raw is None:
        return None
    # Pass-through: уже нормализованный enum (idempotency — нужно при ре-обработке)
    if raw in _VALID_ENUM:
        return raw
    # Cian отдаёт camelCase ('preFine', 'whiteBox') — ключи карты в нижнем регистре,
    # сравниваем case-insensitive, чтобы не плодить дубль-ключи на каждый регистр.
    result = _RAW_TO_ENUM.get(raw) or _RAW_TO_ENUM.get(raw.lower())
    if result is None:
        logger.warning("repair_state_normalizer: unknown raw value %r — stored as NULL", raw)
    return result


# ── Inference из текста описания (#622) ──────────────────────────────────────
# Когда структурное поле repair отсутствует (avito ~1% / cian ~5% / yandex 0%),
# извлекаем состояние из текста описания регэкспами по русским фразам.
#
# Порядок ВАЖЕН: проверяем от самого сильного сигнала к слабому
#   excellent → good → needs_repair → standard,
# чтобы «после черновой сделали дизайнерский ремонт» дал excellent, а не
# needs_repair. Первое совпадение по порядку выигрывает.
#
# Целевой enum выровнен по normalize_repair_state() — те же 4 значения.
_TEXT_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
    # ── excellent: дизайнерский / премиум / эксклюзивный ремонт ──────────────
    (
        re.compile(
            r"дизайнерск(?:ий|ого|ом|ая)\s+ремонт"
            r"|дизайнерск(?:ая|ой)\s+отделк"
            r"|премиальн\w*\s+ремонт"
            r"|эксклюзивн\w*\s+ремонт"
            r"|авторск\w*\s+ремонт",
            re.IGNORECASE,
        ),
        "excellent",
    ),
    # ── good: евроремонт / отличный / свежий / современный ремонт ────────────
    (
        re.compile(
            r"евроремонт"
            r"|евро[\s-]?ремонт"
            r"|отличн\w*\s+ремонт"
            r"|свеж\w*\s+ремонт"
            r"|современн\w*\s+ремонт"
            r"|качественн\w*\s+ремонт"
            r"|ремонт\s+в\s+отличн",
            re.IGNORECASE,
        ),
        "good",
    ),
    # ── needs_repair: без отделки / черновая / требует ремонта / под ремонт ──
    (
        re.compile(
            r"без\s+отделк"
            r"|без\s+ремонт"
            r"|черновая\s+отделк"
            r"|черновой\s+ремонт"
            r"|требует(?:ся)?\s+ремонт"
            r"|требует(?:ся)?\s+космет"
            r"|нужен\s+ремонт"
            r"|под\s+ремонт"
            r"|под\s+чистов\w+\s+отделк"
            r"|предчистов",
            re.IGNORECASE,
        ),
        "needs_repair",
    ),
    # ── standard: косметический / обычный / жилое состояние ─────────────────
    (
        re.compile(
            r"косметическ\w*\s+ремонт"
            r"|косметическ\w*\s+отделк"
            r"|обычн\w*\s+ремонт"
            r"|жил(?:ое|ом)\s+состоян"
            r"|хорош\w*\s+состоян",
            re.IGNORECASE,
        ),
        "standard",
    ),
)


def infer_repair_state_from_text(description: str | None) -> str | None:
    """Извлечь repair_state из текста описания объявления (#622).

    Fallback-путь для повышения покрытия listings.repair_state, когда
    структурное поле источника отсутствует. Проверяет паттерны в порядке
    убывания силы сигнала (excellent → good → needs_repair → standard);
    первое совпадение выигрывает.

    Args:
        description: текст описания объявления (avito/cian/yandex) или None.

    Returns:
        Одно из needs_repair / standard / good / excellent, либо None если
        ни один паттерн не сработал (НЕ фабрикуем — unknown остаётся NULL).
    """
    if not description:
        return None
    for pattern, enum_value in _TEXT_PATTERNS:
        if pattern.search(description):
            return enum_value
    return None
