import json
import re
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any

import click

EXPORT_DECL_RE = re.compile(r"export\s+(?:function|const|class)\s+([A-Z][a-zA-Z0-9]*)")
EXPORT_BLOCK_RE = re.compile(r"export\s*\{([^}]+)\}")


def _to_kebab(name: str) -> str:
    s = re.sub(r"([a-z])([A-Z])", r"\1-\2", name)
    s = re.sub(r"([A-Z0-9]+)([A-Z][a-z])", r"\1-\2", s)
    return s.lower()


def _extract_exports(content: str) -> set[str]:
    """Parse source content for PascalCase export names."""
    exports = set()
    for m in EXPORT_DECL_RE.finditer(content):
        exports.add(m.group(1))
    for m in EXPORT_BLOCK_RE.finditer(content):
        for name in m.group(1).split(","):
            trimmed = name.strip().split(" as ")[-1].strip()
            if trimmed and trimmed[0].isupper():
                exports.add(trimmed)
    return exports


def _kebab_candidates(pascal_name: str) -> list[str]:
    """Generate candidate component names from a PascalCase tag.

    CardHeader -> ["card-header", "card"]
    Badge -> ["badge"]
    """
    kebab = _to_kebab(pascal_name)
    parts = kebab.split("-")
    return ["-".join(parts[:i]) for i in range(len(parts), 0, -1)]


def _fetch_json(url: str) -> dict[str, Any] | None:
    try:
        with urllib.request.urlopen(url) as resp:
            return json.loads(resp.read())  # type: ignore[no-any-return]
    except (urllib.error.URLError, json.JSONDecodeError):
        return None


def search_registries(
    missing_exports: set[str], project_dir: Path
) -> tuple[list[str], list[str]]:
    """Search registries for components that export the missing names.

    Returns (resolved, unresolved) where resolved is installable names
    like ["card", "@n6k/duckdb-test"] and unresolved is PascalCase names
    that couldn't be found.
    """
    components_json = project_dir / "components.json"
    if not components_json.exists():
        return [], sorted(missing_exports)

    config = json.loads(components_json.read_text())
    style = config.get("style", "default")

    # Build registry sources: (namespace_or_none, url_template, index_url)
    sources: list[tuple[str | None, str, str]] = []

    for namespace, url_template in config.get("registries", {}).items():
        index_url = url_template.replace("/{name}.json", "/registry.json")
        sources.append((namespace, url_template, index_url))

    shadcn_template = f"https://ui.shadcn.com/r/styles/{style}/{{name}}.json"
    shadcn_index = f"https://ui.shadcn.com/r/styles/{style}/registry.json"
    sources.append((None, shadcn_template, shadcn_index))

    # Fetch each registry index once
    registries: list[tuple[str | None, str, set[str]]] = []
    for namespace, url_template, index_url in sources:
        click.echo(f"Fetching registry index: {index_url}")
        data = _fetch_json(index_url)
        if data is None:
            click.echo(f"  Warning: could not fetch {index_url}, skipping", err=True)
            continue
        item_names = {item["name"] for item in data.get("items", [])}
        registries.append((namespace, url_template, item_names))

    resolved_components: set[str] = set()
    resolved_exports: set[str] = set()
    component_cache: dict[tuple[str | None, str], set[str]] = {}

    for export_name in sorted(missing_exports):
        if export_name in resolved_exports:
            continue

        candidates = _kebab_candidates(export_name)
        found = False

        for namespace, url_template, item_names in registries:
            for candidate in candidates:
                if candidate not in item_names:
                    continue

                cache_key = (namespace, candidate)
                if cache_key not in component_cache:
                    url = url_template.replace("{name}", candidate)
                    data = _fetch_json(url)
                    if data is None:
                        continue
                    exports = set()
                    for f in data.get("files", []):
                        exports |= _extract_exports(f.get("content", ""))
                    component_cache[cache_key] = exports

                if export_name in component_cache[cache_key]:
                    install_name = f"{namespace}/{candidate}" if namespace else candidate
                    resolved_components.add(install_name)
                    resolved_exports |= component_cache[cache_key]
                    click.echo(f"  {export_name} -> {install_name}")
                    found = True
                    break

            if found:
                break

    unresolved = sorted(missing_exports - resolved_exports)
    return sorted(resolved_components), unresolved
