"""Canonical master taxonomy leaf paths per product — replaces legacy multi-path intent."""

from __future__ import annotations

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

from sqlalchemy import select
from sqlalchemy.orm import Session

from channel.url_canonical import normalize_plp_path
from db.catalog_intent_planner import (
    ALL_PRODUCTS,
    _clean,
    _first_attr,
    _normalize_customer_category_label,
    cabinet_detail_category_labels,
    suppress_raw_taxonomy_child_label,
)
from db.collection_landing_pages import canonical_collection, collection_path_slug
from db.master_taxonomy_hierarchy import NODE_CATEGORY, NODE_HUB
from db.master_taxonomy_intent import (
    MATCH_MATCHED,
    _NodeIndex,
    _ProductLike,
    _norm_key,
    resolve_path_taxonomy,
)
from db.master_taxonomy_merge import resolve_canonical_path_slug
from db.models import MasterProduct, MasterProductType


def leaf_taxonomy_path_slugs(path_slugs: List[str]) -> List[str]:
    """Drop ancestor paths when a deeper assigned path exists."""
    normalized = [normalize_plp_path(slug) for slug in path_slugs if normalize_plp_path(slug)]
    if len(normalized) <= 1:
        return normalized
    return [
        slug
        for slug in normalized
        if not any(other != slug and other.startswith(f"{slug}/") for other in normalized)
    ]


# Hub-direct shopping facet categories used by Start Shopping L1 tiles.
# Master listing paths still expand these ancestors for shopping_* attribute derivation
# and Shopify; Magento product category_links membership does NOT include them
# (see is_magento_outbound_category_membership).
SHOPPING_FACET_SEGMENT_SLUGS: Set[str] = {
    "base-cabinets",
    "wall-cabinets",
    "tall-cabinets",
    "panels-and-fillers",
    "mouldings",
    "moldings",
    "accessories",
}


def is_shopping_facet_rooted_path(path_slug: str) -> bool:
    """True for hub/shopping-facet/... product taxonomy (not hub/collection finish trees).

    Examples:
      kitchen-cabinets/accessories/clearance-kit → True
      kitchen-cabinets/accessories/clearance-kit/base-cabinets → True
      kitchen-cabinets/anna-snow-white/base-cabinets → False
      kitchen-cabinets/rta-cabinets/anna-snow-white → False
    """
    parts = [part for part in normalize_plp_path(path_slug or "").split("/") if part]
    if len(parts) < 2:
        return False
    return parts[1] in SHOPPING_FACET_SEGMENT_SLUGS


def is_magento_outbound_category_membership(
    path_slug: str,
    *,
    taxonomy_kind: Optional[str] = None,
) -> bool:
    """True when a listing path may be sent as Magento product category_links.

    Keep: all-products, collection, and L3+ category leaves.
    Drop: hub (L1) and shopping-facet (L2) — those surface only via shopping_* attrs.
    """
    slug = normalize_plp_path(path_slug or "")
    if not slug:
        return False
    if slug == all_products_path_slug() or slug == "all-products":
        return True
    # Sample door hub is a deliberate single-segment Magento root membership.
    from db.sample_category import is_sample_hub_path

    if is_sample_hub_path(slug):
        return True
    kind = str(taxonomy_kind or "").strip().lower()
    if kind == "collection":
        return True
    parts = [part for part in slug.split("/") if part]
    if len(parts) <= 1:
        return False
    if len(parts) == 2 and parts[1] in SHOPPING_FACET_SEGMENT_SLUGS:
        return False
    return True


def expand_shopping_facet_ancestor_paths(
    path_slugs: List[str],
    *,
    index: Optional[_NodeIndex] = None,
    include_hub: bool = False,
) -> List[str]:
    """Keep leaf paths and re-add shopping-facet (and optional hub) ancestors.

    Example:
      kitchen-cabinets/base-cabinets/single-door
        → also kitchen-cabinets/base-cabinets
        → and kitchen-cabinets when include_hub=True
      kitchen-cabinets/anna-caramel-harvest (collection) is unchanged except hub

    Explicit input leaves are always retained even when the taxonomy node is temporarily
    missing; otherwise SKUs silently demote to L2-only (e.g. panels-and-fillers without
    fillers) and Magento outbound loses the L3 category link.
    """
    normalized = [normalize_plp_path(slug) for slug in path_slugs if normalize_plp_path(slug)]
    out: List[str] = []
    seen: Set[str] = set()

    def _push(slug: str, *, allow_hub: bool = False, require_node: bool = True) -> None:
        if not slug or slug in seen:
            return
        if index is not None and require_node:
            node = index.by_slug.get(slug)
            if node is None or not node.is_active:
                return
            if node.node_kind == NODE_HUB and not (include_hub or allow_hub):
                return
        elif index is not None and not require_node:
            node = index.by_slug.get(slug)
            if node is not None and node.node_kind == NODE_HUB and not (include_hub or allow_hub):
                return
        seen.add(slug)
        out.append(slug)

    for slug in normalized:
        # Always retain caller-provided leaves (even if taxonomy node is missing).
        _push(slug, require_node=False)
        parts = [part for part in slug.split("/") if part]
        # hub / facet / ... → keep hub/facet when facet is a Start Shopping L1 tile
        if len(parts) >= 3 and parts[1] in SHOPPING_FACET_SEGMENT_SLUGS:
            _push(f"{parts[0]}/{parts[1]}", require_node=True)
        if include_hub and parts:
            _push(parts[0], allow_hub=True, require_node=True)
    return out


def all_products_path_slug() -> str:
    """Global catalog-root listing slug (Magento Default/Root + Shopify All)."""
    return normalize_plp_path(ALL_PRODUCTS)


def _manual_taxonomy_override_for_product(session: Session, product: MasterProduct) -> Optional[Dict[str, Any]]:
    sku = _clean(getattr(product, "sku", None))
    if not sku:
        return None
    try:
        from db.manual_taxonomy_assignments import resolve_manual_taxonomy_import_override

        return resolve_manual_taxonomy_import_override(
            session,
            master_sku=sku,
            source_sku=_clean(getattr(product, "source_item", None)),
        )
    except Exception:
        return None


def _shorten_cabinet_detail_label(label: str) -> Optional[str]:
    """Single Door Wall Cabinet → Single Door; keeps Magento leaf as single-door."""
    text = str(label or "").strip()
    if not text:
        return None
    for suffix in (
        " Wall Cabinet",
        " Base Cabinet",
        " Tall Cabinet",
        " Cabinet",
    ):
        if text.lower().endswith(suffix.lower()) and len(text) > len(suffix):
            shortened = text[: -len(suffix)].strip()
            if shortened:
                return shortened
    return None


def _prefer_short_category_leaf_slug(index: _NodeIndex, slug: str) -> str:
    """Prefer .../single-door over .../single-door-wall-cabinet when both exist."""
    normalized = normalize_plp_path(slug)
    if not normalized or "/" not in normalized:
        return normalized
    parts = [part for part in normalized.split("/") if part]
    if len(parts) < 3:
        return normalized
    parent_prefix = "/".join(parts[:-1])
    leaf = parts[-1]
    candidates: List[str] = [normalized]
    for suffix in ("-wall-cabinet", "-base-cabinet", "-tall-cabinet", "-cabinet"):
        if leaf.endswith(suffix):
            short_leaf = leaf[: -len(suffix)]
            if short_leaf:
                candidates.append(f"{parent_prefix}/{short_leaf}")
    tokens = leaf.split("-")
    if len(tokens) >= 2:
        candidates.append(f"{parent_prefix}/{tokens[0]}-{tokens[1]}")

    best: Optional[str] = None
    best_key: Optional[tuple] = None
    for candidate in candidates:
        cand = normalize_plp_path(candidate)
        node = index.by_slug.get(cand)
        if node is None or not node.is_active or node.node_kind != NODE_CATEGORY:
            continue
        leaf_seg = cand.rsplit("/", 1)[-1]
        key = (len(leaf_seg), len(cand))
        if best is None or (best_key is not None and key < best_key):
            best = cand
            best_key = key
    return best or normalized


def canonical_master_taxonomy_path_slugs(
    session: Session,
    product: MasterProduct,
    attrs: Optional[Dict[str, str]] = None,
    *,
    index: Optional[_NodeIndex] = None,
) -> List[str]:
    """Return taxonomy paths for one product.

    Always includes:
      - all-products (Magento Default/Root + Shopify All)

    Plus collection + category leaf (prefer short leaf like ``single-door``) and the
    shopping-facet ancestor so Magento exact-id layered filters still match.

    The hub/root category (e.g. ``kitchen-cabinets``) is only kept when it is the
    only resolved taxonomy path for the SKU. When deeper collection/category paths
    exist, hub membership is redundant and can be dropped.

    Does **not** invent Kitchen Cabinets when ``category_l1`` is missing — that was
    causing bathroom vanities to "miss" kitchen paths on reconcile replace.
    """
    attrs = attrs or {}
    index = index or _NodeIndex.load(session)
    product_like = _ProductLike(
        sku=product.sku,
        category_l1=product.category_l1,
        category_l2=product.category_l2,
        collection=product.collection,
        brand=product.brand,
        product_family=product.product_family,
    )
    l1 = _clean(product.category_l1)
    hub_slug = normalize_plp_path(l1) if l1 else None
    seen: Set[str] = set()
    out: List[str] = []

    # Global root is not a master_taxonomy_node (hierarchy skips All Products).
    root_slug = all_products_path_slug()
    if root_slug:
        seen.add(root_slug)
        out.append(root_slug)

    def _add(slug: Optional[str], *, allow_hub: bool = False) -> None:
        if not slug:
            return
        canonical = normalize_plp_path(resolve_canonical_path_slug(session, slug) or slug)
        if not canonical or canonical in seen:
            return
        node = index.by_slug.get(canonical)
        if node is None or not node.is_active:
            return
        if node.node_kind == NODE_HUB and not allow_hub:
            return
        seen.add(canonical)
        out.append(canonical)

    manual_override = _manual_taxonomy_override_for_product(session, product)
    if manual_override:
        manual_paths = [
            normalize_plp_path(path)
            for path in (manual_override.get("plp_path_slugs") or manual_override.get("membership_path_slugs") or [])
            if normalize_plp_path(path)
        ]
        if not manual_paths and manual_override.get("canonical_taxonomy_path_slug"):
            manual_paths = [normalize_plp_path(manual_override.get("canonical_taxonomy_path_slug"))]
        if manual_paths:
            manual_expanded = expand_shopping_facet_ancestor_paths(
                manual_paths,
                index=index,
                include_hub=(hub_slug == "kitchen-cabinets"),
            )
            if root_slug:
                return [root_slug, *[slug for slug in manual_expanded if slug != root_slug]]
            return manual_expanded

    coll = canonical_collection(product.collection or "")
    if coll and l1:
        _add(collection_path_slug(l1, coll))

    if manual_override and manual_override.get("canonical_taxonomy_path_slug"):
        _add(manual_override.get("canonical_taxonomy_path_slug"))
    elif hub_slug:
        category_slug = _resolve_category_leaf_slug(
            session,
            index,
            product=product,
            product_like=product_like,
            attrs=attrs,
            hub_slug=hub_slug,
        )
        if category_slug:
            _add(_prefer_short_category_leaf_slug(index, category_slug))

    # Keep the hub only as a fallback when no deeper taxonomy path resolved.
    taxonomy_slugs = [slug for slug in out if slug != root_slug]
    if not taxonomy_slugs and hub_slug:
        _add(hub_slug, allow_hub=True)
        taxonomy_slugs = [slug for slug in out if slug != root_slug]

    # Kitchen Cabinets SKUs need the full L1 hub + L2 facet + L3 leaf path set.
    # Other hubs (e.g. Bathroom Vanities) keep facet ancestors only.
    expanded = expand_shopping_facet_ancestor_paths(
        taxonomy_slugs,
        index=index,
        include_hub=(hub_slug == "kitchen-cabinets"),
    )
    if root_slug:
        return [root_slug, *[slug for slug in expanded if slug != root_slug]]
    return expanded


def canonical_taxonomy_leaf_label(
    session: Session,
    product: MasterProduct,
    attrs: Optional[Dict[str, str]] = None,
    *,
    index: Optional[_NodeIndex] = None,
) -> Optional[str]:
    """Authoritative category leaf label from the active master taxonomy hierarchy."""
    attrs = attrs or {}
    index = index or _NodeIndex.load(session)
    hub_slug = normalize_plp_path(_clean(product.category_l1) or "")
    if not hub_slug:
        return None
    product_like = _ProductLike(
        sku=product.sku,
        category_l1=product.category_l1,
        category_l2=product.category_l2,
        collection=product.collection,
        brand=product.brand,
        product_family=product.product_family,
    )
    manual_override = _manual_taxonomy_override_for_product(session, product)
    if manual_override:
        manual_slug = normalize_plp_path(manual_override.get("canonical_taxonomy_path_slug") or "")
        if manual_slug:
            node = index.by_slug.get(manual_slug)
            if node is not None and node.node_kind == NODE_CATEGORY:
                name = str(node.name or "").strip()
                leaf = name.split(" / ")[-1].strip() if " / " in name else name
                return leaf or None
    slug = _resolve_category_leaf_slug(
        session,
        index,
        product=product,
        product_like=product_like,
        attrs=attrs,
        hub_slug=hub_slug,
    )
    if not slug:
        return None
    node = index.by_slug.get(normalize_plp_path(slug))
    if node is None or node.node_kind != NODE_CATEGORY:
        return None
    name = str(node.name or "").strip()
    leaf = name.split(" / ")[-1].strip() if " / " in name else name
    return leaf or None


def _resolve_category_leaf_slug(
    session: Session,
    index: _NodeIndex,
    *,
    product: MasterProduct,
    product_like: _ProductLike,
    attrs: Dict[str, str],
    hub_slug: str,
) -> Optional[str]:
    l1 = _clean(product.category_l1)
    if not l1 or not hub_slug:
        return None

    # Brochure taxonomy import is authoritative for Kitchen Cabinets expected leaves.
    brochure_slug = _brochure_matched_category_leaf_slug(
        session,
        index,
        master_sku=_clean(getattr(product, "sku", None)) or "",
        hub_slug=hub_slug,
    )
    if brochure_slug:
        return brochure_slug

    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")
    )

    if product.product_type_id:
        product_type = session.get(MasterProductType, int(product.product_type_id))
        if product_type and str(product_type.name or "").strip():
            if not type_l2:
                type_l2 = _normalize_customer_category_label(product_type.name)
            elif not type_l1:
                type_l1 = _normalize_customer_category_label(product_type.name)

    path_labels: List[str] = []
    for detail in cabinet_detail_category_labels(product, attrs):
        if l1 and type_l1 and detail:
            path_labels.append(f"{l1}/{type_l1}/{detail}")
    allow_raw_type_l2 = not suppress_raw_taxonomy_child_label(type_l1, type_l2)
    if l1 and type_l1 and type_l2 and allow_raw_type_l2:
        short_l2 = _shorten_cabinet_detail_label(type_l2)
        # Prefer short leaf label first (Single Door before Single Door Wall Cabinet).
        if short_l2 and _norm_key(short_l2) != _norm_key(type_l2):
            path_labels.append(f"{l1}/{type_l1}/{short_l2}")
            if type_l1.lower() in {"base", "wall", "tall"}:
                path_labels.append(f"{l1}/{type_l1} Cabinets/{short_l2}")
        path_labels.append(f"{l1}/{type_l1}/{type_l2}")
        if type_l1.lower() in {"base", "wall", "tall"}:
            path_labels.append(f"{l1}/{type_l1} Cabinets/{type_l2}")
    for label in path_labels:
        resolution = resolve_path_taxonomy(session, label, product_like, index=index)
        if resolution.get("status") != MATCH_MATCHED:
            continue
        slug = normalize_plp_path(str(resolution.get("path_slug") or ""))
        if not slug.startswith(f"{hub_slug}/") and slug != hub_slug:
            continue
        node = index.by_slug.get(slug)
        if node and node.node_kind == NODE_CATEGORY:
            return _prefer_short_category_leaf_slug(index, slug)

    inferred_leaf = _match_taxonomy_child_leaf_from_text(
        index,
        hub_slug=hub_slug,
        type_l1=type_l1,
        product=product,
        attrs=attrs,
    )
    if inferred_leaf:
        return _prefer_short_category_leaf_slug(index, inferred_leaf)

    search_terms = []
    if type_l2 and allow_raw_type_l2:
        short_l2 = _shorten_cabinet_detail_label(type_l2)
        if short_l2:
            search_terms.append(short_l2)
        search_terms.append(type_l2)
    if type_l1:
        search_terms.append(type_l1)
    for term in search_terms:
        if not term:
            continue
        leaf_key = _norm_key(term)
        matches = [
            node
            for node in index.by_leaf_key.get(leaf_key, [])
            if normalize_plp_path(node.path_slug).startswith(f"{hub_slug}/")
            and node.node_kind in {NODE_CATEGORY, "category"}
            and node.is_active
        ]
        if not matches:
            continue
        chosen = sorted(
            matches,
            key=lambda node: (
                -normalize_plp_path(node.path_slug).count("/"),
                len(normalize_plp_path(node.path_slug).rsplit("/", 1)[-1]),
                len(normalize_plp_path(node.path_slug)),
            ),
        )[0]
        return _prefer_short_category_leaf_slug(index, chosen.path_slug)
    if l1 and type_l1:
        resolution = resolve_path_taxonomy(session, f"{l1}/{type_l1}", product_like, index=index)
        if resolution.get("status") == MATCH_MATCHED:
            slug = normalize_plp_path(str(resolution.get("path_slug") or ""))
            node = index.by_slug.get(slug)
            if node and node.node_kind == NODE_CATEGORY:
                return _prefer_short_category_leaf_slug(index, slug)
    return None


def _brochure_matched_category_leaf_slug(
    session: Session,
    index: _NodeIndex,
    *,
    master_sku: str,
    hub_slug: str,
) -> Optional[str]:
    """Return deepest matched brochure taxonomy leaf for this master SKU under hub."""
    if not master_sku or not hub_slug:
        return None
    from db.brochure_taxonomy import ACTIVE_STATUS
    from db.models import BrochureTaxonomyGroup, BrochureTaxonomyGroupSku

    rows = session.execute(
        select(BrochureTaxonomyGroup.canonical_taxonomy_path_slug)
        .join(
            BrochureTaxonomyGroupSku,
            BrochureTaxonomyGroupSku.group_id == BrochureTaxonomyGroup.id,
        )
        .where(BrochureTaxonomyGroup.is_active.is_(True))
        .where(BrochureTaxonomyGroup.mapping_status == MATCH_MATCHED)
        .where(BrochureTaxonomyGroupSku.assignment_status == ACTIVE_STATUS)
        .where(BrochureTaxonomyGroupSku.expected_master_sku == master_sku)
    ).all()
    hub = normalize_plp_path(hub_slug)
    best: Optional[str] = None
    best_depth = -1
    for (raw_slug,) in rows:
        slug = normalize_plp_path(raw_slug or "")
        if not slug or not (slug == hub or slug.startswith(f"{hub}/")):
            continue
        node = index.by_slug.get(slug)
        if node is None or not node.is_active or node.node_kind != NODE_CATEGORY:
            continue
        depth = len([p for p in slug.split("/") if p])
        if depth > best_depth:
            best_depth = depth
            best = slug
    if not best:
        return None
    return _prefer_short_category_leaf_slug(index, best)


def _match_taxonomy_child_leaf_from_text(
    index: _NodeIndex,
    *,
    hub_slug: str,
    type_l1: Optional[str],
    product: MasterProduct,
    attrs: Dict[str, str],
) -> Optional[str]:
    parent = _facet_node_for_type(index, hub_slug=hub_slug, type_l1=type_l1)
    if parent is None:
        return None
    haystack = " ".join(
        str(value or "").strip()
        for value in (
            getattr(product, "sku", None),
            getattr(product, "name", None),
            attrs.get("cabinet_type_sub_category_2"),
            attrs.get("cabinet_type"),
            attrs.get("sub_type"),
            getattr(product, "product_family", None),
        )
        if str(value or "").strip()
    )
    if not haystack:
        return None
    haystack_key = _norm_key(haystack)
    haystack_words = set(re.findall(r"[a-z0-9]+", haystack.lower()))
    best_slug: Optional[str] = None
    best_score: Optional[tuple[int, int, int, str]] = None

    for node in index.by_slug.values():
        if not node.is_active or node.node_kind != NODE_CATEGORY or node.parent_id != parent.id:
            continue
        leaf_title = str(node.name or "").split(" / ")[-1].strip()
        variants = {
            leaf_title,
            node.path_slug.rsplit("/", 1)[-1].replace("-", " "),
        }
        shortened = _shorten_cabinet_detail_label(leaf_title)
        if shortened:
            variants.add(shortened)

        score = 0
        for variant in variants:
            variant_key = _norm_key(variant)
            if not variant_key:
                continue
            if variant_key in haystack_key:
                score = max(score, 100 + len(variant_key))
                continue
            variant_words = set(re.findall(r"[a-z0-9]+", variant.lower()))
            if variant_words and variant_words.issubset(haystack_words):
                score = max(score, 60 + len(variant_words) * 5)
        if score <= 0:
            continue

        slug = normalize_plp_path(node.path_slug)
        leaf = slug.rsplit("/", 1)[-1] if slug else ""
        rank = (score, len(leaf), len(slug), slug)
        if best_score is None or rank > best_score:
            best_score = rank
            best_slug = slug
    return best_slug


def _facet_node_for_type(
    index: _NodeIndex,
    *,
    hub_slug: str,
    type_l1: Optional[str],
) -> Optional[MasterTaxonomyNode]:
    leaf_key = _norm_key(type_l1)
    if not leaf_key:
        return None
    matches = [
        node
        for node in index.by_leaf_key.get(leaf_key, [])
        if node.is_active
        and node.node_kind == NODE_CATEGORY
        and normalize_plp_path(node.path_slug).startswith(f"{hub_slug}/")
    ]
    if not matches:
        return None
    return sorted(
        matches,
        key=lambda node: (
            normalize_plp_path(node.path_slug).count("/"),
            len(normalize_plp_path(node.path_slug)),
        ),
    )[0]


def _path_covered_by_assigned(slug: str, assigned_raw: Set[str]) -> bool:
    """True when slug is assigned, or a deeper assigned path implies it."""
    slug = normalize_plp_path(slug)
    if not slug:
        return False
    if slug in assigned_raw:
        return True
    prefix = f"{slug}/"
    return any(other.startswith(prefix) for other in assigned_raw)


def master_taxonomy_nodes_for_product(
    session: Session,
    product: MasterProduct,
    *,
    attrs: Optional[Dict[str, str]] = None,
) -> List[Dict[str, object]]:
    """Assigned + expected canonical master taxonomy nodes (leaf paths only).

    Ancestor hubs/facets (e.g. Kitchen Cabinets, Wall Cabinets) are implied by an
    assigned L3 leaf. Comparing full expected expansion to leaf-filtered assigned
    paths falsely flags those ancestors as "(expected)" / Needs reconciliation.
    """
    from db.channel_listing_path import resolve_listing_paths_for_sku

    index = _NodeIndex.load(session)
    expected_raw = {
        normalize_plp_path(slug)
        for slug in canonical_master_taxonomy_path_slugs(session, product, attrs, index=index)
        if normalize_plp_path(slug)
    }
    assigned_raw = {
        normalize_plp_path(path.get("path_slug") or "")
        for path in resolve_listing_paths_for_sku(session, product.sku)
        if normalize_plp_path(path.get("path_slug") or "")
    }
    # Display + drift checks use leaves only (drop ancestors covered by deeper paths).
    expected_leaves = set(leaf_taxonomy_path_slugs(list(expected_raw)))
    assigned_leaves = set(leaf_taxonomy_path_slugs(list(assigned_raw)))
    out: List[Dict[str, object]] = []

    for slug in sorted(expected_leaves | assigned_leaves):
        node = index.by_slug.get(slug)
        if node is None:
            continue
        covered = _path_covered_by_assigned(slug, assigned_raw)
        in_expected = slug in expected_leaves or _path_covered_by_assigned(slug, expected_raw)
        if covered and in_expected:
            source = "assigned"
        elif covered:
            source = "stale"
        else:
            source = "expected"
        out.append(
            {
                "node_id": node.id,
                "path_slug": slug,
                "name": node.name,
                "node_kind": node.node_kind,
                "source": source,
            }
        )
    return out
