"""Resolve ``$ref`` pointers in an OpenAPI document."""

from __future__ import annotations

import copy
import re
from typing import Any

from lapis_spec.models.openapi import OpenAPIDict

_SCHEMA_REF_RE = re.compile(r"^#/components/schemas/(.+)$")


class RefResolutionError(Exception):
    """Raised when a ``$ref`` cannot be resolved."""


def resolve_refs(doc: OpenAPIDict) -> OpenAPIDict:
    """Return a deep copy of *doc* with all internal ``$ref`` pointers resolved.

    Only JSON-pointer style internal refs (``#/...``) are supported.
    External file refs are not resolved.  Circular references are
    tolerated: when a cycle is detected the node keeps just its
    ``x-schema-name`` tag so that downstream converters can still
    recognise the named type.
    """
    resolved = copy.deepcopy(doc)
    _resolve_node(resolved, resolved, set())
    return resolved


def count_refs(doc: OpenAPIDict) -> dict[str, int]:
    """Count how many times each ``$ref`` target is referenced.

    Returns a mapping of ref strings (e.g.
    ``#/components/schemas/Invoice``) to their occurrence count.
    """
    counts: dict[str, int] = {}
    _count_refs_recursive(doc, counts)
    return counts


def ref_to_schema_name(ref: str) -> str | None:
    """Extract the schema name from a ``#/components/schemas/X`` ref.

    Returns ``None`` if the ref does not point into ``components/schemas``.
    """
    prefix = "#/components/schemas/"
    if ref.startswith(prefix):
        return ref[len(prefix):]
    return None


def _count_refs_recursive(node: Any, counts: dict[str, int]) -> None:
    if isinstance(node, dict):
        if "$ref" in node:
            ref = node["$ref"]
            if isinstance(ref, str):
                counts[ref] = counts.get(ref, 0) + 1
        for value in node.values():
            _count_refs_recursive(value, counts)
    elif isinstance(node, list):
        for item in node:
            _count_refs_recursive(item, counts)


def _resolve_node(node: Any, root: dict[str, Any], seen: set[str]) -> Any:
    """Recursively walk *node* and resolve any ``$ref`` dicts in-place."""
    if isinstance(node, dict):
        if "$ref" in node:
            ref = node["$ref"]
            if not isinstance(ref, str) or not ref.startswith("#/"):
                return node
            # Preserve annotations (e.g. x-schema-name) placed before resolution
            preserved = {
                k: v for k, v in node.items()
                if k not in ("$ref",) and k.startswith("x-")
            }
            if ref in seen:
                # Circular ref: stop resolving but keep the type name
                # so downstream converters can still match it.
                schema_name = _SCHEMA_REF_RE.match(ref)
                node.clear()
                if schema_name:
                    node["x-schema-name"] = schema_name.group(1)
                node.update(preserved)
                return node
            target = _lookup(root, ref)
            target = copy.deepcopy(target)
            seen_copy = seen | {ref}
            _resolve_node(target, root, seen_copy)
            node.clear()
            node.update(target)
            node.update(preserved)
            return node
        for key, value in node.items():
            node[key] = _resolve_node(value, root, seen)
    elif isinstance(node, list):
        for i, item in enumerate(node):
            node[i] = _resolve_node(item, root, seen)
    return node


def _lookup(root: dict[str, Any], ref: str) -> Any:
    """Follow a JSON pointer (``#/a/b/c``) into *root*."""
    parts = ref.lstrip("#/").split("/")
    current: Any = root
    for part in parts:
        part = part.replace("~1", "/").replace("~0", "~")
        if isinstance(current, dict):
            if part not in current:
                raise RefResolutionError(f"Cannot resolve $ref {ref!r}: key {part!r} not found")
            current = current[part]
        elif isinstance(current, list):
            try:
                current = current[int(part)]
            except (ValueError, IndexError) as exc:
                raise RefResolutionError(
                    f"Cannot resolve $ref {ref!r}: invalid index {part!r}"
                ) from exc
        else:
            raise RefResolutionError(f"Cannot resolve $ref {ref!r}: unexpected type at {part!r}")
    return current
