"""
Rejestr backendów i narzędzi.

Odpowiedzialność:
- Przechowuje listę zarejestrowanych backendów (id, url)
- Mapuje publiczną nazwę narzędzia na backend i oryginalną nazwę narzędzia
- Udostępnia metody: add_backend, remove_backend, register_tool, resolve(tool_name), list_tools
- Nie wykonuje żadnych połączeń sieciowych — czysta logika danych
- Fundament systemu, od którego zależą Executor i Gateway
"""

import re

from .models import (
    Backend,
    ResourceDescriptor,
    ResourceMapping,
    ResourceTemplateDescriptor,
    ResourceTemplateMapping,
    ToolDefinition,
    ToolMapping,
)


# RFC 6570 simple expansion is enough for our backends — `{name}` placeholders
# only, no operators (`{+x}`, `{?x}`). One placeholder spans up to the next `/`
# or end-of-URI. Anything else in the template is matched literally.
_TEMPLATE_VAR_RE = re.compile(r"\{([A-Za-z_][A-Za-z0-9_]*)\}")


def _template_to_regex(template: str) -> re.Pattern[str]:
    parts: list[str] = []
    cursor = 0
    for m in _TEMPLATE_VAR_RE.finditer(template):
        parts.append(re.escape(template[cursor:m.start()]))
        parts.append(r"[^/]+")
        cursor = m.end()
    parts.append(re.escape(template[cursor:]))
    return re.compile("^" + "".join(parts) + "$")


class Registry:
    def __init__(self):
        self._backends: dict[str, Backend] = {}
        self._tools: dict[str, ToolMapping] = {}
        self._resources: dict[str, ResourceMapping] = {}
        # Templates kept in declaration order; first-match wins on resolve.
        # Each entry: (compiled_regex, ResourceTemplateMapping). Compile once.
        self._resource_templates: list[tuple[re.Pattern[str], ResourceTemplateMapping]] = []

    def add_backend(self, backend: Backend) -> None:
        self._backends[backend.id] = backend

    def remove_backend(self, backend_id: str) -> None:
        self._backends.pop(backend_id, None)
        self.remove_backend_tools(backend_id)
        self.remove_backend_resources(backend_id)
        self.remove_backend_resource_templates(backend_id)

    def remove_backend_tools(self, backend_id: str) -> None:
        """Usuwa narzędzia backendu, zachowując wpis backendu w rejestrze."""
        self._tools = {
            name: mapping
            for name, mapping in self._tools.items()
            if mapping.backend.id != backend_id
        }

    def remove_backend_resources(self, backend_id: str) -> None:
        """Usuwa zasoby backendu, zachowując wpis backendu w rejestrze."""
        self._resources = {
            uri: mapping
            for uri, mapping in self._resources.items()
            if mapping.backend.id != backend_id
        }

    def remove_backend_resource_templates(self, backend_id: str) -> None:
        """Usuwa resource templates backendu (przy disconnect/refresh)."""
        self._resource_templates = [
            entry for entry in self._resource_templates
            if entry[1].backend.id != backend_id
        ]

    def has_backend(self, backend_id: str) -> bool:
        return backend_id in self._backends

    def register_tool(
        self,
        backend: Backend,
        definition: ToolDefinition,
        public_name: str | None = None,
    ) -> None:
        name = public_name or definition.name
        existing = self._tools.get(name)
        if existing and existing.backend.id != backend.id:
            raise RuntimeError(
                f"Tool name collision: {name} already registered by backend "
                f"{existing.backend.id}, refused registration from {backend.id}"
            )
        self._tools[name] = ToolMapping(
            public_name=name,
            backend=backend,
            original_name=definition.name,
            definition=definition,
        )

    def resolve(self, public_name: str) -> ToolMapping:
        mapping = self._tools.get(public_name)
        if not mapping:
            raise KeyError(f"Unknown tool: {public_name}")
        return mapping

    def list_tools(self) -> list[ToolMapping]:
        return list(self._tools.values())

    def get_backend(self, backend_id: str) -> Backend:
        backend = self._backends.get(backend_id)
        if not backend:
            raise KeyError(f"Unknown backend: {backend_id}")
        return backend

    def register_resource(
        self,
        backend: Backend,
        descriptor: ResourceDescriptor,
        public_uri: str | None = None,
    ) -> None:
        uri = public_uri or descriptor.uri
        existing = self._resources.get(uri)
        if existing and existing.backend.id != backend.id:
            raise RuntimeError(
                f"Resource URI collision: {uri} already exposed by backend "
                f"{existing.backend.id}, refused registration from {backend.id}"
            )
        self._resources[uri] = ResourceMapping(
            public_uri=uri,
            backend=backend,
            original_uri=descriptor.uri,
            descriptor=descriptor,
        )

    def resolve_resource(self, public_uri: str) -> ResourceMapping:
        """Exact match only — for template-aware resolution use
        `resolve_resource_or_template` which falls back to template
        regex matching."""
        mapping = self._resources.get(public_uri)
        if not mapping:
            raise KeyError(f"Unknown resource: {public_uri}")
        return mapping

    def resolve_resource_or_template(
        self, public_uri: str,
    ) -> tuple[Backend, str]:
        """Returns `(backend, original_uri)` for a URI: exact resource match
        first, then first-matching resource template. The original URI
        substituted into the template is preserved verbatim — backends
        match the placeholders themselves."""
        exact = self._resources.get(public_uri)
        if exact is not None:
            return exact.backend, exact.original_uri
        for pattern, mapping in self._resource_templates:
            if pattern.match(public_uri):
                # public_template == original_template in our setup
                # (no public-name remapping for templates yet); pass the
                # client's URI straight through.
                return mapping.backend, public_uri
        raise KeyError(f"Unknown resource: {public_uri}")

    def list_resources(self) -> list[ResourceMapping]:
        return list(self._resources.values())

    def register_resource_template(
        self,
        backend: Backend,
        descriptor: ResourceTemplateDescriptor,
    ) -> None:
        # Collision = same template string registered by a different backend.
        # We keep first-come-first-served and refuse the second one — same as
        # for static resources.
        for _, existing in self._resource_templates:
            if existing.public_template == descriptor.uri_template:
                if existing.backend.id != backend.id:
                    raise RuntimeError(
                        f"Resource template collision: {descriptor.uri_template} "
                        f"already exposed by backend {existing.backend.id}, "
                        f"refused registration from {backend.id}"
                    )
                # Same backend re-registering — replace (covers refresh after
                # reconnect).
                self._resource_templates = [
                    e for e in self._resource_templates
                    if e[1].public_template != descriptor.uri_template
                ]
                break
        mapping = ResourceTemplateMapping(
            public_template=descriptor.uri_template,
            backend=backend,
            original_template=descriptor.uri_template,
            descriptor=descriptor,
        )
        self._resource_templates.append(
            (_template_to_regex(descriptor.uri_template), mapping),
        )

    def list_resource_templates(self) -> list[ResourceTemplateMapping]:
        return [m for _, m in self._resource_templates]
