from __future__ import annotations

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


SKU_STOPWORDS = {
    "H",
    "L",
    "R",
    "MI",
}


def enrich_payload_with_pdf_taxonomy(
    payload: Dict[str, Any],
    *,
    pdf_path: str | Path,
) -> Dict[str, Any]:
    enriched = copy.deepcopy(payload)
    collection = enriched.get("collection") if isinstance(enriched.get("collection"), dict) else {}
    category_l1 = str(collection.get("category_l1") or "Kitchen Cabinets").strip() or "Kitchen Cabinets"
    taxonomy_groups = extract_taxonomy_groups_from_pdf(pdf_path, category_l1=category_l1)
    metadata = {
        "pdf_path": str(Path(pdf_path)),
        "group_count": len(taxonomy_groups),
        "source": "pdf_classifier",
    }
    enriched["taxonomy_groups"] = taxonomy_groups
    enriched["pdf_taxonomy_extraction"] = metadata
    return enriched


def extract_taxonomy_groups_from_pdf(
    pdf_path: str | Path,
    *,
    category_l1: str = "Kitchen Cabinets",
) -> List[Dict[str, Any]]:
    text = _extract_pdf_text(pdf_path)
    return extract_taxonomy_groups_from_text(text, category_l1=category_l1)


def extract_taxonomy_groups_from_text(
    text: str,
    *,
    category_l1: str = "Kitchen Cabinets",
) -> List[Dict[str, Any]]:
    groups: Dict[tuple[str, str | None, str | None], Dict[str, Any]] = {}
    for sku in _extract_sku_tokens(text):
        classification = _classify_brochure_sku(sku, category_l1=category_l1)
        if classification is None:
            continue
        key = (
            classification["brochure_l1_label"],
            classification.get("brochure_l2_label"),
            classification.get("brochure_leaf_label"),
        )
        group = groups.setdefault(
            key,
            {
                "brochure_l1_label": classification["brochure_l1_label"],
                "brochure_l2_label": classification.get("brochure_l2_label"),
                "brochure_leaf_label": classification.get("brochure_leaf_label"),
                "canonical_taxonomy_path_label": classification.get("canonical_taxonomy_path_label"),
                "canonical_taxonomy_path_slug": classification.get("canonical_taxonomy_path_slug"),
                "skus": [],
            },
        )
        if sku not in group["skus"]:
            group["skus"].append(sku)
    return sorted(groups.values(), key=lambda item: (item["brochure_l1_label"], str(item.get("brochure_l2_label") or ""), str(item.get("brochure_leaf_label") or "")))


def _extract_pdf_text(pdf_path: str | Path) -> str:
    from pypdf import PdfReader

    reader = PdfReader(str(pdf_path))
    parts: List[str] = []
    for page in reader.pages:
        parts.append(page.extract_text() or "")
    return "\n".join(parts)


def _extract_sku_tokens(text: str) -> List[str]:
    token_re = re.compile(r"\b[A-Z][A-Z0-9/\-\.]*[A-Z0-9]\b")
    seen: set[str] = set()
    out: List[str] = []
    for match in token_re.finditer(str(text or "").upper()):
        token = match.group(0).strip().strip("*^")
        token = token.replace("–", "-").replace("—", "-")
        if not token or token in seen:
            continue
        if not _looks_like_brochure_sku(token):
            continue
        seen.add(token)
        out.append(token)
    return out


def _looks_like_brochure_sku(token: str) -> bool:
    if token in SKU_STOPWORDS:
        return False
    if token in {"SD"}:
        return True
    if not re.search(r"[A-Z]", token):
        return False
    if not re.search(r"\d", token):
        return False
    if token.startswith(("V-", "X", "H")):
        return False
    return True


def _classify_brochure_sku(sku: str, *, category_l1: str) -> Optional[Dict[str, Any]]:
    code = sku.upper()

    # Wall cabinets
    if code.startswith(("GDWCOR",)):
        return _group(category_l1, "Wall Cabinets", "Stacked Wall", None, "Stacked Wall", sku)
    if code.startswith(("GDWDC", "WGDDC")):
        return _group(
            category_l1,
            "Wall Cabinets",
            "Corner Wall",
            "Diagonal Corner Glass Wall Cabinet",
            "Diagonal Corner Glass Wall Cabinet",
            sku,
        )
    if code.startswith("WBC"):
        return _group(
            category_l1,
            "Wall Cabinets",
            "Corner Wall",
            "Blind Corner Wall Cabinet",
            "Blind Corner Wall Cabinet",
            sku,
        )
    if code.startswith("WDC"):
        return _group(
            category_l1,
            "Wall Cabinets",
            "Corner Wall",
            "Open Diagonal Corner Wall Cabinet",
            "Open Diagonal Corner Wall Cabinet",
            sku,
        )
    if code.startswith("WCOR"):
        return _group(
            category_l1,
            "Wall Cabinets",
            "Corner Wall",
            "Open Pie Cut Corner Wall Cabinet",
            "Open Pie Cut Corner Wall Cabinet",
            sku,
        )
    if code.startswith(("WECA", "WEC")):
        return _group(
            category_l1,
            "Wall Cabinets",
            "Special Wall",
            "Angle End Wall Cabinet",
            "Angle End Wall Cabinet",
            sku,
        )
    if code.startswith(("WHB", "RHAR", "RHCL")):
        return _group(
            category_l1,
            "Wall Cabinets",
            "Special Wall",
            "Range Hood Cabinet",
            "Range Hood Cabinet",
            sku,
        )
    if code.startswith(("WSC", "WR", "WPR", "WGR")):
        return _group(
            category_l1,
            "Wall Cabinets",
            "Special Wall",
            "Wine Rack Cabinet",
            "Wine Rack Cabinet",
            sku,
        )
    if code.startswith(("MC", "WMC")):
        return _group(
            category_l1,
            "Wall Cabinets",
            "Special Wall",
            "Microwave Shelf Cabinet",
            "Microwave Shelf Cabinet",
            sku,
        )
    if _is_stacked_wall_glass(code):
        return _group(category_l1, "Wall Cabinets", "Stacked Wall", None, "Stacked Wall", sku)
    if _is_bridge_wall(code):
        return _group(category_l1, "Wall Cabinets", "Bridge Wall", None, "Bridge Wall", sku)
    if _is_basic_wall_glass(code):
        leaf = "Single Door Glass Wall Cabinet" if _wall_width(code) and _wall_width(code) <= 21 else "Double Door Glass Wall Cabinet"
        return _group(category_l1, "Wall Cabinets", leaf, None, leaf, sku)
    if _is_basic_wall(code):
        return _group(category_l1, "Wall Cabinets", "Basic Wall", None, "Basic Wall", sku)

    # Mouldings before tall/base prefixes that collide (OCM vs OC, SM vs ...)
    if _is_toe_kick_molding(code):
        return _group(category_l1, "Mouldings", "Toe Kick", None, "Toe Kick", sku)
    if _is_moulding(code):
        leaf = _moulding_leaf(code)
        return _group(category_l1, "Mouldings", leaf, None, leaf or "Mouldings", sku)

    # Tall cabinets
    if code.startswith(("PC", "P")) and re.search(r"\d{4}(?:X24)?$", code):
        return _group(category_l1, "Base Cabinets", "Tall Cabinets", "Pantry Tall", "Pantry Tall", sku)
    if re.match(r"^OC(?:D)?\d", code):
        return _group(category_l1, "Base Cabinets", "Tall Cabinets", "Oven Tall", "Oven Tall", sku)

    # Base cabinets
    if code.startswith("BBC"):
        return _group(
            category_l1,
            "Base Cabinets",
            "Corner Bases",
            "Blind Corner Base Cabinet",
            "Blind Corner Base Cabinet",
            sku,
        )
    if code.startswith(("BCOR", "LS", "PBWLS", "EZR", "EZ")):
        return _group(category_l1, "Base Cabinets", "Corner Bases", "Corner Base", "Corner Base", sku)
    if code.startswith(("BMC",)):
        return _group(
            category_l1,
            "Base Cabinets",
            "Special Bases",
            "Microwave Base Cabinet",
            "Microwave Base Cabinet",
            sku,
        )
    if code.startswith(("BEC",)):
        return _group(
            category_l1,
            "Base Cabinets",
            "Special Bases",
            "Angle End Base Cabinet",
            "Angle End Base Cabinet",
            sku,
        )
    if code.startswith(("BWB",)):
        return _group(category_l1, "Accessories", "Trash Storage", None, "Trash Storage", sku)
    if code.startswith(("DB",)):
        return _group(category_l1, "Base Cabinets", "Drawer Bases", "Drawer Base", "Drawer Base", sku)
    if code.startswith(("SBF", "SB")):
        return _group(category_l1, "Base Cabinets", "Sink Bases", "Sink Base", "Sink Base", sku)
    if code.startswith(("CSB", "CSF", "CSFF")):
        return _group(category_l1, "Base Cabinets", "Sink Bases", "Sink Base", "Sink Base", sku)
    if _is_full_height_base(code):
        return _group(category_l1, "Base Cabinets", "Basic Bases", "Single Door", "Single Door", sku)
    if _is_basic_base(code):
        leaf = "Single Door" if _base_width(code) and _base_width(code) <= 21 else "Double Door Base Cabinet"
        return _group(category_l1, "Base Cabinets", "Basic Bases", leaf, leaf, sku)

    # Panels, mouldings, accessories
    if _is_panel_or_filler(code):
        return _group(category_l1, "Panels and Fillers", None, None, "Panels and Fillers", sku)
    if _is_toe_kick_molding(code):
        return _group(category_l1, "Mouldings", "Toe Kick", None, "Toe Kick", sku)
    if _is_moulding(code):
        leaf = _moulding_leaf(code)
        return _group(category_l1, "Mouldings", leaf, None, leaf or "Mouldings", sku)
    if _is_accessory(code):
        if code.startswith("GD-W"):
            return _group(category_l1, "Accessories", "Loose Glass Door", None, "Loose Glass Door", sku)
        return _group(category_l1, "Accessories", None, "Items", "Accessories", sku)
    return None


def _group(
    category_l1: str,
    brochure_l1: str,
    brochure_l2: Optional[str],
    brochure_leaf: Optional[str],
    canonical_leaf: str,
    sku: str,
) -> Dict[str, Any]:
    path_parts = [category_l1, brochure_l1]
    if canonical_leaf and brochure_l1 in {"Wall Cabinets", "Base Cabinets"} and brochure_l2 in {"Tall Cabinets"}:
        canonical_path_label = f"{category_l1}/Tall Cabinets/{canonical_leaf}"
    elif canonical_leaf and brochure_l1 in {"Wall Cabinets", "Base Cabinets", "Mouldings", "Accessories", "Panels and Fillers", "Panels And Fillers"}:
        if brochure_l2 and brochure_l2 != canonical_leaf:
            canonical_path_label = f"{category_l1}/{brochure_l1}/{canonical_leaf}"
        elif brochure_l2:
            canonical_path_label = f"{category_l1}/{brochure_l1}/{brochure_l2}"
        else:
            canonical_path_label = f"{category_l1}/{brochure_l1}/{canonical_leaf}"
    elif canonical_leaf:
        canonical_path_label = f"{category_l1}/{canonical_leaf}"
    else:
        canonical_path_label = "/".join(path_parts)
    return {
        "brochure_l1_label": brochure_l1,
        "brochure_l2_label": brochure_l2,
        "brochure_leaf_label": brochure_leaf,
        "canonical_taxonomy_path_label": canonical_path_label,
        "canonical_taxonomy_path_slug": None,
        "sku": sku,
    }


def _wall_width(code: str) -> Optional[int]:
    m = re.search(r"(\d{2})(30|36|42)$", code)
    return int(m.group(1)) if m else None


def _base_width(code: str) -> Optional[int]:
    m = re.search(r"B(\d{2})", code)
    return int(m.group(1)) if m else None


def _is_stacked_wall_glass(code: str) -> bool:
    return bool(re.match(r"^GDW(?:12|15|18|21|24|27|30|33|36)15$", code))


def _is_basic_wall_glass(code: str) -> bool:
    return bool(re.match(r"^(?:GDW|WGD)(?:09|12|15|18|21|24|27|30|33|36|39|42)(30|36|42)(?:MI|GD4|GD6)?$", code))


def _is_basic_wall(code: str) -> bool:
    return bool(re.match(r"^W(?:09|12|15|18|21|24|27|30|33|36|39|42)(30|36|42)$", code))


def _is_bridge_wall(code: str) -> bool:
    return bool(re.match(r"^W(?:21|24|30|33|36|39)(12|15|18|24)(24)?$", code))


def _is_full_height_base(code: str) -> bool:
    return bool(re.match(r"^B\d{2}-FH$", code) or re.match(r"^BH\d{2}$", code))


def _is_basic_base(code: str) -> bool:
    return bool(re.match(r"^B\d{2}$", code))


def _is_panel_or_filler(code: str) -> bool:
    return bool(re.match(r"^(BF|F\d|FO\d|FF\d|PNL|BPNL|DWR|REP|UREP|TSKIN|WSKIN|BASESKIN|WALLSKIN|BDEP|WDEP|TDEP|MBEP|MWEP|MPBEP)", code))


def _is_toe_kick_molding(code: str) -> bool:
    return bool(re.match(r"^TK\d", code)) and not code.startswith("TKBM")


def _is_moulding(code: str) -> bool:
    return bool(re.match(r"^(TKBM|OCM|ICM|SBM|SHM|LRM|LM|LRD|LRS|CM|CMA|CMV|CMC|SM8|BM8|TLM|CVM|LCVM|QRM)", code))


def _moulding_leaf(code: str) -> Optional[str]:
    if code.startswith("TKBM"):
        return "Furniture Base"
    if code.startswith("OCM"):
        return "Outside Corner"
    if code.startswith("ICM"):
        return "Inside Corner"
    if code.startswith("SHM"):
        return "Shoe"
    if code.startswith(("SM8",)) and not code.startswith("SMCROWN"):
        return "Scribe"
    if code.startswith(("SBM",)) or re.match(r"^BM\d+$", code):
        return "Bead Molding"
    if code.startswith(("CVM", "LCVM")):
        return "Cove Crown"
    if code.startswith(("LRM", "LM", "LRD", "LRS")):
        return "Light Molding"
    if code.startswith(("TLM", "CM", "LGCROWN", "SMCROWN")):
        return "Crown Molding"
    return "Additional Moldings"


def _is_accessory(code: str) -> bool:
    return bool(re.match(r"^(SC|VAR|V\d|VSC|CORBEL|DECOLEG|SSDECOLEG|TUK|BUNFOOT|HBUNFOOT|TFOOT|HTFOOT|ROT|ROTD|FS|SD|GD-W|SSPA|SDP|VKD|POST|WGR)", code))
