from __future__ import annotations

from collections.abc import Iterable
from dataclasses import dataclass


class RouteCollisionError(ValueError):
    pass


@dataclass(frozen=True, slots=True)
class RouteSignature:
    method: str
    path: str


@dataclass(frozen=True, slots=True)
class RouteRecord:
    signature: RouteSignature
    owner: str


def validate_no_route_collisions(routes_or_app: object, *, owner: str = "router") -> None:
    seen: dict[RouteSignature, RouteRecord] = {}
    for record in iter_route_records(routes_or_app, owner=owner):
        previous = seen.get(record.signature)
        if previous is not None:
            raise RouteCollisionError(_collision_message(record.signature, previous.owner, record.owner))
        seen[record.signature] = record


def validate_router_can_mount(existing_routes_or_app: object, candidate_router: object, *, prefix: str = "", owner: str) -> None:
    validate_no_route_collisions(candidate_router, owner=owner)
    existing = {record.signature: record for record in iter_route_records(existing_routes_or_app, owner="existing application")}
    for record in iter_route_records(candidate_router, prefix=prefix, owner=owner):
        previous = existing.get(record.signature)
        if previous is not None:
            raise RouteCollisionError(_collision_message(record.signature, previous.owner, record.owner))


def iter_route_records(routes_or_app: object, *, prefix: str = "", owner: str = "router") -> Iterable[RouteRecord]:
    routes = getattr(routes_or_app, "routes", routes_or_app)
    for route in routes or ():
        path = _join_route_path(prefix, str(getattr(route, "path", "") or ""))
        include_context = getattr(route, "include_context", None)
        nested_prefix = _join_route_path(prefix, str(getattr(include_context, "prefix", "") or ""))
        nested_router = getattr(route, "original_router", None)
        if nested_router is not None:
            yield from iter_route_records(nested_router, prefix=nested_prefix, owner=owner)

        methods = getattr(route, "methods", None)
        if methods:
            for method in sorted(str(item).upper() for item in methods):
                yield RouteRecord(RouteSignature(method=method, path=path), owner)

        nested_routes = getattr(route, "routes", None)
        if nested_routes is not None and nested_routes is not routes:
            yield from iter_route_records(nested_routes, prefix=path, owner=owner)


def _join_route_path(prefix: str, path: str) -> str:
    if not prefix:
        return path or "/"
    if not path:
        return prefix or "/"
    return f"{prefix.rstrip('/')}/{path.lstrip('/')}" or "/"


def _collision_message(signature: RouteSignature, previous_owner: str, next_owner: str) -> str:
    return (
        f"Route collision: {signature.method} {signature.path} is registered by "
        f"{previous_owner} and {next_owner}"
    )
