from __future__ import annotations

import copy
import json
import re
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Tuple

from sqlalchemy.orm import Session

from channel.url_canonical import normalize_plp_path
from db.collection_brochure_seed import load_collection_brochure_seed, seed_collection_brochure
from db.master_taxonomy_product_paths import is_shopping_facet_rooted_path
from db.master_taxonomy_sync import upsert_taxonomy_node


TWIN_NOTE_RE = re.compile(r"inherited from (?P<name>.+?) brochure sibling", re.IGNORECASE)

CLEARANCE_KIT_COLLECTION_NOTE = (
    "Clearance Kit (and other Accessories L3s) are product taxonomy leaves under "
    "kitchen-cabinets/accessories/*, never finish collections. Do not create Start "
    "Shopping / collection landings or nested paths like accessories/clearance-kit/base-cabinets."
)


def load_kitchen_cabinets_taxonomy(path: str | Path) -> Dict[str, Any]:
    payload = json.loads(Path(path).read_text(encoding="utf-8"))
    return sanitize_kitchen_cabinets_taxonomy_payload(payload)["payload"]


def sanitize_kitchen_cabinets_taxonomy_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
    """Strip polluted finish-collection shapes under shopping facets from taxonomy JSON.

    Clearance Kit belongs only under Accessories as an L3 product leaf. Nested paths
    like accessories/clearance-kit/base-cabinets must never appear as collections.
    """
    data = copy.deepcopy(payload) if isinstance(payload, dict) else {}
    removed_collections: List[str] = []
    removed_paths: List[str] = []

    collections = data.get("collections") if isinstance(data.get("collections"), dict) else {}
    cleaned_collections: Dict[str, Any] = {}
    for code, entry in collections.items():
        if not isinstance(entry, dict):
            continue
        path_slug = normalize_plp_path(
            str(entry.get("path_slug") or entry.get("landing_path_slug") or "")
        )
        if path_slug and is_shopping_facet_rooted_path(path_slug):
            removed_collections.append(str(code))
            removed_paths.append(path_slug)
            continue
        blob = json.dumps(entry).lower()
        if "accessories/clearance-kit/" in blob or "/clearance-kit/base-cabinets" in blob:
            # Drop nested Start Shopping / SEO pollution while keeping the finish collection.
            entry = _strip_nested_clearance_kit_pollution(entry)
        cleaned_collections[str(code)] = entry
    data["collections"] = cleaned_collections

    reference = data.get("master_taxonomy_reference") if isinstance(data.get("master_taxonomy_reference"), dict) else {}
    notes = reference.get("cross_l2_notes") if isinstance(reference.get("cross_l2_notes"), list) else []
    notes = [str(n) for n in notes if str(n or "").strip()]
    if not any("never finish collections" in n.lower() for n in notes):
        notes.append(CLEARANCE_KIT_COLLECTION_NOTE)
    reference["cross_l2_notes"] = notes
    data["master_taxonomy_reference"] = reference

    return {
        "payload": data,
        "removed_collections": removed_collections,
        "removed_paths": removed_paths,
        "note_ensured": True,
    }


def sanitize_brochure_collection_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
    """Remove deep clearance-kit / shopping-facet collection pollution from brochure JSON."""
    data = copy.deepcopy(payload) if isinstance(payload, dict) else {}
    removed: List[str] = []

    for key in ("path_slug", "landing_path_slug", "collection_path_slug"):
        slug = normalize_plp_path(str(data.get(key) or ""))
        if slug and is_shopping_facet_rooted_path(slug) and len([p for p in slug.split("/") if p]) >= 4:
            removed.append(f"{key}:{slug}")
            data.pop(key, None)

    if isinstance(data.get("start_shopping_config"), dict):
        data["start_shopping_config"] = _strip_nested_clearance_kit_pollution(data["start_shopping_config"])

    landing = data.get("landing") if isinstance(data.get("landing"), dict) else None
    if landing is not None:
        data["landing"] = _strip_nested_clearance_kit_pollution(landing)

    groups = data.get("taxonomy_groups")
    if isinstance(groups, list):
        kept = []
        for group in groups:
            if not isinstance(group, dict):
                continue
            slug = normalize_plp_path(
                str(group.get("canonical_taxonomy_path_slug") or group.get("path_slug") or "")
            )
            # Keep real L3 accessories/clearance-kit product groups; drop deeper SEO nests.
            parts = [p for p in slug.split("/") if p]
            if is_shopping_facet_rooted_path(slug) and len(parts) >= 4:
                removed.append(slug)
                continue
            kept.append(group)
        data["taxonomy_groups"] = kept

    return {"payload": data, "removed": removed}


def _strip_nested_clearance_kit_pollution(value: Any) -> Any:
    if isinstance(value, dict):
        out: Dict[str, Any] = {}
        for key, item in value.items():
            text = f"{key}:{item}".lower() if not isinstance(item, (dict, list)) else str(key).lower()
            if "accessories/clearance-kit/" in text or "/clearance-kit/base-cabinets" in text:
                continue
            if isinstance(item, str):
                slug = normalize_plp_path(item)
                parts = [p for p in slug.split("/") if p]
                if is_shopping_facet_rooted_path(slug) and len(parts) >= 4:
                    continue
            out[key] = _strip_nested_clearance_kit_pollution(item)
        return out
    if isinstance(value, list):
        cleaned = []
        for item in value:
            if isinstance(item, str):
                slug = normalize_plp_path(item)
                parts = [p for p in slug.split("/") if p]
                if is_shopping_facet_rooted_path(slug) and len(parts) >= 4:
                    continue
            cleaned.append(_strip_nested_clearance_kit_pollution(item))
        return cleaned
    return value


def load_brochure_payloads(folder: str | Path) -> Dict[str, Dict[str, Any]]:
    out: Dict[str, Dict[str, Any]] = {}
    for path in sorted(Path(folder).glob("*.json")):
        payload = sanitize_brochure_collection_payload(load_collection_brochure_seed(path))["payload"]
        collection = payload.get("collection") if isinstance(payload.get("collection"), dict) else {}
        canonical_code = str(collection.get("canonical_code") or "").strip()
        if canonical_code:
            out[canonical_code] = payload
    return out


def expand_taxonomy_collections_with_brochure_twins(
    taxonomy_payload: Dict[str, Any],
    brochure_payloads: Dict[str, Dict[str, Any]],
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
    expanded = copy.deepcopy(taxonomy_payload)
    collections = expanded.setdefault("collections", {})
    if not isinstance(collections, dict):
        raise ValueError("taxonomy_payload.collections must be an object")

    source_by_display = {
        str(item.get("display_name") or "").strip().lower(): key
        for key, item in collections.items()
        if isinstance(item, dict)
    }

    created: List[Dict[str, Any]] = []
    skipped: List[Dict[str, Any]] = []
    for canonical_code, brochure_payload in sorted(brochure_payloads.items()):
        if canonical_code in collections:
            continue
        source_code, source_name = _twin_source_for_payload(brochure_payload, source_by_display)
        if not source_code:
            skipped.append(
                {
                    "canonical_code": canonical_code,
                    "reason": "no_brochure_twin_note",
                    "display_name": _collection_value(brochure_payload, "display_name"),
                }
            )
            continue
        source_entry = collections.get(source_code)
        if not isinstance(source_entry, dict):
            skipped.append(
                {
                    "canonical_code": canonical_code,
                    "reason": "missing_source_collection",
                    "source_code": source_code,
                    "source_name": source_name,
                }
            )
            continue

        clone = copy.deepcopy(source_entry)
        clone["display_name"] = _collection_value(brochure_payload, "display_name") or clone.get("display_name")
        clone["sku_count"] = len(clone.get("assignments") or [])
        clone["copied_from_collection"] = source_code
        clone["copied_from_display_name"] = source_entry.get("display_name")
        clone["copy_reason"] = f"Copied SKU taxonomy pattern from brochure sibling '{source_name}'."
        collections[canonical_code] = clone
        created.append(
            {
                "canonical_code": canonical_code,
                "display_name": clone.get("display_name"),
                "source_code": source_code,
                "source_name": source_name,
                "assignment_count": len(clone.get("assignments") or []),
            }
        )
    return expanded, {"created": created, "skipped": skipped}


def ensure_kitchen_cabinets_taxonomy_nodes(session: Session, taxonomy_payload: Dict[str, Any]) -> Dict[str, Any]:
    reference = taxonomy_payload.get("master_taxonomy_reference") if isinstance(taxonomy_payload.get("master_taxonomy_reference"), dict) else {}
    confirmed = reference.get("confirmed_hierarchy") if isinstance(reference.get("confirmed_hierarchy"), dict) else {}
    proposed = reference.get("proposed_new_l3_categories") if isinstance(reference.get("proposed_new_l3_categories"), dict) else {}

    l2_children: Dict[str, List[str]] = {}
    for source in (confirmed, proposed):
        for l2_label, children in source.items():
            bucket = l2_children.setdefault(str(l2_label).strip(), [])
            for child in children or []:
                label = str(child or "").strip()
                if label and label not in bucket:
                    bucket.append(label)

    hub_result = upsert_taxonomy_node(
        session,
        {
            "name": "Kitchen Cabinets",
            "path_slug": "kitchen-cabinets",
            "node_kind": "hub",
            "code": "kitchen-cabinets",
            "is_active": True,
            "notes": "Seeded from kitchen_cabinets_taxonomy.json",
        },
    )
    hub_id = int(hub_result["node"]["id"])
    created = 1 if hub_result["created"] else 0
    updated = 0 if hub_result["created"] else 1

    for l2_label, children in l2_children.items():
        l2_path = normalize_plp_path(f"Kitchen Cabinets/{l2_label}")
        l2_result = upsert_taxonomy_node(
            session,
            {
                "name": f"Kitchen Cabinets / {l2_label}",
                "path_slug": l2_path,
                "parent_id": hub_id,
                "node_kind": "category",
                "code": l2_path.split("/")[-1],
                "is_active": True,
                "notes": "Seeded from kitchen_cabinets_taxonomy.json",
            },
        )
        created += 1 if l2_result["created"] else 0
        updated += 0 if l2_result["created"] else 1
        parent_id = int(l2_result["node"]["id"])
        # Legacy short facet "kitchen-cabinets/tall" → canonical "tall-cabinets".
        if l2_path.endswith("/tall-cabinets"):
            remapped = _remap_legacy_tall_facet_node(session, canonical_node_id=parent_id, hub_id=hub_id)
            updated += int(remapped.get("updated_count") or 0)

        for child in children:
            child_path = normalize_plp_path(f"Kitchen Cabinets/{l2_label}/{child}")
            child_result = upsert_taxonomy_node(
                session,
                {
                    "name": f"Kitchen Cabinets / {l2_label} / {child}",
                    "path_slug": child_path,
                    "parent_id": parent_id,
                    "node_kind": "category",
                    "code": child_path.split("/")[-1],
                    "is_active": True,
                    "notes": "Seeded from kitchen_cabinets_taxonomy.json",
                },
            )
            created += 1 if child_result["created"] else 0
            updated += 0 if child_result["created"] else 1

    return {"created_or_updated_hub": hub_result["node"]["path_slug"], "created_count": created, "updated_count": updated}


def _remap_legacy_tall_facet_node(
    session: Session,
    *,
    canonical_node_id: int,
    hub_id: int,
) -> Dict[str, Any]:
    """Move children off legacy kitchen-cabinets/tall onto tall-cabinets and deactivate legacy."""
    from db.master_taxonomy_sync import reparent_taxonomy_node
    from db.models import MasterTaxonomyNode
    from sqlalchemy import select

    legacy = session.scalar(
        select(MasterTaxonomyNode)
        .where(MasterTaxonomyNode.path_slug == "kitchen-cabinets/tall")
        .where(MasterTaxonomyNode.is_active.is_(True))
        .limit(1)
    )
    if legacy is None or int(legacy.id) == int(canonical_node_id):
        return {"updated_count": 0, "remapped_children": 0}

    children = list(
        session.scalars(
            select(MasterTaxonomyNode)
            .where(MasterTaxonomyNode.parent_id == int(legacy.id))
            .where(MasterTaxonomyNode.is_active.is_(True))
        ).all()
    )
    remapped = 0
    for child in children:
        reparent_taxonomy_node(session, int(child.id), parent_id=int(canonical_node_id))
        remapped += 1

    legacy.is_active = False
    legacy.notes = (
        f"{str(legacy.notes or '').strip()}; remapped to kitchen-cabinets/tall-cabinets".strip("; ")
    )
    # Keep hub parent for audit trail; inactive short facet must not stay as Magento parent.
    if legacy.parent_id is None:
        legacy.parent_id = int(hub_id)
    session.flush()
    return {"updated_count": remapped + 1, "remapped_children": remapped}


def build_seed_payload_from_taxonomy_collection(
    collection_entry: Dict[str, Any],
    brochure_payload: Dict[str, Any],
) -> Dict[str, Any]:
    payload = copy.deepcopy(brochure_payload)
    collection_meta = payload.get("collection") if isinstance(payload.get("collection"), dict) else {}
    payload["collection"] = dict(collection_meta)
    if collection_entry.get("display_name"):
        payload["collection"]["display_name"] = collection_entry.get("display_name")

    groups: Dict[Tuple[str, str, str], Dict[str, Any]] = {}
    for item in collection_entry.get("assignments") or []:
        if not isinstance(item, dict):
            continue
        brochure_l1 = str(item.get("l2_category") or "").strip()
        brochure_l2 = str(item.get("l3_category") or "").strip()
        sku = str(item.get("sku") or "").strip()
        if not brochure_l1 or not brochure_l2 or not sku:
            continue
        canonical_slug = normalize_plp_path(
            str(item.get("canonical_taxonomy_path_slug") or "")
        ) or normalize_plp_path(f"Kitchen Cabinets/{brochure_l1}/{brochure_l2}")
        canonical_label = (
            str(item.get("canonical_taxonomy_path_label") or "").strip()
            or _label_from_canonical_taxonomy_path_slug(canonical_slug)
            or f"Kitchen Cabinets / {brochure_l1} / {brochure_l2}"
        )
        key = (brochure_l1, brochure_l2, canonical_slug)
        group = groups.setdefault(
            key,
            {
                "brochure_l1_label": brochure_l1,
                "brochure_l2_label": brochure_l2,
                "brochure_leaf_label": None,
                "canonical_taxonomy_path_label": canonical_label,
                "canonical_taxonomy_path_slug": canonical_slug,
                "skus": [],
            },
        )
        group["skus"].append(
            {
                "brochure_sku": sku,
                "source_family": item.get("source_family"),
                "source_sku_type": item.get("source_sku_type"),
                "confidence": item.get("confidence"),
                "note": item.get("note"),
            }
        )

    payload["taxonomy_groups"] = list(groups.values())
    payload.setdefault("normalization_seed_hints", {})
    payload["normalization_seed_hints"]["source"] = "kitchen_cabinets_taxonomy_json"
    return payload


def _label_from_canonical_taxonomy_path_slug(path_slug: str) -> str:
    parts = [part for part in normalize_plp_path(path_slug).split("/") if part]
    if not parts:
        return ""
    return " / ".join(part.replace("-", " ").title() for part in parts)


def seed_kitchen_cabinets_taxonomy(
    session: Session,
    *,
    taxonomy_path: str | Path,
    brochure_folder: str | Path,
    channel_code: Optional[str] = None,
    connection_id: Optional[int] = None,
) -> Dict[str, Any]:
    taxonomy_payload = load_kitchen_cabinets_taxonomy(taxonomy_path)
    brochure_payloads = load_brochure_payloads(brochure_folder)
    expanded_payload, expansion = expand_taxonomy_collections_with_brochure_twins(taxonomy_payload, brochure_payloads)
    sanitize_stats = sanitize_kitchen_cabinets_taxonomy_payload(expanded_payload)
    expanded_payload = sanitize_stats["payload"]

    nodes = ensure_kitchen_cabinets_taxonomy_nodes(session, expanded_payload)
    collections = expanded_payload.get("collections") if isinstance(expanded_payload.get("collections"), dict) else {}

    seeded: List[Dict[str, Any]] = []
    skipped: List[Dict[str, Any]] = []
    collection_items = sorted(collections.items())
    for idx, (canonical_code, entry) in enumerate(collection_items, start=1):
        brochure_payload = brochure_payloads.get(canonical_code)
        if brochure_payload is None:
            skipped.append({"canonical_code": canonical_code, "reason": "missing_brochure_payload"})
            continue
        payload = build_seed_payload_from_taxonomy_collection(entry, brochure_payload)
        result = seed_collection_brochure(
            session,
            payload,
            channel_code=channel_code,
            connection_id=connection_id,
        )
        seeded.append(
            {
                "canonical_code": canonical_code,
                "display_name": result["collection_registry"]["name"],
                "collection_registry_id": result["collection_registry"]["id"],
                "brochure_collection_code": (
                    payload.get("source_document", {}) if isinstance(payload.get("source_document"), dict) else {}
                ).get("collection_code"),
                "group_count": result["brochure_taxonomy"]["group_count"],
            }
        )
        if idx == 1 or idx % 5 == 0 or idx == len(collection_items):
            print(
                f"[seed] collection {idx}/{len(collection_items)} {canonical_code} "
                f"groups={result['brochure_taxonomy']['group_count']}",
                flush=True,
            )

    return {
        "taxonomy_path": str(Path(taxonomy_path)),
        "brochure_folder": str(Path(brochure_folder)),
        "node_seed": nodes,
        "twin_expansion": expansion,
        "sanitize": {
            "removed_collections": sanitize_stats.get("removed_collections") or [],
            "removed_paths": sanitize_stats.get("removed_paths") or [],
        },
        "seeded_collections": seeded,
        "skipped_collections": skipped,
    }


def _collection_value(payload: Dict[str, Any], key: str) -> Optional[str]:
    collection = payload.get("collection") if isinstance(payload.get("collection"), dict) else {}
    value = str(collection.get(key) or "").strip()
    return value or None


def _twin_source_for_payload(
    brochure_payload: Dict[str, Any],
    source_by_display: Dict[str, str],
) -> Tuple[Optional[str], Optional[str]]:
    source_document = brochure_payload.get("source_document") if isinstance(brochure_payload.get("source_document"), dict) else {}
    notes = source_document.get("notes") if isinstance(source_document.get("notes"), list) else []
    for note in notes:
        text = str(note or "").strip()
        match = TWIN_NOTE_RE.search(text)
        if not match:
            continue
        source_name = match.group("name").strip().rstrip(".")
        source_code = source_by_display.get(source_name.lower())
        if source_code:
            return source_code, source_name
    return None, None
