"""One term per concept, held by a grep (Stream K1, rian 16 Sep: "I want the technical language
in the database to match what I'm being told as a human in the labeling"). The retired
identifiers below cost a day of reading in which `Product` was a variant, `house` a brand,
`location` a shop and `variation` an attribute; a survivor here is a new session about to
learn the old words. Scans `main/app` and `main/web/src`; the migration history, the
changelog and the vendored kits are outside it, and the few places where the old spelling
is data or someone else's word (a retailer's JSON key, the HTTP Location header, a word
list, schema.org's Product type) are named here with their reason."""
from __future__ import annotations

import pathlib
import re

MAIN = pathlib.Path(__file__).resolve().parents[1]
ROOTS = (MAIN / "app", MAIN / "web" / "src")
SKIP_DIRS = ("vendor", "alembic", "node_modules", "__pycache__")
SKIP_FILES = ("schema.ts",)  # generated from the OpenAPI summaries; the routes are named in the routers
SUFFIXES = (".py", ".ts", ".tsx")

#: pattern -> (reason it is retired, allowed survivors as (path suffix, substring the line must carry))
RETIRED: dict[str, tuple[str, tuple[tuple[str, str], ...]]] = {
    r"\bhouse\b": ("a brand row after its aliases is the brand; brand_of() resolves it", (
        ("app/services/awards_import.py", '"house"'),  # a token in a word list, data
        ("web/src/lib/airportTemplate.ts", "fashion house"),  # English, not the identifier
        ("web/src/lib/structure.ts", "house rules"),
    )),
    r"\bhouses\b|house_id|house_slug|house_name|house_words|_house_counts": ("brand, brand_of_id, brand_slug, brand_name, brand_words, _brand_counts", ()),
    r"\bcanonical_id\b": ("alias_of_id", ()),
    r"merge_candidates|MergeCandidate": ("suggestions, Suggestion", ()),
    r"\bkept_apart\b": ("separate (Keep separate)", (
        ("app/cli.py", "kept_apart"),  # backfill suggestion_decisions must name the value it retires
    )),
    r"\bsize_ml\b|size_value|size_unit": ("quantity_ml, quantity_stated_value, quantity_stated_unit", (
        ("app/services/collectors/", "size_ml"),  # the raw record's own variant dict, data
        ("app/services/collector_view.py", 'tile.get("size_ml")'),  # the raw tile against our variant
        ("app/services/collected.py", '"size_ml"'), ("app/services/verify.py", '"size_ml"'),
        ("app/services/merges.py", '"size_ml"'),  # the merge snapshot stored in merges.detail
        ("app/services/audit.py", '"size_ml"'),  # reads that snapshot
    )),
    r"\bLocation\b|LocationSpec|CoverageLocation": ("Shop, ShopSpec, CoverageShop", ()),
    r"location_id|location_code|location_ids|\blocations\b": ("shop_id, shop_code, shop_ids, shops", ()),
    r"(?<![\w\"'/.])Product(?![\w\"']| [Vv]ariant| [Ll]ine)": ("ProductVariant (a bare Product was the variant)", ()),
    r"VARIATION_|VariationAlias|variation_aliases|variation_kind|variation_of\b": ("ATTRIBUTE_, AttributeAlias, attribute_aliases, attribute_kind, attribute_of", (
        ("app/cli.py", "variation_kind"),  # backfill attribute_keys must name the old JSON keys it moves
    )),
    r"\bdeterminant\b|\bdescriptor\b": ("picked / shown / fact, the display setting of an attribute kind", ()),
    r"product_merges|ProductMerge\b": ("merges, Merge", ()),
    r"\blines\.py\b|\bline_key\b|\bline_for\b|\bline_id\b": ("product_lines.py, product_line_key, product_line_for, product_line_id", ()),
}


#: Word-like patterns are checked on code only: a comment or a quoted sentence may say "Location
#: codes" or "a fashion house" in English. Identifier patterns (with an underscore or a case
#: marker) are checked on the whole line.
WORDLIKE = (r"\bhouse\b", r"\bLocation\b|LocationSpec|CoverageLocation", r"location_id|location_code|location_ids|\blocations\b",
            r"(?<![\w\"'/.])Product(?![\w\"']| [Vv]ariant| [Ll]ine)")
_COMMENT = re.compile(r"^\s*(#|//|/\*|\*)")
_QUOTED = re.compile(r"\"(?:\\.|[^\"\\])*\"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`")


def _code_only(line: str) -> str:
    return "" if _COMMENT.match(line) else _QUOTED.sub('""', line)


def _files():
    for root in ROOTS:
        for f in sorted(root.rglob("*")):
            if f.suffix in SUFFIXES and f.is_file() and f.name not in SKIP_FILES and not any(part in SKIP_DIRS for part in f.parts):
                yield f


def test_no_retired_identifier_survives_outside_its_named_exceptions():
    hits: list[str] = []
    for f in _files():
        rel = str(f.relative_to(MAIN.parent))
        for n, line in enumerate(f.read_text().splitlines(), 1):
            for pattern, (_, allowed) in RETIRED.items():
                probe = _code_only(line) if pattern in WORDLIKE else line
                if re.search(pattern, probe) and not any(rel.startswith(p.replace("app/", "main/app/")) or p in rel for p, must in allowed if must in line):
                    hits.append(f"{rel}:{n}: {line.strip()[:110]}  [{pattern}]")
    assert not hits, "retired identifiers (see RETIRED for the word that replaced each):\n" + "\n".join(hits[:60])
