"""ParserRegistry — registry extensible que despacha parsing por extension de archivo."""

from __future__ import annotations

import re
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from base_parser import BaseParser
    from parse_result import ParseResult

# Patron para detectar URLs de repositorios de codigo.
_CODE_REPO_PATTERN = re.compile(
    r"^https?://(github\.com|gitlab\.com|bitbucket\.org)/[\w.\-]+/[\w.\-]+"
)

# Longitud maxima que puede tener un path de archivo valido (heuristica).
_MAX_PATH_LEN = 512


def _looks_like_path(source: str) -> bool:
    """Heuristica: True si source parece un path de archivo, no contenido."""
    return len(source) <= _MAX_PATH_LEN and "\n" not in source


class ParserRegistry:
    """Registry extensible que despacha parsing al parser correcto segun extension.

    Patron plugin: registrar parsers por nombre y lista de extensiones,
    resolver automaticamente al parsear un archivo. El registry mantiene
    un mapa de extension→nombre para O(1) lookup.

    Uso tipico:
        registry = ParserRegistry()
        registry.register("markdown", MarkdownParser())
        registry.register("pdf", PDFParser())
        result = await registry.parse("doc.md")
    """

    def __init__(self) -> None:
        # nombre → instancia de parser
        self._parsers: dict[str, "BaseParser"] = {}
        # extension → nombre del parser
        self._ext_map: dict[str, str] = {}

    # ------------------------------------------------------------------
    # Registro
    # ------------------------------------------------------------------

    def register(self, name: str, parser: "BaseParser") -> None:
        """Registra un parser con el nombre dado.

        Sobreescribe cualquier parser previamente registrado con el mismo nombre.
        Actualiza el mapa de extensiones automaticamente.

        Args:
            name:   Nombre unico del parser (ej: "markdown", "pdf").
            parser: Instancia de BaseParser a registrar.
        """
        self._parsers[name] = parser
        for ext in parser.supported_extensions:
            self._ext_map[ext.lower()] = name

    def unregister(self, name: str) -> None:
        """Elimina un parser del registry.

        Tambien elimina sus extensiones del mapa. Si el nombre no existe,
        no hace nada.

        Args:
            name: Nombre del parser a eliminar.
        """
        parser = self._parsers.pop(name, None)
        if parser is None:
            return
        for ext in parser.supported_extensions:
            key = ext.lower()
            if self._ext_map.get(key) == name:
                del self._ext_map[key]

    # ------------------------------------------------------------------
    # Resolucion
    # ------------------------------------------------------------------

    def get_parser(self, name: str) -> "BaseParser | None":
        """Retorna el parser registrado bajo el nombre dado.

        Args:
            name: Nombre del parser.

        Returns:
            Instancia de BaseParser o None si no existe.
        """
        return self._parsers.get(name)

    def get_parser_for_file(self, path: str) -> "BaseParser | None":
        """Resuelve el parser correcto para un archivo dado su extension.

        Args:
            path: Path del archivo (puede ser relativo o absoluto).

        Returns:
            Instancia de BaseParser o None si no hay parser para esa extension.
        """
        ext = Path(path).suffix.lower()
        name = self._ext_map.get(ext)
        if name is None:
            return None
        return self._parsers.get(name)

    # ------------------------------------------------------------------
    # Parseo
    # ------------------------------------------------------------------

    async def parse(self, source: str, **kwargs) -> "ParseResult":
        """Parsea source despachando al parser correcto segun el tipo de entrada.

        Algoritmo de despacho:
        1. Si source es URL de repo de codigo → delega al parser "code".
        2. Si source parece path de archivo (corto, sin newlines) y existe en disco:
           - Si es directorio → delega al parser "directory".
           - Resuelve parser por extension → delega.
           - Si no hay parser para la extension → fallback al parser "text".
        3. Si no parece path (contenido largo o con newlines) → parsea como
           string de contenido con el parser "text".

        Args:
            source:   Path de archivo, directorio, URL o string de contenido.
            **kwargs: Argumentos adicionales pasados al parser seleccionado.

        Returns:
            ParseResult con el arbol de nodos del documento.

        Raises:
            ValueError: Si el parser requerido no esta registrado.
        """
        # 1. URL de repositorio de codigo
        if _CODE_REPO_PATTERN.match(source):
            return await self._dispatch("code", source, **kwargs)

        # 2. Parece un path de archivo
        if _looks_like_path(source):
            p = Path(source)
            if p.exists():
                if p.is_dir():
                    return await self._dispatch("directory", source, **kwargs)
                parser = self.get_parser_for_file(source)
                if parser is not None:
                    return await parser.parse(source, **kwargs)
                # Fallback a text si no hay parser para la extension
                return await self._dispatch_content("text", source, **kwargs)

        # 3. Es contenido directo (string largo o con newlines)
        text_parser = self._parsers.get("text")
        if text_parser is None:
            raise ValueError(
                "No hay parser 'text' registrado. Registra un TextParser primero."
            )
        return await text_parser.parse_content(source, **kwargs)

    # ------------------------------------------------------------------
    # Introspeccion
    # ------------------------------------------------------------------

    def list_parsers(self) -> list[str]:
        """Retorna los nombres de todos los parsers registrados.

        Returns:
            Lista de nombres de parsers, ordenada alfabeticamente.
        """
        return sorted(self._parsers.keys())

    def list_supported_extensions(self) -> list[str]:
        """Retorna todas las extensiones soportadas por parsers registrados.

        Returns:
            Lista de extensiones (con punto, lowercase), ordenada alfabeticamente.
        """
        return sorted(self._ext_map.keys())

    # ------------------------------------------------------------------
    # Helpers internos
    # ------------------------------------------------------------------

    async def _dispatch(self, name: str, source: str, **kwargs) -> "ParseResult":
        """Despacha a un parser por nombre via parse()."""
        parser = self._parsers.get(name)
        if parser is None:
            raise ValueError(
                f"Parser '{name}' no esta registrado. "
                f"Parsers disponibles: {self.list_parsers()}"
            )
        return await parser.parse(source, **kwargs)

    async def _dispatch_content(
        self, name: str, content: str, **kwargs
    ) -> "ParseResult":
        """Despacha a un parser por nombre via parse_content()."""
        parser = self._parsers.get(name)
        if parser is None:
            raise ValueError(
                f"Parser '{name}' no esta registrado. "
                f"Parsers disponibles: {self.list_parsers()}"
            )
        return await parser.parse_content(content, **kwargs)


# ------------------------------------------------------------------
# Singleton global
# ------------------------------------------------------------------

_registry: ParserRegistry | None = None


def get_registry() -> ParserRegistry:
    """Retorna la instancia global del ParserRegistry (lazy-initialized).

    Returns:
        Instancia singleton de ParserRegistry.
    """
    global _registry
    if _registry is None:
        _registry = ParserRegistry()
    return _registry
