from __future__ import annotations

import re
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Pattern
from urllib.parse import urlencode


_DYNAMIC_SEGMENT_RE = re.compile(r"<(?:(?P<converter>[a-zA-Z_][a-zA-Z0-9_]*):)?(?P<name>[a-zA-Z_][a-zA-Z0-9_]*)>")


@dataclass(slots=True)
class CompiledRule:
    rule: str
    pattern: Pattern[str]
    converters: dict[str, Callable[[str], Any]]


def compile_rule(rule: str) -> CompiledRule:
    if not rule.startswith("/"):
        raise ValueError(f"route must start with '/': {rule!r}")

    converters: dict[str, Callable[[str], Any]] = {}
    regex_parts: list[str] = []
    last_index = 0

    for match in _DYNAMIC_SEGMENT_RE.finditer(rule):
        start, end = match.span()
        regex_parts.append(re.escape(rule[last_index:start]))

        converter_name = match.group("converter") or "str"
        param_name = match.group("name")
        param_pattern, converter = _resolve_converter(converter_name)
        regex_parts.append(f"(?P<{param_name}>{param_pattern})")
        converters[param_name] = converter
        last_index = end

    regex_parts.append(re.escape(rule[last_index:]))
    pattern = re.compile(f"^{''.join(regex_parts)}$")
    return CompiledRule(rule=rule, pattern=pattern, converters=converters)


def convert_matched_params(compiled_rule: CompiledRule, values: dict[str, str]) -> dict[str, Any]:
    parsed: dict[str, Any] = {}
    for key, value in values.items():
        converter = compiled_rule.converters.get(key, str)
        parsed[key] = converter(value)
    return parsed


def build_rule_path(rule: str, values: dict[str, Any]) -> str:
    remaining = dict(values)
    chunks: list[str] = []
    last_index = 0

    for match in _DYNAMIC_SEGMENT_RE.finditer(rule):
        start, end = match.span()
        chunks.append(rule[last_index:start])
        param_name = match.group("name")
        if param_name not in remaining:
            raise KeyError(param_name)
        chunks.append(str(remaining.pop(param_name)))
        last_index = end

    chunks.append(rule[last_index:])
    path = "".join(chunks)

    if remaining:
        query = urlencode(remaining, doseq=True)
        if query:
            return f"{path}?{query}"
    return path


def _resolve_converter(name: str) -> tuple[str, Callable[[str], Any]]:
    if name == "str":
        return r"[^/]+", str
    if name == "int":
        return r"\d+", int
    if name == "float":
        return r"\d+(?:\.\d+)?", float
    if name == "path":
        return r".+", str
    raise ValueError(f"unsupported converter: {name}")
