"""Channel-neutral catalog intent planner for master products."""

from __future__ import annotations

import re
from typing import Any, Dict, Iterable, List, Optional, Set

from sqlalchemy.orm import Session

from channel.url_canonical import infer_path_kind, normalize_plp_path
from db.collection_channel_placement import shopify_collection_title
from db.collection_landing_pages import canonical_collection, collection_path_slug, DEFAULT_CATEGORY_L1
from db.models import MasterProduct
from db.source_imports import normalize_column_key

ALL_PRODUCTS = "All Products"


def plan_product_catalog_intent(
    product: MasterProduct,
    attrs: Optional[Dict[str, str]] = None,
    *,
    include_brand_collections: bool = True,
) -> Dict[str, Any]:
    """Compute channel-neutral placement intent for one master product."""
    attrs = attrs or {}
    category_paths = category_targets_for_product(product, attrs)
    shopify_collections = shopify_collection_targets_for_product(
        product,
        category_paths,
        attrs,
        include_brand=include_brand_collections,
    )
    listing_slugs = listing_path_slugs_from_paths(category_paths)
    collection_slug = None
    if product.collection_registry_id and getattr(product, "collection_registry", None):
        collection_slug = product.collection_registry.path_slug  # type: ignore[attr-defined]
    if not collection_slug and product.collection:
        collection_slug = collection_path_slug(product.category_l1 or DEFAULT_CATEGORY_L1, product.collection)
    return {
        "master_sku": product.sku,
        "category_paths": category_paths,
        "shopify_collection_titles": shopify_collections,
        "listing_path_slugs": listing_slugs,
        "collection_path_slug": normalize_plp_path(collection_slug) if collection_slug else None,
    }


def category_targets_for_product(product: MasterProduct, attrs: Optional[Dict[str, str]] = None) -> List[str]:
    attrs = attrs or {}
    l1 = _clean(product.category_l1)
    collection = _clean(product.collection)
    type_l1 = _normalize_customer_category_label(
        _first_attr(attrs, "cabinet_type_sub_category_1", "type", "product_type")
    )
    type_l2 = _normalize_customer_category_label(
        _first_attr(attrs, "cabinet_type_sub_category_2", "cabinet_type", "sub_type")
    )
    type_l1, type_l2 = _coerce_structural_taxonomy_labels(product, type_l1, type_l2)
    detail_labels = cabinet_detail_category_labels(product, attrs, top_override=type_l1, sub_override=type_l2)
    targets = [ALL_PRODUCTS]
    if l1:
        targets.append(l1)
    if l1 and type_l1:
        targets.append(f"{l1}/{type_l1}")
    if (
        l1
        and type_l1
        and type_l2
        and _norm(type_l2) != _norm(type_l1)
        and not suppress_raw_taxonomy_child_label(type_l1, type_l2, derived_detail_labels=detail_labels)
    ):
        targets.append(f"{l1}/{type_l1}/{type_l2}")
    for detail in detail_labels:
        if l1 and type_l1 and detail:
            targets.append(f"{l1}/{type_l1}/{detail}")
    if l1 and collection:
        targets.append(f"{l1}/{_canonical_collection_name(collection)}")
    if collection:
        targets.append(_canonical_collection_name(collection))
    return _dedupe_text(targets)


def category_targets_for_product_guarded(
    session: Session,
    product: MasterProduct,
    attrs: Optional[Dict[str, str]] = None,
    *,
    magento_connection_id: Optional[int] = None,
    known_names: Optional[Set[str]] = None,
) -> List[str]:
    """Category targets with suffix-only polluted collection paths removed."""
    from db.collection_path_guard import filter_suffix_only_category_targets

    return filter_suffix_only_category_targets(
        session,
        category_targets_for_product(product, attrs),
        category_l1=product.category_l1,
        magento_connection_id=magento_connection_id,
        known_names=known_names,
    )


def shopify_collection_targets_for_product(
    product: MasterProduct,
    category_targets: List[str],
    attrs: Optional[Dict[str, str]] = None,
    *,
    include_brand: bool = True,
) -> List[str]:
    attrs = attrs or {}
    brand = _clean(product.brand) or _clean(product.category_l2)
    blocked = {_norm(ALL_PRODUCTS), "cabinets"}
    if brand and not include_brand:
        blocked.add(_norm(brand))
    targets = [
        path.split("/")[-1] if "/" in path else path
        for path in category_targets
        if _norm(path.split("/")[-1] if "/" in path else path) not in blocked
    ]
    if product.collection:
        title = shopify_collection_title(product.collection, collection_path_slug(product.category_l1, product.collection))
        if title:
            targets.append(title)
    type_l1 = _clean(_first_attr(attrs, "cabinet_type_sub_category_1", "type", "product_type"))
    type_l2 = _clean(_first_attr(attrs, "cabinet_type_sub_category_2", "cabinet_type", "sub_type"))
    if type_l1:
        targets.append(type_l1)
    if type_l2:
        targets.append(type_l2)
    assembly = _clean(product.assembly_type)
    l1 = _clean(product.category_l1)
    if l1 and assembly:
        targets.append(f"{l1} - {assembly}")
    return _dedupe_text(targets)


def listing_path_slugs_from_paths(category_paths: Iterable[str]) -> List[str]:
    slugs: List[str] = []
    for path in category_paths:
        slug = normalize_plp_path(path)
        if slug and slug not in slugs:
            slugs.append(slug)
    return slugs


def listing_path_title(path: str) -> str:
    text = str(path or "").strip()
    return text.split("/")[-1].strip() if "/" in text else text


def listing_path_kind(slug: str) -> str:
    return infer_path_kind(slug)


def parent_path_slug(slug: str) -> Optional[str]:
    parts = [part for part in normalize_plp_path(slug).split("/") if part]
    if len(parts) <= 1:
        return None
    return "/".join(parts[:-1])


def hub_path_slug(slug: str) -> Optional[str]:
    parts = [part for part in normalize_plp_path(slug).split("/") if part]
    return parts[0] if len(parts) > 1 else None


def listing_path_target_from_category_path(path: str) -> Dict[str, Any]:
    slug = normalize_plp_path(path)
    return {
        "path_slug": slug,
        "title": listing_path_title(path),
        "source_path": path,
        "path_kind": listing_path_kind(slug),
        "parent_path_slug": parent_path_slug(slug),
        "hub_path_slug": hub_path_slug(slug),
    }


def shopify_path_slug_for_collection_title(title: str, *, category_l1: Optional[str] = None) -> str:
    coll = canonical_collection(title) or title
    return collection_path_slug(category_l1 or DEFAULT_CATEGORY_L1, coll)


def _first_attr(attrs: Dict[str, str], *codes: str) -> Optional[str]:
    for code in codes:
        value = attrs.get(normalize_column_key(code))
        if _clean(value):
            return str(value).strip()
    return None


def _norm(value: Optional[str]) -> str:
    return " ".join(str(value or "").strip().lower().replace("_", " ").replace("-", " ").split())


def _clean(value: Any) -> Optional[str]:
    text = str(value or "").strip()
    if not text or text.lower() in {"nan", "none", "null"}:
        return None
    return text


def _canonical_collection_name(value: str) -> str:
    text = str(value or "").strip()
    words = ["Gray" if word.lower() == "grey" else word for word in text.split()]
    return " ".join(words)


def cabinet_detail_category_labels(
    product: MasterProduct,
    attrs: Optional[Dict[str, str]] = None,
    *,
    top_override: Optional[str] = None,
    sub_override: Optional[str] = None,
) -> List[str]:
    """Derived third-level cabinet category labels such as Basic Wall or Corner Base."""
    attrs = attrs or {}
    top = top_override or _normalize_customer_category_label(
        _first_attr(attrs, "cabinet_type_sub_category_1", "type", "product_type")
    )
    sub = sub_override or _normalize_customer_category_label(
        _first_attr(attrs, "cabinet_type_sub_category_2", "cabinet_type", "sub_type")
    )
    if sub and top and top != "Wall Cabinets" and _norm(sub) != _norm(top):
        return []
    sku = _clean(getattr(product, "sku", None)) or ""
    name = _clean(getattr(product, "name", None)) or ""
    haystack = _norm(" ".join([sku, name, sub or ""]))
    sku_tail = sku.split("-")[-1].upper()

    if top == "Wall Cabinets":
        sku_u = sku.upper()
        # ACH-GDW1530 → GDW1530 (collection-prefixed master SKUs)
        code = sku_u.rsplit("-", 1)[-1] if "-" in sku_u else sku_u

        # Glass wall cabinets must never fall through to Basic Wall.
        # GDW*15 → Stacked Wall; GDW*30/36/42 → Single/Double Door Glass by width.
        if code.startswith("GDWDC") or ("diagonal" in haystack and "glass" in haystack):
            return ["Diagonal Corner Glass Wall Cabinet"]
        if code.startswith("GDWCOR"):
            return ["Stacked Wall"]
        m_gdw = re.match(r"^GDW(\d{2})(\d{2})$", code)
        if m_gdw:
            width, height = int(m_gdw.group(1)), int(m_gdw.group(2))
            if height == 15:
                return ["Stacked Wall"]
            if height in (30, 36, 42):
                return [
                    "Single Door Glass Wall Cabinet"
                    if width <= 21
                    else "Double Door Glass Wall Cabinet"
                ]
        if "glass" in haystack and "wall" in haystack:
            if "stack" in haystack:
                return ["Stacked Wall"]
            if "double" in haystack:
                return ["Double Door Glass Wall Cabinet"]
            if "diagonal" in haystack:
                return ["Diagonal Corner Glass Wall Cabinet"]
            return ["Single Door Glass Wall Cabinet"]

        # Prefer brochure SKU families over generic name heuristics.
        if code.startswith("WBC") or "blind corner" in haystack:
            return ["Blind Corner Wall Cabinet"]
        if code.startswith("WDC"):
            return ["Open Diagonal Corner Wall Cabinet"]
        if code.startswith("WCOR") or "wcor" in sku_tail.lower():
            return ["Open Pie Cut Corner Wall Cabinet"]
        if (
            code.startswith(("WAE", "AEW", "WECA", "WEC"))
            or "angle end" in haystack
        ):
            return ["Angle End Wall Cabinet"]
        if "corner" in haystack or "wcw" in sku_tail.lower():
            return ["Corner Wall"]
        # Bridge: W3012B / W3015 / W301824 (deep) — not solid Basic Wall W3030.
        if (
            "bridge" in haystack
            or code.endswith("B")
            or re.match(r"^W(?:21|24|30|33|36|39)(12|15|18|24)(24)?$", code)
        ):
            return ["Bridge Wall"]
        if "stack" in haystack or re.search(r"^W\d{2}(36|39|42)$", code):
            return ["Stacked Wall"]
        if any(
            term in haystack
            for term in ("microwave", "oven", "hood", "wine", "open shelf", "diagonal")
        ):
            return ["Special Wall"]
        # Solid basic walls: W3030 / W2436 / … (height 30/36/42 only).
        if re.match(r"^W(?:09|12|15|18|21|24|27|30|33|36|39|42)(30|36|42)$", code) or (
            sub
            and "wall" in _norm(sub)
            and "glass" not in _norm(sub)
            and not any(
                term in _norm(sub)
                for term in ("bridge", "angle", "blind", "corner", "stack", "special")
            )
        ):
            return ["Basic Wall"]
        return ["Special Wall"]

    if top == "Base Cabinets":
        if "corner" in haystack or "bcor" in sku_tail or "blind corner" in haystack:
            return ["Corner Base"]
        if "sink" in haystack or "sb" in sku_tail:
            return ["Sink Base"]
        if "drawer" in haystack or re.search(r"DB\d+", sku_tail):
            return ["Drawer Base"]
        if any(term in haystack for term in ("oven", "microwave", "trash", "lazy susan", "farm sink", "special")):
            return ["Special Base"]
        if re.search(r"B\d+", sku_tail) or (sub and "base" in _norm(sub)):
            return ["Basic Base"]
        return ["Special Base"]

    if top == "Tall Cabinets":
        if "oven" in haystack:
            return ["Oven Tall"]
        if "pantry" in haystack or sku_tail.startswith("TP"):
            return ["Pantry Tall"]
        return ["Special Tall"]

    if top == "Panels and Fillers":
        if "panel" in haystack:
            return ["Panels"]
        if "filler" in haystack:
            return ["Fillers"]
    if top == "Mouldings":
        detail = _normalize_moulding_detail_label(" ".join(part for part in (sku, name, sub or "") if part))
        return [detail] if detail else []
    return []


def suppress_raw_taxonomy_child_label(
    type_l1: Optional[str],
    type_l2: Optional[str],
    *,
    derived_detail_labels: Optional[List[str]] = None,
) -> bool:
    top = _normalize_customer_category_label(type_l1)
    sub = _normalize_customer_category_label(type_l2)
    if top in {"Mouldings", "Panels and Fillers"} and sub and derived_detail_labels:
        sub_key = _norm(sub)
        derived_keys = {_norm(item) for item in derived_detail_labels if _clean(item)}
        if derived_keys and sub_key not in derived_keys:
            return True
    if top != "Wall Cabinets" or not sub:
        return False
    sub_key = _norm(sub)
    if not sub_key or "wall" not in sub_key:
        return False
    structural_terms = (
        "angle end",
        "blind",
        "bridge",
        "corner",
        "diagonal",
        "hood",
        "microwave",
        "open",
        "pie cut",
        "special",
        "stack",
        "wine rack",
    )
    if any(term in sub_key for term in structural_terms):
        return False
    if "glass" in sub_key:
        return False
    return "door" in sub_key or sub_key == "wall cabinet"


def _normalize_customer_category_label(value: Optional[str]) -> Optional[str]:
    text = _clean(value)
    if not text:
        return None
    aliases = {
        "base": "Base Cabinets",
        "base cabinet": "Base Cabinets",
        "base cabinets": "Base Cabinets",
        "wall": "Wall Cabinets",
        "wall cabinet": "Wall Cabinets",
        "wall cabinets": "Wall Cabinets",
        "tall": "Tall Cabinets",
        "tall cabinet": "Tall Cabinets",
        "tall cabinets": "Tall Cabinets",
        "panel": "Panels and Fillers",
        "panels": "Panels and Fillers",
        "panels & fillers": "Panels and Fillers",
        "panels and fillers": "Panels and Fillers",
        "filler": "Panels and Fillers",
        "fillers": "Panels and Fillers",
        "moulding": "Mouldings",
        "mouldings": "Mouldings",
        "molding": "Mouldings",
        "moldings": "Mouldings",
        "accessory": "Accessories",
        "accessories": "Accessories",
        "storage accessories": "Accessories",
        "single door base cabinet": "Single Door",
        "trash pullout": "Trash Storage",
        "pantry cabinet": "Pantry",
        "crown moldings": "Crown Molding",
        "crown mouldings": "Crown Molding",
        "additional molding": "Additional Moldings",
        "additional moulding": "Additional Moldings",
        "bead moulding": "Bead Molding",
        "cove moulding": "Cove Crown",
        "light rail": "Light Molding",
        "light moulding": "Light Molding",
        "light molding": "Light Molding",
        "outside corner molding": "Outside Corner",
        "outside corner moulding": "Outside Corner",
        "shoe moulding": "Shoe Molding",
    }
    return aliases.get(_norm(text), text)


def _coerce_structural_taxonomy_labels(
    product: MasterProduct,
    top: Optional[str],
    sub: Optional[str],
) -> tuple[Optional[str], Optional[str]]:
    top_label = _normalize_customer_category_label(top)
    sub_label = _normalize_customer_category_label(sub)
    inferred = _infer_structural_group(product, sub_label)
    if inferred and (top_label is None or top_label in {"Accessories", "Mouldings", "Panels and Fillers"}):
        top_label = inferred
    if top_label == "Mouldings":
        moulding_detail = _normalize_moulding_detail_label(" ".join(part for part in (product.sku, product.name, sub_label or "") if part))
        if moulding_detail:
            sub_label = moulding_detail
    elif top_label == "Panels and Fillers":
        structural_text = _norm(" ".join(part for part in (product.sku, product.name, sub_label or "") if _clean(part)))
        if sub_label and "panel" in _norm(sub_label):
            sub_label = "Panels"
        elif sub_label and "filler" in _norm(sub_label):
            sub_label = "Fillers"
        elif "panel" in structural_text:
            sub_label = "Panels"
        elif "filler" in structural_text:
            sub_label = "Fillers"
    return top_label, sub_label


def _infer_structural_group(product: MasterProduct, sub: Optional[str]) -> Optional[str]:
    haystack = _norm(" ".join(part for part in (product.sku, product.name, sub or "") if _clean(part)))
    if re.search(r"\b(bead|crown|cove|scribe|toe kick|toe-kick|light rail|light molding|light moulding|quarter round|shoe|outside corner|decorative trim|moulding|molding)\b", haystack):
        return "Mouldings"
    if re.search(r"\b(panel|panels|filler|fillers)\b", haystack):
        return "Panels and Fillers"
    if re.search(r"\b(accessory|accessories|trash pullout|trash storage|rollout|roll out|glass door|corbel|post|shelf)\b", haystack):
        return "Accessories"
    return None


def _normalize_moulding_detail_label(value: Optional[str]) -> Optional[str]:
    text = _norm(value)
    if not text:
        return None
    mappings = (
        ("bead", "Bead Molding"),
        ("decorative trim", "Decorative Trim"),
        ("cove crown", "Cove Crown"),
        ("crown", "Crown Molding"),
        ("light rail", "Light Molding"),
        ("light molding", "Light Molding"),
        ("light moulding", "Light Molding"),
        ("scribe", "Scribe"),
        ("quarter round", "Quarter Round"),
        ("shoe", "Shoe Molding"),
        ("toe kick", "Toe Kick"),
        ("outside corner", "Outside Corner"),
        ("additional molding", "Additional Moldings"),
        ("additional moulding", "Additional Moldings"),
        ("moulding", "Additional Moldings"),
        ("molding", "Additional Moldings"),
    )
    for needle, label in mappings:
        if needle in text:
            return label
    return None


def _dedupe_text(values: Iterable[Optional[str]]) -> List[str]:
    seen = set()
    out: List[str] = []
    for value in values:
        text = _clean(value)
        key = _norm(text)
        if not text or key in seen:
            continue
        seen.add(key)
        out.append(text)
    return out


def slugify_segment(value: str) -> str:
    text = _norm(value)
    text = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
    return text[:250] or "collection"
