"""CSS and HTML extraction heuristics for URL-derived branding."""

from __future__ import annotations

import re
import urllib.parse
from typing import Any, Dict, List, Optional, Tuple


def find_css_urls(html: str, base_url: str) -> List[str]:
    """Find CSS file URLs in HTML."""
    css_urls = []

    # Find <link rel="stylesheet" href="...">
    link_pattern = re.compile(
        r'<link[^>]+rel=["\']stylesheet["\'][^>]+href=["\']([^"\']+)["\']|'
        r'<link[^>]+href=["\']([^"\']+)["\'][^>]+rel=["\']stylesheet["\']',
        re.IGNORECASE,
    )

    for match in link_pattern.finditer(html):
        href = match.group(1) or match.group(2)
        if href:
            # Resolve relative URLs
            full_url = urllib.parse.urljoin(base_url, href)
            css_urls.append(full_url)

    return css_urls


def extract_inline_css(html: str) -> str:
    """Extract inline <style> content from HTML."""
    style_pattern = re.compile(r"<style[^>]*>(.*?)</style>", re.DOTALL | re.IGNORECASE)
    styles = []
    for match in style_pattern.finditer(html):
        styles.append(match.group(1))
    return "\n".join(styles)


def extract_site_name(html: str, url: str) -> str:
    """Extract site name from HTML or URL."""
    # Try <title>
    title_match = re.search(r"<title>([^<]+)</title>", html, re.IGNORECASE)
    if title_match:
        title = title_match.group(1).strip()
        # Clean up common patterns
        title = re.split(r"\s*[-|–—]\s*", title)[0].strip()
        if title and len(title) < 50:
            return title

    # Try og:site_name
    og_match = re.search(
        r'property=["\']og:site_name["\'][^>]+content=["\']([^"\']+)["\']', html
    )
    if og_match:
        return og_match.group(1).strip()

    # Fall back to domain
    parsed = urllib.parse.urlparse(url)
    domain = parsed.netloc.replace("www.", "")
    return domain.split(".")[0].title()


def detect_locale(html: str, url: str) -> str:
    """Detect locale from HTML or URL."""
    # First check TLD - more reliable for business sites
    parsed = urllib.parse.urlparse(url)
    tld = parsed.netloc.split(".")[-1].upper()
    tld_map = {
        "NL": "NL",
        "DE": "DE",
        "FR": "FR",
        "UK": "GB",
        "EU": "NL",
        "JP": "JP",
        "CN": "CN",
        "BE": "BE",
        "AT": "AT",
        "CH": "CH",
        "IT": "IT",
        "ES": "ES",
    }
    if tld in tld_map:
        return tld_map[tld]

    # Check <html lang="...">
    lang_match = re.search(
        r'<html[^>]+lang=["\']([a-z]{2}(?:-[A-Z]{2})?)["\']', html, re.IGNORECASE
    )
    if lang_match:
        lang = lang_match.group(1).upper()
        if "-" in lang:
            return lang.split("-")[1]
        # Map language to likely country
        lang_map = {
            "EN": "US",
            "NL": "NL",
            "DE": "DE",
            "FR": "FR",
            "ES": "ES",
            "IT": "IT",
            "PT": "PT",
            "JA": "JP",
        }
        return lang_map.get(lang[:2], "US")

    return "US"


def infer_brand_tone(css: str, colors: Dict[str, str]) -> float:
    """Infer brand tone from CSS (0=formal, 1=casual)."""
    tone = 0.5

    # Large border-radius suggests casual
    radius_matches = re.findall(r"border-radius:\s*(\d+)", css)
    if radius_matches:
        avg_radius = sum(int(r) for r in radius_matches) / len(radius_matches)
        if avg_radius > 20:
            tone += 0.2
        elif avg_radius < 5:
            tone -= 0.2

    # Bright, saturated colors suggest casual
    # Dark, desaturated colors suggest formal
    # (simplified heuristic)

    return max(0, min(1, tone))


def infer_type_scale(typography: Dict[str, Dict]) -> float:
    """Infer type scale ratio from extracted typography."""
    sizes = []
    for name, props in typography.items():
        if "fontSize" in props:
            dim = props["fontSize"]
            if isinstance(dim, dict) and "value" in dim:
                val = dim["value"]
                # Ensure val is numeric
                try:
                    val = float(val)
                except (ValueError, TypeError):
                    continue
                unit = dim.get("unit", "px")
                # Normalize to px
                if unit == "rem":
                    val *= 16
                elif unit == "em":
                    val *= 16
                elif unit == "pt":
                    val *= 1.333
                sizes.append(val)

    if len(sizes) >= 2:
        sizes.sort()
        # Calculate average ratio between adjacent sizes
        ratios = [
            sizes[i + 1] / sizes[i] for i in range(len(sizes) - 1) if sizes[i] > 0
        ]
        if ratios:
            avg_ratio = sum(ratios) / len(ratios)
            # Clamp to reasonable range
            return max(1.1, min(1.5, avg_ratio))

    return 1.25  # Default: major third


def select_primary_color(colors: Dict[str, str]) -> str:
    """Select the most likely primary brand color."""
    # Priority: header/nav background gradient, accent colors

    # First: look for gradient start colors in header/nav (these are usually the brand color)
    for name, color in colors.items():
        name_lower = name.lower()
        if (
            ("header" in name_lower or "nav" in name_lower)
            and "background" in name_lower
            and "0" in name
        ):
            # Skip white/near-white
            if color.startswith("#"):
                r, g, b = int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)
                if r < 240 or g < 240 or b < 240:  # Not near-white
                    return color

    # Second: look for any gradient start that's a saturated color
    for name, color in colors.items():
        if "gradient" in name.lower() and "0" in name and "background" in name.lower():
            if color.startswith("#") and len(color) == 7:
                r, g, b = int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)
                # Must be saturated (not grayscale) and not too light
                saturation = max(r, g, b) - min(r, g, b)
                brightness = (r + g + b) / 3
                if saturation > 30 and brightness < 220:
                    return color

    # Third: explicit brand/primary keywords
    priority_keywords = ["brand", "primary", "accent1", "main"]
    for keyword in priority_keywords:
        for name, color in colors.items():
            if keyword in name.lower():
                if color.startswith("#") and len(color) == 7:
                    r, g, b = (
                        int(color[1:3], 16),
                        int(color[3:5], 16),
                        int(color[5:7], 16),
                    )
                    if max(r, g, b) - min(r, g, b) > 30:  # Saturated
                        return color

    # Fourth: any saturated, non-light color
    for name, color in colors.items():
        if color.startswith("#") and len(color) == 7:
            r, g, b = int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)
            saturation = max(r, g, b) - min(r, g, b)
            brightness = (r + g + b) / 3
            # Good candidate: saturated and not too bright
            if saturation > 50 and brightness < 180:
                return color

    return "#3B82F6"  # Default blue


def select_secondary_color(colors: Dict[str, str], primary: str) -> str:
    """Select secondary/accent color."""
    priority_keywords = ["secondary", "accent", "cta", "button", "link"]

    for keyword in priority_keywords:
        for name, color in colors.items():
            if keyword in name.lower() and color != primary:
                return color

    # Look for gradient end
    for name, color in colors.items():
        if "gradient" in name.lower() and "1" in name and color != primary:
            return color

    # Lighten primary as fallback
    if primary.startswith("#"):
        from tokenmoulds.colors.transforms import lighten

        return lighten(primary, 0.3)

    return "#60A5FA"


def select_fonts(typography: Dict[str, Dict]) -> Tuple[str, str]:
    """Select heading and body fonts."""
    heading_font = None
    body_font = None

    for name, props in typography.items():
        font = props.get("fontFamily")
        if not font:
            continue

        name_lower = name.lower()
        if "heading" in name_lower or name_lower.startswith("h"):
            heading_font = font
        elif "body" in name_lower or name_lower == "p":
            body_font = font

    # Defaults
    if not heading_font:
        heading_font = body_font or "Georgia"
    if not body_font:
        body_font = "Arial"

    return heading_font, body_font


def extract_all_hex_colors(css_content: str) -> List[Tuple[str, int]]:
    """Extract all hex colors from CSS with frequency counts.

    Args:
        css_content: Raw CSS content

    Returns:
        List of (color, count) tuples sorted by frequency
    """
    from collections import Counter

    # Find all 6-digit hex colors
    hex_pattern = re.compile(r"#([0-9a-fA-F]{6})(?![0-9a-fA-F])")
    colors = hex_pattern.findall(css_content)

    # Normalize to uppercase and count
    counter = Counter(f"#{c.upper()}" for c in colors)
    return counter.most_common()


def score_brand_color(color: str, frequency: int) -> float:
    """Score a color for likelihood of being a brand color.

    Higher scores = more likely brand color.
    Brand colors are typically: saturated, not too light, not too dark.

    Args:
        color: Hex color string
        frequency: How often it appears

    Returns:
        Score (higher = better candidate)
    """
    try:
        r, g, b = int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)
    except (ValueError, IndexError):
        return 0

    # Calculate saturation (0-255 range difference)
    saturation = max(r, g, b) - min(r, g, b)

    # Calculate brightness (average)
    brightness = (r + g + b) / 3

    score = 0.0

    # Saturated colors score high (brand colors are colorful)
    if saturation > 100:
        score += 50
    elif saturation > 50:
        score += 30
    elif saturation < 20:
        # Grayscale - unlikely brand color
        return 0

    # Mid-brightness scores best (not too light, not too dark)
    if 60 < brightness < 200:
        score += 30
    elif brightness >= 200:
        # Too light (near white)
        score -= 20
    elif brightness <= 60:
        # Dark colors can be secondary
        score += 10

    # Frequency bonus (common colors are likely intentional)
    score += min(frequency * 2, 20)

    # Skip common UI grays
    if saturation < 30 and 100 < brightness < 200:
        return 0

    return score


def extract_fonts_from_css(css_content: str) -> List[str]:
    """Extract font family names from CSS @font-face and font-family rules.

    Args:
        css_content: Raw CSS content

    Returns:
        List of font family names found
    """
    fonts = set()

    # Extract from @font-face declarations
    fontface_pattern = re.compile(
        r"@font-face\s*\{[^}]*font-family\s*:\s*['\"]?([^'\";}]+)['\"]?",
        re.IGNORECASE | re.DOTALL,
    )
    for match in fontface_pattern.finditer(css_content):
        font = match.group(1).strip()
        if font:
            fonts.add(font)

    # Extract from font-family declarations
    family_pattern = re.compile(
        r"font-family\s*:\s*['\"]?([^'\";}]+)['\"]?", re.IGNORECASE
    )
    for match in family_pattern.finditer(css_content):
        families = match.group(1).split(",")
        for family in families:
            family = family.strip().strip("\"'")
            # Skip generic families and CSS variables
            if family.lower() not in (
                "serif",
                "sans-serif",
                "monospace",
                "cursive",
                "fantasy",
                "system-ui",
            ) and not family.startswith("var("):
                fonts.add(family)

    return list(fonts)


def normalize_font_name(font_value: str) -> Optional[str]:
    """Normalize a font filename/variable to a standard family name.

    Maps font file patterns to their family names:
    - "InterTight-Medium" -> "Inter"
    - "Roboto-Bold" -> "Roboto"

    Args:
        font_value: Font filename or family name

    Returns:
        Normalized font family name or None
    """
    if not font_value:
        return None

    value = font_value.strip()
    value_lower = value.lower()

    # Skip CSS variables and generic families
    if value.startswith("var(") or value_lower in ("serif", "sans-serif", "monospace"):
        return None

    # Common font family mappings (file name patterns -> family)
    font_mappings = [
        # Google Fonts / Open Source
        (r"inter", "Inter"),
        (r"roboto", "Roboto"),
        (r"opensans|open.?sans", "Open Sans"),
        (r"montserrat", "Montserrat"),
        (r"lato", "Lato"),
        (r"poppins", "Poppins"),
        (r"nunito", "Nunito"),
        (r"source.?sans", "Source Sans Pro"),
        (r"source.?serif", "Source Serif Pro"),
        (r"source.?code", "Source Code Pro"),
        (r"fira.?sans", "Fira Sans"),
        (r"fira.?code", "Fira Code"),
        (r"jet.?brains", "JetBrains Mono"),
        (r"space.?mono", "Space Mono"),
        (r"merriweather", "Merriweather"),
        (r"playfair", "Playfair Display"),
        (r"eb.?garamond", "EB Garamond"),
        (r"lora", "Lora"),
        (r"raleway", "Raleway"),
        (r"oswald", "Oswald"),
        (r"work.?sans", "Work Sans"),
        (r"dm.?sans", "DM Sans"),
        (r"dm.?serif", "DM Serif Display"),
        (r"ibm.?plex.?sans", "IBM Plex Sans"),
        (r"ibm.?plex.?serif", "IBM Plex Serif"),
        (r"ibm.?plex.?mono", "IBM Plex Mono"),
        (r"barlow", "Barlow"),
        (r"manrope", "Manrope"),
        (r"mulish", "Mulish"),
        (r"jost", "Jost"),
        # System/standard fonts
        (r"georgia", "Georgia"),
        (r"arial", "Arial"),
        (r"helvetica", "Helvetica"),
        (r"times", "Times New Roman"),
        (r"verdana", "Verdana"),
    ]

    for pattern, family in font_mappings:
        if re.search(pattern, value_lower):
            return family

    # If no mapping found, try to clean up the name
    # Remove weight/style suffixes like "-Medium", "-Bold", "-Regular"
    cleaned = re.sub(
        r"[-_](thin|light|regular|medium|semi.?bold|bold|extra.?bold|black|italic).*$",
        "",
        value,
        flags=re.IGNORECASE,
    )
    cleaned = cleaned.strip()

    if cleaned and cleaned != value:
        # Add spaces before capital letters (InterTight -> Inter Tight)
        cleaned = re.sub(r"([a-z])([A-Z])", r"\1 \2", cleaned)
        return cleaned

    return value if value else None


def find_semantic_colors(css_content: str) -> Dict[str, str]:
    """Find colors defined in semantically-named CSS variables.

    Looks for patterns like:
    - --brand-primary: #407fff
    - --color-primary: #407fff
    - --dq-color-primary: #407fff
    - --color-marble-dark: #0a1f2d (for secondary)

    Args:
        css_content: Raw CSS/HTML content

    Returns:
        Dict mapping semantic role to color (e.g., {"primary": "#407FFF"})
    """
    semantic = {}

    # Pattern: --[prefix-]<semantic-name>: #hexcolor (6 or 8 digits)
    # Match variable names containing semantic keywords followed by hex color
    pattern = re.compile(
        r"--[a-z0-9-]*?(primary|secondary|accent|brand|main|highlight)[a-z0-9-]*?\s*:\s*#([0-9a-fA-F]{6})(?:[0-9a-fA-F]{2})?(?![0-9a-fA-F])",
        re.IGNORECASE,
    )

    for match in pattern.finditer(css_content):
        role = match.group(1).lower()
        color = f"#{match.group(2).upper()}"

        # Skip black, white, near-black, near-white
        try:
            r, g, b = int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)
            brightness = (r + g + b) / 3
            if brightness < 20 or brightness > 235:
                continue
        except ValueError:
            continue

        # Map to standard roles
        if role in ("primary", "brand", "main"):
            if "primary" not in semantic:
                semantic["primary"] = color
        elif role in ("secondary", "accent", "highlight"):
            if "secondary" not in semantic:
                semantic["secondary"] = color

    # If no secondary found, look for dark/navy/marble colors
    if "secondary" not in semantic:
        dark_pattern = re.compile(
            r"--[a-z0-9-]*?(dark|navy|marble|deep)[a-z0-9-]*?\s*:\s*#([0-9a-fA-F]{6})(?![0-9a-fA-F])",
            re.IGNORECASE,
        )
        for match in dark_pattern.finditer(css_content):
            color = f"#{match.group(2).upper()}"
            try:
                r, g, b = int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)
                brightness = (r + g + b) / 3
                # Must be actually dark
                if brightness < 80:
                    semantic["secondary"] = color
                    break
            except ValueError:
                continue

    return semantic


def extract_brand_from_raw_css(css_content: str) -> Dict[str, Any]:
    """Extract brand colors and fonts directly from raw CSS content.

    Uses a combination of:
    1. Semantic CSS variable names (--brand-primary, --color-primary, etc.)
    2. Frequency analysis and color scoring as fallback

    Args:
        css_content: Raw CSS content

    Returns:
        Dict with primary, secondary, heading_font, body_font
    """
    result: dict[str, str | None] = {
        "primary": None,
        "secondary": None,
        "heading_font": None,
        "body_font": None,
        "text_color": None,
    }

    # First: look for semantically-named colors (most reliable)
    semantic_colors = find_semantic_colors(css_content)
    if semantic_colors.get("primary"):
        result["primary"] = semantic_colors["primary"]
    if semantic_colors.get("secondary"):
        result["secondary"] = semantic_colors["secondary"]

    # Fallback: frequency + scoring analysis
    if not result["primary"]:
        color_counts = extract_all_hex_colors(css_content)

        scored_colors = []
        for color, count in color_counts:
            score = score_brand_color(color, count)
            if score > 0:
                scored_colors.append((color, score, count))

        scored_colors.sort(key=lambda x: x[1], reverse=True)

        if scored_colors:
            result["primary"] = scored_colors[0][0]

            if not result["secondary"]:
                for color, score, count in scored_colors[1:]:
                    if color != result["primary"]:
                        result["secondary"] = color
                        break

    # Extract fonts
    raw_fonts = extract_fonts_from_css(css_content)

    # Filter and normalize fonts - skip system/emoji fonts
    skip_fonts = {
        "apple color emoji",
        "segoe ui emoji",
        "segoe ui symbol",
        "noto color emoji",
        "emoji",
        "symbol",
        "blinkmacsfont",
        "blinkmacmempty",
        "system-ui",
        "ui-sans-serif",
        "ui-serif",
        "ui-monospace",
        "-apple-system",
        "blinkmacfont",
    }

    normalized_fonts = []
    for font in raw_fonts:
        norm = normalize_font_name(font)
        if norm:
            norm_lower = norm.lower()
            # Skip system/emoji fonts
            if any(skip in norm_lower for skip in skip_fonts):
                continue
            # Skip if already in list
            if norm not in normalized_fonts:
                normalized_fonts.append(norm)

    # Assign fonts (prefer common web fonts over system fonts)
    preferred_fonts = ["Inter", "Roboto", "Open Sans", "Montserrat", "Lato", "Poppins"]
    heading_font = None
    body_font = None

    for font in normalized_fonts:
        if font in preferred_fonts:
            if not heading_font:
                heading_font = font
            elif not body_font:
                body_font = font
                break

    # If no preferred fonts, use first normalized fonts
    if not heading_font and normalized_fonts:
        heading_font = normalized_fonts[0]
    if not body_font and len(normalized_fonts) > 1:
        body_font = normalized_fonts[1]
    elif not body_font:
        body_font = heading_font

    result["heading_font"] = heading_font
    result["body_font"] = body_font

    return result
