from __future__ import annotations

import re
from typing import Any
from urllib.parse import urlparse


def slugify_tag(value: str) -> str:
    value = value.strip().lower()
    value = re.sub(r"[^a-z0-9]+", "-", value)
    return value.strip("-")[:48]


CUISINE_TAGS = {
    "asian": {"asian"},
    "chinese": {"asian", "chinese"},
    "indian": {"asian", "indian"},
    "irish": {"irish"},
    "italian": {"italian"},
    "japanese": {"asian", "japanese"},
    "korean": {"asian", "korean"},
    "lebanese": {"middle-eastern", "lebanese"},
    "mediterranean": {"mediterranean"},
    "mexican": {"mexican"},
    "middle eastern": {"middle-eastern"},
    "middle-eastern": {"middle-eastern"},
    "thai": {"asian", "thai"},
}

PROTEIN_TAGS = {
    "beef": "beef",
    "chicken": "chicken",
    "lamb": "lamb",
    "pork": "pork",
    "shrimp": "seafood",
    "tofu": "vegetarian",
    "turkey": "turkey",
}

COURSE_HINTS = {
    "appetizer": "appetizer",
    "breakfast": "breakfast",
    "dessert": "dessert",
    "main course": "main-course",
    "main dish": "main-course",
    "salad": "salad",
    "side dish": "side-dish",
    "snack": "snack",
    "soup": "soup",
}

METHOD_HINTS = {
    "baked": "baked",
    "fried": "fried",
    "fry": "fried",
    "grilled": "grilled",
    "noodle": "noodles",
    "ramen": "noodles",
    "rice": "rice",
    "slow cooker": "slow-cooker",
    "stir fry": "stir-fry",
    "stir-fry": "stir-fry",
}

DISH_HINTS = {
    "tater tot": {"tater-tots", "potato", "side-dish"},
    "tater tots": {"tater-tots", "potato", "side-dish"},
    "potato": {"potato"},
    "potatoes": {"potato"},
}

CUISINE_PHRASE_HINTS = {
    "halal": {"middle-eastern"},
    "harissa": {"middle-eastern"},
    "pita": {"middle-eastern"},
}

SWEET_HINTS = (
    "brown sugar", "cake", "caramel", "chocolate", "coconut milk", "cookie", "dessert", "frosting",
    "honey", "icing", "mango", "maple syrup", "molasses", "muffin", "pie", "pudding", "sugar", "sweet",
    "vanilla", "whipped cream",
)

SAVORY_HINTS = (
    "beef", "broth", "butter", "cheese", "chicken", "cilantro", "cumin", "garlic", "ginger",
    "ground beef", "lamb", "meat", "noodle", "onion", "paprika", "parsley", "pine nuts",
    "rice", "salt", "savory", "soy sauce", "stock", "thyme",
)

SOURCE_NOISE_WORDS = {
    "and",
    "better",
    "chef",
    "com",
    "ever",
    "guys",
    "https",
    "joshua",
    "recipe",
    "recipes",
    "the",
    "weissman",
    "www",
}

PROTEIN_EXCLUSION_PATTERNS = {
    "beef": re.compile(r"\bbeef\s+(?:tallow|stock|broth|bouillon)\b"),
    "chicken": re.compile(r"\bchicken\s+(?:stock|broth|bouillon)\b"),
    "pork": re.compile(r"\bpork\s+(?:fat|lard|stock|broth)\b"),
}

MANAGED_TAGS = {
    *(tag for expanded in CUISINE_TAGS.values() for tag in expanded),
    *(tag for expanded in CUISINE_PHRASE_HINTS.values() for tag in expanded),
    *COURSE_HINTS.values(),
    *METHOD_HINTS.values(),
    *(tag for expanded in DISH_HINTS.values() for tag in expanded),
    *PROTEIN_TAGS.values(),
    "curry",
    "pasta",
    "savory",
    "sweet",
}


def _flatten_texts(items: list[dict[str, Any]] | None) -> str:
    flattened: list[str] = []
    for item in items or []:
        flattened.append(str(item.get("name") or ""))
        flattened.append(str(item.get("description") or ""))
    return " ".join(flattened)


def _tokenize_slug(value: str) -> set[str]:
    return {part for part in slugify_tag(value).split("-") if part}


def _source_tokens(source: str) -> set[str]:
    parsed = urlparse(source or "")
    parts = re.split(r"[^a-z0-9]+", f"{parsed.netloc} {parsed.path}".lower())
    return {part for part in parts if part}


def _is_boilerplate_existing_tag(tag: str, name: str, source: str) -> bool:
    slug = slugify_tag(tag)
    if not slug:
        return True

    if slug == slugify_tag(name):
        return True

    tag_tokens = set(slug.split("-"))
    if not tag_tokens:
        return True

    combined_noise = _tokenize_slug(name) | _source_tokens(source) | SOURCE_NOISE_WORDS
    meaningful_tokens = {token for token in tag_tokens if token not in combined_noise}

    if not meaningful_tokens and len(tag_tokens) >= 3:
        return True

    if {"chef", "recipe"}.issubset(tag_tokens):
        return True

    return False


def _has_phrase(text: str, phrase: str) -> bool:
    return re.search(rf"\b{re.escape(phrase)}\b", text) is not None


def _has_protein(text: str, protein: str) -> bool:
    if not _has_phrase(text, protein):
        return False
    exclusion = PROTEIN_EXCLUSION_PATTERNS.get(protein)
    if exclusion and exclusion.search(text):
        stripped = exclusion.sub("", text)
        return _has_phrase(stripped, protein)
    return True


def curate_recipe_tags(
    *,
    name: str,
    description: str,
    items: list[dict[str, Any]] | None,
    source: str = "",
    existing_tags: list[str] | None = None,
) -> list[str]:
    item_text = _flatten_texts(items)
    haystack = " ".join(
        part for part in [
            str(name or ""),
            str(description or ""),
            item_text,
            str(source or ""),
        ]
        if part
    ).lower()
    protein_haystack = " ".join(
        part for part in [
            str(name or ""),
            item_text,
            str(source or ""),
        ]
        if part
    ).lower()

    inferred: list[str] = []
    inferred_seen: set[str] = set()

    def add_inferred(tag: str) -> None:
        slug = slugify_tag(tag)
        if slug and slug not in inferred_seen:
            inferred_seen.add(slug)
            inferred.append(slug)

    for key, expanded in CUISINE_TAGS.items():
        if _has_phrase(haystack, key):
            for tag in expanded:
                add_inferred(tag)

    for key, expanded in CUISINE_PHRASE_HINTS.items():
        if _has_phrase(haystack, key):
            for tag in expanded:
                add_inferred(tag)

    for key, tag in COURSE_HINTS.items():
        if _has_phrase(haystack, key):
            add_inferred(tag)

    for key, tag in METHOD_HINTS.items():
        if _has_phrase(haystack, key):
            add_inferred(tag)

    for key, expanded in DISH_HINTS.items():
        if _has_phrase(haystack, key):
            for tag in expanded:
                add_inferred(tag)

    for key, tag in PROTEIN_TAGS.items():
        if _has_protein(protein_haystack, key):
            add_inferred(tag)

    if "mango sticky rice" in haystack:
        add_inferred("thai")
        add_inferred("dessert")
        add_inferred("sweet")
        add_inferred("rice")

    if "pasta chips" in haystack:
        add_inferred("snack")
        add_inferred("pasta")
        add_inferred("savory")

    if "kibbeh" in haystack:
        add_inferred("middle-eastern")
        add_inferred("lebanese")
        add_inferred("main-course")

    if "larb" in haystack or "laab" in haystack:
        add_inferred("thai")
        add_inferred("main-course")

    if "curry" in haystack:
        add_inferred("curry")
        add_inferred("main-course")

    if "pasta" in haystack:
        add_inferred("pasta")

    sweet_score = sum(1 for hint in SWEET_HINTS if hint in haystack)
    savory_score = sum(1 for hint in SAVORY_HINTS if hint in haystack)
    if sweet_score > savory_score and sweet_score > 0:
        add_inferred("sweet")
    elif savory_score > 0:
        add_inferred("savory")

    if "dessert" not in inferred_seen and "sweet" not in inferred_seen and not any(
        tag in inferred_seen for tag in COURSE_HINTS.values()
    ):
        add_inferred("main-course")

    curated: list[str] = []
    seen: set[str] = set()

    def add_final(tag: str) -> None:
        slug = slugify_tag(tag)
        if slug and slug not in seen:
            seen.add(slug)
            curated.append(slug)

    for tag in existing_tags or []:
        if _is_boilerplate_existing_tag(tag, name, source):
            continue
        slug = slugify_tag(tag)
        if slug in MANAGED_TAGS and slug not in inferred_seen:
            continue
        add_final(tag)

    for tag in inferred:
        add_final(tag)

    return curated[:20]
