"""
Shared utility functions for card directives.

Contains helper functions for column/gap normalization, link resolution,
icon rendering, HTML escaping, and child collection.
"""

from __future__ import annotations

import re
from collections.abc import Callable
from typing import Any

from bengal.rendering.pipeline.thread_local import get_thread_parser
from bengal.rendering.template_functions.strings import first_sentence
from bengal.utils.observability.logger import get_logger
from bengal.utils.primitives.text import escape_html

logger = get_logger(__name__)

# Valid option values
VALID_LAYOUTS = frozenset(["default", "horizontal", "portrait", "compact"])
VALID_GAPS = frozenset(["small", "medium", "large"])
VALID_STYLES = frozenset(["default", "minimal", "bordered"])
VALID_COLORS = frozenset(
    [
        "blue",
        "green",
        "red",
        "yellow",
        "orange",
        "purple",
        "gray",
        "pink",
        "indigo",
        "teal",
        "cyan",
        "violet",
    ]
)


def normalize_columns(columns: str) -> str:
    """Normalize columns specification."""
    columns = str(columns).strip()

    if columns in ("auto", ""):
        return "auto"

    if columns.isdigit():
        num = int(columns)
        return str(num) if 1 <= num <= 6 else "auto"

    if "-" in columns:
        parts = columns.split("-")
        if all(p.isdigit() and 1 <= int(p) <= 6 for p in parts) and len(parts) in (2, 3, 4):
            return columns

    return "auto"


def extract_octicon(title: str) -> tuple[str, str]:
    """Extract octicon from title."""
    pattern = r"\{octicon\}`([^;`]+)(?:;[^`]*)?`\s*"
    match = re.search(pattern, title)

    if match:
        icon_name = match.group(1).strip()
        clean_title = re.sub(pattern, "", title).strip()
        return icon_name, clean_title

    return "", title


def pull_from_linked_page(renderer: Any, link: str, fields: list[str]) -> dict[str, Any]:
    """Pull metadata from a linked page."""
    page = None

    # Object tree for relative paths
    if link.startswith("./"):
        current_page = getattr(renderer, "_current_page", None)
        if current_page:
            section = getattr(current_page, "_section", None)
            if section:
                child_name = link[2:].rstrip("/").split("/")[0]

                for subsection in getattr(section, "subsections", []):
                    if getattr(subsection, "name", "") == child_name:
                        page = getattr(subsection, "index_page", None)
                        if page:
                            break

                if not page:
                    for p in getattr(section, "pages", []):
                        source_str = str(getattr(p, "source_path", ""))
                        if f"/{child_name}." in source_str or f"/{child_name}/" in source_str:
                            page = p
                            break

    # Fall back to xref_index
    if not page:
        xref_index = getattr(renderer, "_xref_index", None)
        current_page_dir = getattr(renderer, "_current_page_dir", None)
        if xref_index:
            page = resolve_page(xref_index, link, current_page_dir)

    if not page:
        return {}

    return extract_page_fields(page, fields)


def extract_page_fields(page: Any, fields: list[str]) -> dict[str, Any]:
    """Extract requested fields from a page object."""
    result: dict[str, Any] = {}

    for field in fields:
        if field == "title":
            result["title"] = getattr(page, "title", "")
        elif field == "description":
            result["description"] = (
                page.metadata.get("description", "") if hasattr(page, "metadata") else ""
            )
        elif field == "icon":
            result["icon"] = page.metadata.get("icon", "") if hasattr(page, "metadata") else ""
        elif field == "image":
            result["image"] = page.metadata.get("image", "") if hasattr(page, "metadata") else ""
        elif field == "badge":
            result["badge"] = page.metadata.get("badge", "") if hasattr(page, "metadata") else ""

    return result


def resolve_page(xref_index: dict[str, Any], link: str, current_page_dir: str | None = None) -> Any:
    """Resolve a link to a page object.

    Delegates to bengal.utils.xref.resolve_page (single source of truth).
    """
    from bengal.utils.xref import resolve_page as _resolve_page

    return _resolve_page(xref_index, link, current_page_dir)


def resolve_link_url(renderer: Any, link: str) -> str:
    """Resolve a link reference to a URL (includes baseurl for absolute paths)."""
    # External URLs - return as-is
    if link.startswith(("http://", "https://")):
        return link

    # Site-relative paths need baseurl applied
    if link.startswith("/"):
        site = getattr(renderer, "_site", None)
        if site:
            from bengal.rendering.utils.url import apply_baseurl

            return apply_baseurl(link, site)
        return link

    # Try to resolve as page reference
    xref_index = getattr(renderer, "_xref_index", None)
    if not xref_index:
        return link

    current_page_dir = getattr(renderer, "_current_page_dir", None)
    page = resolve_page(xref_index, link, current_page_dir)

    if page and hasattr(page, "href"):
        return page.href

    return link


def render_icon(icon_name: str, card_title: str = "") -> str:
    """
    Render icon using Bengal SVG icons.

    Args:
        icon_name: Name of the icon to render
        card_title: Title of the card (for warning context)

    Returns:
        SVG HTML string, or empty string if not found

    """
    from bengal.icons.svg import render_icon as _render_icon
    from bengal.icons.svg import warn_missing_icon

    icon_html = _render_icon(icon_name, size=20)

    if not icon_html and icon_name:
        warn_missing_icon(icon_name, directive="card", context=card_title)

    return icon_html


def _ensure_absolute_url(
    url: str,
    xref_index: dict[str, Any] | None,
    current_page_dir: str | None,
) -> str:
    """Resolve relative URLs to absolute when content may be embedded (e.g. in tracks)."""
    if not url:
        return url
    if url.startswith(("http://", "https://", "//", "/")):
        return url
    if not xref_index or not current_page_dir:
        return url
    from bengal.utils.xref import resolve_link_to_url_and_page

    resolved, _ = resolve_link_to_url_and_page(xref_index, url, current_page_dir)
    return resolved if resolved else url


def collect_children(
    section: Any,
    current_page: Any,
    include: str,
    xref_index: dict[str, Any] | None = None,
    current_page_dir: str | None = None,
) -> list[dict[str, Any]]:
    """Collect child sections/pages from section."""
    children: list[dict[str, Any]] = []

    if include in ("sections", "all"):
        for subsection in getattr(section, "subsections", []):
            # Skip hidden sections
            if hasattr(subsection, "metadata") and subsection.metadata.get("hidden", False):
                continue
            has_weight = hasattr(subsection, "metadata") and "weight" in subsection.metadata
            url = get_section_url(subsection)
            url = _ensure_absolute_url(url, xref_index, current_page_dir)
            children.append(
                {
                    "type": "section",
                    "title": getattr(subsection, "title", subsection.name),
                    "description": (
                        subsection.metadata.get("description", "")
                        if hasattr(subsection, "metadata")
                        else ""
                    ),
                    "icon": (
                        subsection.metadata.get("icon", "")
                        if hasattr(subsection, "metadata")
                        else ""
                    ),
                    "url": url,
                    "weight": (
                        subsection.metadata.get("weight", 0)
                        if hasattr(subsection, "metadata")
                        else 0
                    ),
                    "_has_explicit_weight": has_weight,
                }
            )

    if include in ("pages", "all"):
        for page in getattr(section, "pages", []):
            source_str = str(getattr(page, "source_path", ""))
            if source_str.endswith(("_index.md", "index.md")):
                continue
            if (
                hasattr(current_page, "source_path")
                and hasattr(page, "source_path")
                and page.source_path == current_page.source_path
            ):
                continue
            # Skip hidden pages
            if hasattr(page, "metadata") and page.metadata.get("hidden", False):
                continue
            has_weight = hasattr(page, "metadata") and "weight" in page.metadata
            url = getattr(page, "href", "")
            url = _ensure_absolute_url(url, xref_index, current_page_dir)
            children.append(
                {
                    "type": "page",
                    "title": getattr(page, "title", ""),
                    "description": (
                        page.metadata.get("description", "") if hasattr(page, "metadata") else ""
                    ),
                    "icon": page.metadata.get("icon", "") if hasattr(page, "metadata") else "",
                    "url": url,
                    "weight": page.metadata.get("weight", 0) if hasattr(page, "metadata") else 0,
                    "_has_explicit_weight": has_weight,
                }
            )

    # Warn if some children have weights and others don't (likely unintentional)
    warn_mixed_weights(children, current_page)

    # Sort by weight, then title
    children.sort(key=lambda c: (c.get("weight", 0), c.get("title", "").lower()))

    # Clean up internal tracking field
    for child in children:
        child.pop("_has_explicit_weight", None)

    return children


def warn_mixed_weights(children: list[dict[str, Any]], current_page: Any) -> None:
    """Warn if some children have explicit weights and others don't."""
    if len(children) < 2:
        return

    with_weight = [c for c in children if c.get("_has_explicit_weight")]
    without_weight = [c for c in children if not c.get("_has_explicit_weight")]

    # Only warn if there's a mix (some have weights, some don't)
    if with_weight and without_weight:
        page_path = getattr(current_page, "source_path", "unknown") if current_page else "unknown"
        missing_titles = ", ".join(c.get("title", "Untitled") for c in without_weight[:3])
        if len(without_weight) > 3:
            missing_titles += f" (+{len(without_weight) - 3} more)"

        logger.warning(
            "child_cards_mixed_weights",
            page=str(page_path),
            weighted=len(with_weight),
            unweighted=len(without_weight),
            unweighted_items=missing_titles,
            hint="Unweighted items default to weight=0 and sort first. "
            "Add 'weight:' to frontmatter for consistent ordering.",
        )


def get_section_url(section: Any) -> str:
    """Get URL for a section."""
    if hasattr(section, "index_page") and section.index_page:
        return getattr(section.index_page, "href", "/")
    path = getattr(section, "path", None)
    if path:
        return f"/{path}/"
    return "/"


def render_child_card(
    child: dict[str, Any],
    fields: list[str],
    layout: str,
    escape_html: Callable[[str], str],
) -> str:
    """Render a single card for a child section/page."""
    title = child.get("title", "") if "title" in fields else ""
    description = child.get("description", "") if "description" in fields else ""
    icon = child.get("icon", "") if "icon" in fields else ""
    url = child.get("url", "")
    child_type = child.get("type", "page")

    # Fallback icon
    if not icon and "icon" in fields:
        icon = "folder" if child_type == "section" else "file"

    classes = ["card"]
    if layout:
        classes.append(f"card-layout-{layout}")
    class_str = " ".join(classes)

    parts = [f'<a class="{class_str}" href="{escape_html(url)}">']

    if icon or title:
        parts.append('  <div class="card-header">')
        if icon:
            rendered_icon = render_icon(icon, card_title=title)
            if rendered_icon:
                parts.append(f'    <span class="card-icon" data-icon="{escape_html(icon)}">')
                parts.append(f"      {rendered_icon}")
                parts.append("    </span>")
        if title:
            parts.append(f'    <div class="card-title">{escape_html(title)}</div>')
        parts.append("  </div>")

    if description:
        parts.append('  <div class="card-content">')
        parts.append(f"    {_render_description_html(description)}")
        parts.append("  </div>")

    parts.append("</a>")

    return "\n".join(parts) + "\n"


def _render_description_html(description: str) -> str:
    """
    Render a short markdown description safely for card content.

    - Truncates to the first sentence to avoid dumping full docstrings.
    - Parses with the configured markdown parser so inline code/bullets render.
    - Falls back to HTML-escaped text if markdown parsing fails.
    """
    if not description:
        return ""

    # Keep cards succinct; docstrings can be long.
    desc = first_sentence(description, max_length=240)

    try:
        parser = get_thread_parser()
        html = parser.parse(desc, metadata={}).strip()

        # If the parser wraps the content in a single <p>, strip the wrapper
        # so we don't nest paragraphs inside the card-content block.
        if html.startswith("<p>") and html.endswith("</p>"):
            inner = html[3:-4].strip()
            # Only strip if nothing else wraps the content
            if "<p" not in inner and "</p>" not in inner:
                return inner
        return html
    except Exception:
        # Last-resort safe escape
        return f"<p>{escape_html(desc)}</p>"


__all__ = [
    "VALID_COLORS",
    "VALID_GAPS",
    "VALID_LAYOUTS",
    "VALID_STYLES",
    "collect_children",
    "escape_html",
    "extract_octicon",
    "extract_page_fields",
    "get_section_url",
    "normalize_columns",
    "pull_from_linked_page",
    "render_child_card",
    "render_icon",
    "resolve_link_url",
    "resolve_page",
]
