# Copyright 2025 Softwell S.r.l.
# Licensed under the Apache License, Version 2.0

"""OpenApiApplication - mounts a RoutingClass with automatic OpenAPI + docs."""

from __future__ import annotations

import importlib
from typing import Any, ClassVar

from genro_routes import Router, RoutingClass, route  # type: ignore[import-untyped]

from .asgi_application import AsgiApplication, RESOURCES_DIR

__all__ = ["OpenApiApplication"]

_SWAGGER_HTML = (RESOURCES_DIR / "swagger.html").read_text()
_OPENAPI_INDEX_HTML = (RESOURCES_DIR / "openapi_index.html").read_text()


class OpenApiApplication(AsgiApplication):
    """Wraps a RoutingClass and adds OpenAPI schema + Swagger UI endpoints.

    Meta endpoints live under _meta/ to avoid conflicts with API routes:
    - /{app}/_meta/schema_json — OpenAPI 3.1 JSON schema
    - /{app}/_meta/docs — Swagger UI page
    - /{app}/_meta/index — splash page

    API endpoints from the RoutingClass keep their original router name:
    - /{app}/{router_name}/endpoint

    Usage::

        api = SourcererAPI(db)
        app = OpenApiApplication(routing_class=api)
        server.mount("sourcerer", app)
    """

    openapi_info: ClassVar[dict[str, Any]] = {}

    def on_init(self, routing_class: RoutingClass | None = None,
                module: str | None = None,
                docs: str = "swagger",
                **kwargs: Any) -> None:
        """Initialize with a routing class instance or import path.

        Args:
            routing_class: RoutingClass instance to mount.
            module: Import path "package.module:ClassName" (alternative to routing_class).
            docs: Documentation style — "swagger", "redoc", or "off".
            **kwargs: Passed to the routing class constructor if module is used.
        """
        self._docs_style = docs
        self._openapi_info: dict[str, Any] = {}
        self._meta = Router(self, name="_meta")

        if routing_class is None and module:
            routing_class = self._import_routing_class(module, **kwargs)

        if routing_class is not None:
            self._mount_routing_class(routing_class)

    def _import_routing_class(self, module_path: str, **kwargs: Any) -> Any:
        """Import and instantiate a RoutingClass from an import path.

        Args:
            module_path: "package.module:ClassName" format.
            **kwargs: Constructor arguments.

        Returns:
            Instantiated RoutingClass.
        """
        module_name, class_name = module_path.split(":")
        mod = importlib.import_module(module_name)
        cls = getattr(mod, class_name)
        return cls(**kwargs)

    def _mount_routing_class(self, routing_class: RoutingClass) -> None:
        """Mount the routing class as a child of this app's main router.

        Uses attach_instance without explicit name, so genro-routes
        uses the child router's original name.

        Args:
            routing_class: The RoutingClass instance to mount.
        """
        self.main.attach_instance(routing_class)

        info = getattr(routing_class, "openapi_info", None)
        if info:
            self._openapi_info = info

    @route("_meta")
    def schema_json(self) -> dict[str, Any]:
        """Return OpenAPI 3.1 JSON schema for mounted routes.

        Delegates to ServerApplication.openapi() when mounted on a server,
        falls back to local generation for standalone usage.
        """
        mount_name = self.mount_name
        if self.server and mount_name:
            result: dict[str, Any] = self.server.server_application.openapi(mount_name)
            return result

        paths_data = self.main.nodes(mode="openapi")
        info = self._openapi_info or self.openapi_info or {}
        return {
            "openapi": "3.1.0",
            "info": {
                "title": info.get("title", self.__class__.__name__),
                "version": info.get("version", "1.0.0"),
                "description": info.get("description", ""),
            },
            **paths_data,
        }

    @route("_meta", meta_mime_type="text/html")
    def docs(self) -> str:
        """Serve Swagger UI page pointing to the server's openapi endpoint."""
        if self._docs_style == "off":
            return "Documentation disabled."

        mount_name = self.mount_name
        schema_url = f"/_server/openapi/{mount_name}" if mount_name else "/_server/openapi"
        info = self._openapi_info or self.openapi_info
        title = info.get("title", "API") if info else "API"

        return _SWAGGER_HTML.format(title=title, schema_url=schema_url)

    @route("_meta", meta_mime_type="text/html")
    def index(self) -> str:
        """Splash page with link to docs."""
        info = self._openapi_info or self.openapi_info or {}
        title = info.get("title", self.__class__.__name__)
        version = info.get("version", "")
        description = info.get("description", "")

        mount_name = self.mount_name
        docs_url = f"/{mount_name}/_meta/docs" if mount_name else "/_meta/docs"

        version_html = f"<p>Version: {version}</p>" if version else ""
        desc_html = f"<p>{description}</p>" if description else ""

        return _OPENAPI_INDEX_HTML.format(
            title=title, version_html=version_html,
            desc_html=desc_html, docs_url=docs_url,
        )


if __name__ == "__main__":
    pass
