"""
Caprail Router.

Provides a Laravel-inspired fluent routing API::

    Route.get("/", HomeController.index)
    Route.post("/users", UserController.store)

    Route.group({"prefix": "/api", "middleware": ["auth"]}, lambda: [
        Route.get("/users", UserController.index),
    ])
"""
from __future__ import annotations

import re
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any


@dataclass
class Route:
    """Represents a single registered route."""

    methods: list[str]
    path: str
    handler: Callable
    name: str | None = None
    middleware: list[str] = field(default_factory=list)
    # Compiled regex and param names (populated by Router.compile)
    _regex: re.Pattern | None = field(default=None, repr=False)
    _param_names: list[str] = field(default_factory=list, repr=False)

    def name_as(self, name: str) -> "Route":
        self.name = name
        return self

    def middleware_names(self) -> list[str]:
        return self.middleware


class RouteGroup:
    """Temporarily stores prefix/middleware/namespace for grouped routes."""

    def __init__(
        self,
        prefix: str = "",
        middleware: list[str] | None = None,
        namespace: str = "",
    ) -> None:
        self.prefix = prefix.rstrip("/")
        self.middleware: list[str] = middleware or []
        self.namespace = namespace


class Router:
    """
    The Caprail HTTP Router.

    Stores all registered routes and resolves incoming requests to
    their handlers.
    """

    def __init__(self) -> None:
        self._routes: list[Route] = []
        self._named: dict[str, Route] = {}
        self._group_stack: list[RouteGroup] = []
        # Middleware aliases registered by the application
        self.middleware_aliases: dict[str, type] = {}
        self.middleware_groups: dict[str, list[str]] = {}

    # ------------------------------------------------------------------
    # Registration
    # ------------------------------------------------------------------

    def get(self, path: str, handler: Callable, **kwargs) -> Route:
        return self._add(["GET", "HEAD"], path, handler, **kwargs)

    def post(self, path: str, handler: Callable, **kwargs) -> Route:
        return self._add(["POST"], path, handler, **kwargs)

    def put(self, path: str, handler: Callable, **kwargs) -> Route:
        return self._add(["PUT"], path, handler, **kwargs)

    def patch(self, path: str, handler: Callable, **kwargs) -> Route:
        return self._add(["PATCH"], path, handler, **kwargs)

    def delete(self, path: str, handler: Callable, **kwargs) -> Route:
        return self._add(["DELETE"], path, handler, **kwargs)

    def options(self, path: str, handler: Callable, **kwargs) -> Route:
        return self._add(["OPTIONS"], path, handler, **kwargs)

    def any(self, path: str, handler: Callable, **kwargs) -> Route:
        return self._add(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"], path, handler, **kwargs)

    def match(self, methods: list[str], path: str, handler: Callable, **kwargs) -> Route:
        return self._add([m.upper() for m in methods], path, handler, **kwargs)

    def redirect(self, from_path: str, to: str, status: int = 302) -> Route:
        from caprail.http.response import Response

        async def _redirect(request):
            return Response.redirect(to, status)

        return self._add(["GET", "HEAD"], from_path, _redirect)

    def view(self, path: str, template: str, data: dict | None = None) -> Route:
        async def _view(request):
            from caprail.view.engine import ViewEngine
            engine = ViewEngine.instance()
            return await engine.render_response(template, data or {})

        return self._add(["GET", "HEAD"], path, _view)

    def resource(self, path: str, controller: type) -> None:
        """Register CRUD resource routes for a controller."""
        name = path.strip("/").replace("/", ".")
        self.get(path, controller.index).name_as(f"{name}.index")  # type: ignore[attr-defined]
        self.get(f"{path}/create", controller.create).name_as(f"{name}.create")  # type: ignore[attr-defined]
        self.post(path, controller.store).name_as(f"{name}.store")  # type: ignore[attr-defined]
        self.get(f"{path}/{{id}}", controller.show).name_as(f"{name}.show")  # type: ignore[attr-defined]
        self.get(f"{path}/{{id}}/edit", controller.edit).name_as(f"{name}.edit")  # type: ignore[attr-defined]
        self.put(f"{path}/{{id}}", controller.update).name_as(f"{name}.update")  # type: ignore[attr-defined]
        self.patch(f"{path}/{{id}}", controller.update).name_as(f"{name}.update_patch")  # type: ignore[attr-defined]
        self.delete(f"{path}/{{id}}", controller.destroy).name_as(f"{name}.destroy")  # type: ignore[attr-defined]

    def api_resource(self, path: str, controller: type) -> None:
        """Resource routes without create/edit (for API usage)."""
        name = path.strip("/").replace("/", ".")
        self.get(path, controller.index).name_as(f"{name}.index")  # type: ignore[attr-defined]
        self.post(path, controller.store).name_as(f"{name}.store")  # type: ignore[attr-defined]
        self.get(f"{path}/{{id}}", controller.show).name_as(f"{name}.show")  # type: ignore[attr-defined]
        self.put(f"{path}/{{id}}", controller.update).name_as(f"{name}.update")  # type: ignore[attr-defined]
        self.patch(f"{path}/{{id}}", controller.update).name_as(f"{name}.update_patch")  # type: ignore[attr-defined]
        self.delete(f"{path}/{{id}}", controller.destroy).name_as(f"{name}.destroy")  # type: ignore[attr-defined]

    def group(self, attributes: dict, callback: Callable) -> None:
        """Define a route group with shared attributes."""
        group = RouteGroup(
            prefix=attributes.get("prefix", ""),
            middleware=attributes.get("middleware", []),
            namespace=attributes.get("namespace", ""),
        )
        self._group_stack.append(group)
        callback()
        self._group_stack.pop()

    # ------------------------------------------------------------------
    # Resolution
    # ------------------------------------------------------------------

    def resolve(self, method: str, path: str) -> tuple[Route, dict[str, str]] | None:
        """
        Find the route matching the given method and path.

        Returns ``(route, params)`` or ``None``.
        """
        for route in self._routes:
            if method.upper() not in route.methods:
                continue
            if route._regex is None:
                self._compile(route)
            m = route._regex.match(path)  # type: ignore[union-attr]
            if m:
                return route, m.groupdict()
        return None

    # ------------------------------------------------------------------
    # URL generation
    # ------------------------------------------------------------------

    def url_for(self, name: str, **params) -> str:
        """Generate a URL for a named route."""
        route = self._named.get(name)
        if route is None:
            # Scan routes for names set via name_as() after registration
            for r in self._routes:
                if r.name == name:
                    route = r
                    self._named[name] = r
                    break
        if route is None:
            raise ValueError(f"No route named '{name}'")
        url = route.path
        for key, value in params.items():
            url = url.replace(f"{{{key}}}", str(value))
            url = url.replace(f"{{{key}?}}", str(value))
        # Remove any leftover optional segments
        url = re.sub(r"\{[^}]+\?\}", "", url)
        return url

    @property
    def routes(self) -> list[Route]:
        return list(self._routes)

    # ------------------------------------------------------------------
    # Internal
    # ------------------------------------------------------------------

    def _add(self, methods: list[str], path: str, handler: Callable, **kwargs) -> Route:
        full_path = self._resolve_path(path)
        middleware = self._resolve_middleware(kwargs.get("middleware", []))
        route = Route(
            methods=methods,
            path=full_path,
            handler=handler,
            name=kwargs.get("name"),
            middleware=middleware,
        )
        self._routes.append(route)
        if route.name:
            self._named[route.name] = route
        return route

    def _resolve_path(self, path: str) -> str:
        prefix = "".join(g.prefix for g in self._group_stack)
        return f"{prefix}/{path.lstrip('/')}".rstrip("/") or "/"

    def _resolve_middleware(self, extra: list[str]) -> list[str]:
        result: list[str] = []
        for group in self._group_stack:
            result.extend(group.middleware)
        result.extend(extra)
        return result

    @staticmethod
    def _compile(route: Route) -> None:
        """Convert a path pattern like /users/{id} into a regex."""
        pattern = route.path
        # Escape dots but not braces
        pattern = re.sub(r"\.", r"\\.", pattern)
        # Optional parameters: {name?}
        pattern = re.sub(r"\{(\w+)\?\}", r"(?P<\1>[^/]*)?", pattern)
        # Required parameters: {name}
        pattern = re.sub(r"\{(\w+)\}", r"(?P<\1>[^/]+)", pattern)
        route._regex = re.compile(f"^{pattern}/?$")
        route._param_names = re.findall(r"\{(\w+)\??}", route.path)
