"""
Caprail Application — the heart of the framework.

Bootstraps the IoC container, registers service providers,
and serves as the ASGI entry point.
"""
from __future__ import annotations

import inspect
import os
from collections.abc import Callable
from pathlib import Path
from typing import Any

from caprail.foundation.container import Container
from caprail.foundation.service_provider import ServiceProvider
from caprail.http.request import Request
from caprail.http.response import Response
from caprail.routing.router import Router


class Application(Container):
    """
    The Caprail application.

    Extends the service container and manages the full request lifecycle.

    Usage (in ``bootstrap/app.py``)::

        from caprail import Application

        app = Application(base_path=Path(__file__).parent.parent)
        app.register_providers([AppServiceProvider, RouteServiceProvider])
        application = app.make_asgi()
    """

    VERSION = "0.1.0"

    def __init__(self, base_path: Path | str | None = None) -> None:
        super().__init__()
        self.base_path = Path(base_path) if base_path else Path.cwd()
        self._providers: list[ServiceProvider] = []
        self._booted = False
        self._router = Router()
        self._global_middleware: list[type] = []
        self._env: dict[str, str] = {}

        self.instance("app", self)
        self.instance(Application, self)
        self.instance("router", self._router)
        self.instance(Router, self._router)

        self._load_env()

    # ------------------------------------------------------------------
    # Paths
    # ------------------------------------------------------------------

    def path(self, *parts: str) -> Path:
        return self.base_path.joinpath(*parts)

    def app_path(self, *parts: str) -> Path:
        return self.base_path / "app" / Path(*parts) if parts else self.base_path / "app"

    def config_path(self, *parts: str) -> Path:
        return self.base_path / "config" / Path(*parts) if parts else self.base_path / "config"

    def database_path(self, *parts: str) -> Path:
        return self.base_path / "database" / Path(*parts) if parts else self.base_path / "database"

    def resources_path(self, *parts: str) -> Path:
        return self.base_path / "resources" / Path(*parts) if parts else self.base_path / "resources"

    def storage_path(self, *parts: str) -> Path:
        return self.base_path / "storage" / Path(*parts) if parts else self.base_path / "storage"

    def public_path(self, *parts: str) -> Path:
        return self.base_path / "public" / Path(*parts) if parts else self.base_path / "public"

    # ------------------------------------------------------------------
    # Environment
    # ------------------------------------------------------------------

    def _load_env(self) -> None:
        env_file = self.base_path / ".env"
        if env_file.exists():
            from dotenv import dotenv_values
            self._env = dict(dotenv_values(str(env_file)))
            os.environ.update({k: v for k, v in self._env.items() if v is not None})

    def env(self, key: str, default: Any = None) -> Any:
        return os.environ.get(key, self._env.get(key, default))

    def is_production(self) -> bool:
        return self.env("APP_ENV", "production") == "production"

    def is_debug(self) -> bool:
        return str(self.env("APP_DEBUG", "false")).lower() in ("true", "1", "yes")

    # ------------------------------------------------------------------
    # Providers
    # ------------------------------------------------------------------

    def register_providers(self, providers: list[type[ServiceProvider]]) -> None:
        for cls in providers:
            provider = cls(self)
            provider.register()
            self._providers.append(provider)

    def boot(self) -> None:
        if self._booted:
            return
        for provider in self._providers:
            provider.boot()
        self._booted = True

    # ------------------------------------------------------------------
    # Routing helpers
    # ------------------------------------------------------------------

    @property
    def router(self) -> Router:
        return self._router

    def add_middleware(self, *middleware_classes: type) -> None:
        self._global_middleware.extend(middleware_classes)

    # ------------------------------------------------------------------
    # ASGI interface
    # ------------------------------------------------------------------

    def make_asgi(self) -> Callable:
        self.boot()
        # Return a plain coroutine function (not a bound method) so uvicorn's
        # ASGI2 vs ASGI3 detection correctly identifies it as ASGI3.
        async def asgi_app(scope: dict, receive, send) -> None:
            await self._asgi_handler(scope, receive, send)
        return asgi_app

    async def _asgi_handler(self, scope: dict, receive, send) -> None:
        if scope["type"] == "lifespan":
            await self._handle_lifespan(receive, send)
            return
        if scope["type"] != "http":
            return
        request = Request(scope, receive, send)
        response = await self._dispatch(request)
        if response._headers.pop("_back", None):
            url = request.header("referer", "/")
            response._headers["location"] = url
        await response.send(send)

    async def _dispatch(self, request: Request) -> Response:
        from caprail.http.middleware import Pipeline

        result = self._router.resolve(request.method, request.path)
        if result is None:
            return self._not_found(request)

        route, params = result
        request.route_params = params
        request.route_name = route.name

        route_mw_classes = [
            self._router.middleware_aliases.get(name)
            for name in route.middleware
            if name in self._router.middleware_aliases
        ]
        all_classes = self._global_middleware + [c for c in route_mw_classes if c]
        middleware_instances = self._resolve_middleware(all_classes)

        async def call_handler(req: Request) -> Response:
            return await self._call_handler(route.handler, req)

        return await Pipeline(middleware_instances).send(request).then(call_handler)

    def _resolve_middleware(self, classes: list[type]) -> list:
        instances = []
        for cls in classes:
            try:
                instances.append(self.make(cls))
            except Exception:
                instances.append(cls())
        return instances

    @staticmethod
    async def _call_handler(handler: Callable, request: Request) -> Response:
        result = await handler(request) if inspect.iscoroutinefunction(handler) else handler(request)
        if isinstance(result, Response):
            return result
        if isinstance(result, str):
            return Response(result)
        if isinstance(result, dict):
            return Response.json(result)
        return Response(str(result) if result is not None else "")

    @staticmethod
    def _not_found(request: Request) -> Response:
        if request.wants_json():
            return Response.json({"message": "Not found."}, status=404)
        return Response("<h1>404 — Not Found</h1>", status=404)

    async def _handle_lifespan(self, receive, send) -> None:
        while True:
            message = await receive()
            if message["type"] == "lifespan.startup":
                await send({"type": "lifespan.startup.complete"})
            elif message["type"] == "lifespan.shutdown":
                await send({"type": "lifespan.shutdown.complete"})
                return
