"""Prompt-aware smart defaults for the Automatic post-run review controls.

The review's shape refine (Right angles, Fill holes, Round corners, Clean
edges) and the Min size filter have one objectively best starting value per
kind of object: buildings want orthogonal footprints with courtyard-noise
holes filled, forests must NEVER fill holes (clearings are real), a tree is
never 0.2 m2. This module maps the run's English prompt token to a SHAPE
CLASS and derives those defaults, so the first result is already tuned and
the user only touches the controls to disagree.

Resolution awareness: Min size is a ground-area floor (m2) combining a
prudent per-object minimum (never large enough to hide a real small object)
with the run's resolution noise floor, (2 x mask ground pixel)^2, below
which a detection is mask noise by construction.

Pure data + functions, no Qt: callable from the plugin controller, the dock
and the headless MCP path alike. The per-run entry point is
``review_preset_for(prompt, mask_gsd_m)``.
"""
from __future__ import annotations

import re

from .review_defaults import (
    AUTO_REVIEW_CLEAN_DEFAULT,
    AUTO_REVIEW_EXPAND_DEFAULT,
    AUTO_REVIEW_FILL_HOLES_DEFAULT,
    AUTO_REVIEW_ORTHO_DEFAULT,
    AUTO_REVIEW_SIMPLIFY_DEFAULT,
    AUTO_REVIEW_SMOOTH_DEFAULT,
)

# Shape classes. Each maps to the review's boolean regularizers AND its own
# px tuning (simplify/clean, in dynamic mask px): small rounded objects
# (vehicles) get gentler tolerances so nothing real is eaten, vegetation and
# land cover get stronger ones (their outlines are fuzzy by nature and the
# extra simplify also pays for the Round corners vertex cost).
#   rectilinear      man-made 90-degree footprints: orthogonalize + fill.
#   vehicle          small rounded man-made objects: faithful outline + fill.
#   linear           road/rail networks: faithful outline + fill.
#   natural_discrete individual plants/objects: round corners + fill.
#   natural_area     land cover / water: round corners + fill.
#   default          unknown prompt: the faithful review_defaults values.
# Fill holes is ON for EVERY class by default (Yvann 2026-07-07): a solid
# footprint is the expected output across prompts; the user can still uncheck
# it per run when real internal gaps (forest clearings, lake islands) matter.

_CLASS_SETTINGS: dict[str, dict] = {
    # Right angles subsumes the outline knobs: the orthogonalize step already
    # de-staircases at max(simplify, ~2.5 px), so a 0.4 px simplify is
    # absorbed, and clean-edges' two buffer passes (the costliest op in the
    # pipeline) repeat work on outlines the regularizer straightens anyway.
    # Both seed 0 whenever ortho is on; the user can still raise them.
    "rectilinear": {"ortho": True, "fill_holes": True, "smooth": False,
                    "simplify_px": 0.0, "clean_px": 0.0},
    "vehicle": {"ortho": False, "fill_holes": True, "smooth": False,
                "simplify_px": 0.3, "clean_px": 0.3},
    "linear": {"ortho": False, "fill_holes": True, "smooth": False,
               "simplify_px": 0.4, "clean_px": 0.5},
    "natural_discrete": {"ortho": False, "fill_holes": True, "smooth": True,
                         "simplify_px": 0.5, "clean_px": 0.5},
    "natural_area": {"ortho": False, "fill_holes": True, "smooth": True,
                     "simplify_px": 0.6, "clean_px": 1.0},
    "default": {"ortho": AUTO_REVIEW_ORTHO_DEFAULT,
                "fill_holes": AUTO_REVIEW_FILL_HOLES_DEFAULT,
                "smooth": AUTO_REVIEW_SMOOTH_DEFAULT,
                "simplify_px": AUTO_REVIEW_SIMPLIFY_DEFAULT,
                "clean_px": AUTO_REVIEW_CLEAN_DEFAULT},
}

# Keyword -> shape class. Whole-word matching (with a plural "s"/"es" allowed)
# so "street" never matches "tree". Multi-word phrases are matched the same
# way and checked before their component words by longest-first ordering.
_CLASS_KEYWORDS: dict[str, tuple[str, ...]] = {
    "rectilinear": (
        "building", "house", "home", "rooftop", "roof", "warehouse",
        "factory", "hangar", "shed", "barn", "garage", "greenhouse", "silo",
        "stadium", "storage tank", "tank", "tennis court", "basketball court",
        "court", "parking lot", "parking", "shipping container", "container",
        "solar panel", "solar farm", "solar", "panel", "pv", "swimming pool",
        "pool", "pier", "dock", "helipad", "block",
    ),
    "vehicle": (
        "car", "vehicle", "van", "truck", "bus", "trailer", "caravan",
        "train", "wagon", "airplane", "plane", "aircraft", "jet",
        "helicopter", "boat", "ship", "vessel", "yacht", "kayak",
    ),
    "linear": (
        "road", "street", "highway", "motorway", "avenue", "path", "track",
        "trail", "sidewalk", "railway", "railroad", "rail", "runway",
        "taxiway", "bridge", "roundabout", "pipeline", "power line", "fence",
        "wall", "railway track", "running track",
    ),
    "natural_discrete": (
        "tree", "hedge", "bush", "shrub", "hay bale", "haybale", "bale",
        "haystack", "rock", "boulder",
    ),
    "natural_area": (
        "forest", "woodland", "wood", "jungle", "rainforest", "canopy",
        "tree cover", "tree canopy", "forest cover", "vegetation", "grass",
        "lawn", "meadow", "pasture", "wetland", "marsh", "swamp", "field",
        "farmland", "farm field", "crop", "crop field", "parcel", "vineyard",
        "orchard", "center pivot", "lake", "pond", "water", "reservoir",
        "lagoon", "river", "stream", "canal", "creek", "coastline",
        "shoreline", "waterway", "sand", "beach", "dune", "snow", "glacier",
        "bare ground", "soil", "quarry", "mine", "garden", "park",
    ),
}

# Catalogue-category fallback for prompts with no keyword hit (the library
# catalogue carries a category per preset). Conservative: only categories
# whose members share one shape treatment map to a class.
_CATEGORY_TO_CLASS: dict[str, str] = {
    "buildings": "rectilinear",
    "sport_recreation": "rectilinear",
    "vehicles_transport": "vehicle",
    "aircraft_vessels": "vehicle",
    "land_water": "natural_area",
}

# Prudent per-object Min size floors (m2): large enough to kill obvious mask
# noise, never large enough to hide a real small instance of the object (a
# 12 m2 cabin passes the 10 m2 "building" floor). Longest keyword wins so
# "solar panel" (1.7 m2 object) beats "solar farm" component words.
# Aggressive floors (Yvann 2026-07-03, replacing the earlier prudent ones):
# roughly HALF the smallest REAL instance of each object, instead of the
# smallest plausible fragment. Real runs showed the prudent values let too
# much sub-object noise through; anything the floor eats is a partial/noise
# reading of an object another detection already covers, and the spin stays
# visible so one edit relaxes it.
_MIN_SIZE_M2: dict[str, float] = {
    "warehouse": 100.0, "factory": 100.0, "hangar": 100.0, "stadium": 1000.0,
    "building": 25.0, "house": 25.0, "home": 25.0, "rooftop": 25.0,
    "roof": 25.0, "barn": 40.0, "greenhouse": 30.0, "shed": 8.0,
    "garage": 12.0, "silo": 10.0, "storage tank": 10.0, "tank": 10.0,
    "tennis court": 150.0, "basketball court": 120.0, "court": 120.0,
    "parking lot": 200.0, "parking": 200.0, "swimming pool": 12.0,
    "pool": 12.0, "solar farm": 200.0, "solar panel": 1.5, "solar": 1.5,
    "panel": 1.5, "pv": 1.5, "container": 14.0,
    "car": 5.0, "vehicle": 5.0, "van": 5.0, "truck": 18.0, "bus": 25.0,
    "trailer": 12.0, "caravan": 12.0, "train": 80.0, "airplane": 60.0,
    "plane": 60.0, "aircraft": 60.0, "helicopter": 25.0, "boat": 6.0,
    "ship": 100.0, "vessel": 6.0, "yacht": 25.0,
    "road": 50.0, "street": 50.0, "highway": 100.0, "path": 10.0,
    "track": 20.0, "railway": 40.0, "runway": 800.0, "bridge": 40.0,
    "roundabout": 100.0,
    "river": 100.0, "canal": 40.0, "stream": 20.0, "lake": 250.0,
    "pond": 20.0, "tree": 3.0, "hedge": 4.0, "bush": 2.0, "shrub": 2.0,
    "hay bale": 1.0, "haybale": 1.0, "bale": 1.0,
    "forest": 250.0, "woodland": 250.0, "vegetation": 10.0, "grass": 10.0,
    "field": 300.0, "farmland": 300.0, "crop": 150.0, "vineyard": 150.0,
    "orchard": 150.0, "wetland": 100.0, "water": 50.0, "quarry": 500.0,
}


def _normalize(prompt: str) -> str:
    text = (prompt or "").strip().lower().replace("_", " ")
    return re.sub(r"\s+", " ", text)


def _matches(text: str, keyword: str) -> bool:
    """Whole-word keyword match tolerating a trailing plural (s/es), so
    "buildings" hits "building" while "street" never hits "tree"."""
    pattern = r"\b" + re.escape(keyword) + r"(?:e?s)?\b"
    return re.search(pattern, text) is not None


def shape_class_for(prompt: str) -> str:
    """The shape class of an English prompt token (see _CLASS_SETTINGS)."""
    text = _normalize(prompt)
    if not text:
        return "default"
    # Longest keyword first across ALL classes, so a phrase ("running track",
    # linear) beats a component word of another class and ordering between
    # classes never decides a tie.
    candidates = [
        (kw, cls)
        for cls, kws in _CLASS_KEYWORDS.items()
        for kw in kws
    ]
    candidates.sort(key=lambda item: len(item[0]), reverse=True)
    for kw, cls in candidates:
        if _matches(text, kw):
            return cls
    # No keyword hit: fall back to the library catalogue's category.
    try:
        from .presets.segmentation_presets import fallback_categories
        for cat in fallback_categories():
            for preset in cat["presets"]:
                if preset["prompt"].lower() == text:
                    return _CATEGORY_TO_CLASS.get(cat["key"], "default")
    except Exception:  # noqa: BLE001 -- catalogue lookup is best-effort  # nosec B110
        pass
    return "default"


def min_size_m2_for(prompt: str, mask_gsd_m: float) -> float:
    """Min size (m2): the per-object floor, never below the run's resolution
    noise floor (3 x mask ground pixel)^2 - a blob under 3 mask pixels across
    is noise by construction, whatever the prompt. 0.0 = off (unknown prompt
    at unknown resolution)."""
    text = _normalize(prompt)
    object_floor = 0.0
    if text:
        for kw in sorted(_MIN_SIZE_M2, key=len, reverse=True):
            if _matches(text, kw):
                object_floor = _MIN_SIZE_M2[kw]
                break
    noise_floor = (3.0 * mask_gsd_m) ** 2 if mask_gsd_m and mask_gsd_m > 0 else 0.0
    return round(max(object_floor, noise_floor), 1)


def review_preset_for(prompt: str, mask_gsd_m: float) -> dict:
    """Smart review defaults for one run: prompt-shaped regularizers plus the
    resolution-aware Min size. Keys mirror the dock's review controls:

        simplify_px (float), smooth (bool), expand_px (int),
        fill_holes (bool), clean_px (float), ortho (bool),
        min_size_m2 (float, 0 = off), shape_class (str, for logging).
    """
    cls = shape_class_for(prompt)
    settings = _CLASS_SETTINGS[cls]
    return {
        "simplify_px": float(settings["simplify_px"]),
        "smooth": bool(settings["smooth"]),
        "expand_px": AUTO_REVIEW_EXPAND_DEFAULT,
        "fill_holes": bool(settings["fill_holes"]),
        "clean_px": float(settings["clean_px"]),
        "ortho": bool(settings["ortho"]),
        "min_size_m2": min_size_m2_for(prompt, mask_gsd_m),
        "shape_class": cls,
    }
