"""The targeted beauty set: which lines a collector reads beauty pages for (Decision 4).

Beauty is collected everywhere, but targeted pages first and full crawls last: a store's
beauty tree is walked one listing page deep, and a product page is fetched only for a line
on the list. The list is `import/beauty-targets.json`, written by
`scripts/beauty-candidates.py` (task A7) and mounted read-only into the container like the
other import files; an absent or empty file means "no beauty product pages", never "all".

A target is a fold of brand key + line words + size. A slug or a name matches when the
brand's words and the line's words all appear in it (a slug that omits the brand, as one
retailer's do, matches on the line words plus the size instead, and only for a line with a
word long enough to be distinctive). Matching is a filter on what to FETCH; identity is
still decided at ingest.
"""

import json
import logging
import os
import re
import unicodedata
from pathlib import Path

from app.services.normalize import brand_key

logger = logging.getLogger(__name__)

_CONTAINER_PATH = Path("/srv/import/beauty-targets.json")
_REPO_PATH = Path(__file__).resolve().parents[4] / "import" / "beauty-targets.json"
_TOKEN_RE = re.compile(r"[^a-z0-9]+")
_SIZE_RE = re.compile(r"\b(\d+(?:[.,]\d+)?)\s*(ml|cl|l)\b")
_UNIT_ML = {"ml": 1.0, "cl": 10.0, "l": 1000.0}


def _tokens(text: str) -> set[str]:
    text = unicodedata.normalize("NFKD", text or "").encode("ascii", "ignore").decode().lower()
    return {t for t in _TOKEN_RE.sub(" ", text).split() if t}


def _sizes_in(text: str) -> set[int]:
    text = unicodedata.normalize("NFKD", text or "").encode("ascii", "ignore").decode().lower()
    text = re.sub(r"(\d)-(\d)", r"\1.\2", text).replace("-", " ")
    return {round(float(v.replace(",", ".")) * _UNIT_ML[u]) for v, u in _SIZE_RE.findall(text)}


class BeautyTargets:
    """The folds to fetch product pages for, with a text matcher."""

    def __init__(self, folds: list[dict]) -> None:
        self.folds: list[tuple[frozenset[str], frozenset[str], int | None]] = []
        for fold in folds:
            brand_words = frozenset(brand_key(fold.get("brand")).split())
            line_words = frozenset(_tokens(fold.get("line") or ""))
            if brand_words and line_words:
                self.folds.append((brand_words, line_words, fold.get("size_ml")))

    def __len__(self) -> int:
        return len(self.folds)

    def __bool__(self) -> bool:
        return bool(self.folds)

    def matches(self, text: str, brand: str | None = None) -> bool:
        """Whether a slug or a tile name is one of the targeted lines."""
        hay = _tokens(text)
        if brand:
            hay |= _tokens(brand)
        sizes = _sizes_in(text)
        for brand_words, line_words, quantity_ml in self.folds:
            if not line_words <= hay:
                continue
            if brand_words <= hay:
                return True
            # No brand in the text: the size must agree and the line must be
            # distinctive enough that a stray short word cannot match.
            if quantity_ml and quantity_ml in sizes and any(len(w) >= 5 for w in line_words):
                return True
        return False


def load_beauty_targets(path: Path | str | None = None) -> BeautyTargets:
    """The current list, from the explicit path, the env, the container mount or the repo."""
    candidates = [Path(p) for p in (path, os.environ.get("BEAUTY_TARGETS_PATH")) if p]
    candidates += [_CONTAINER_PATH, _REPO_PATH]
    for candidate in candidates:
        if candidate.is_file():
            try:
                data = json.loads(candidate.read_text())
            except ValueError:
                logger.warning("beauty_targets_unreadable path=%s", candidate)
                return BeautyTargets([])
            folds = [f for kind in ("fragrance", "skincare") for f in data.get(kind, [])]
            return BeautyTargets(folds)
    return BeautyTargets([])
