import logging
from pathlib import Path

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles

logger = logging.getLogger(__name__)

INDEX_CACHE_CONTROL = "no-store"
ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable"
STATIC_FILE_CACHE_CONTROL = "public, max-age=3600"
FRONTEND_BUILD_INSTRUCTIONS = (
    "Run 'cd frontend && npm install && npm run build', "
    "or use a release zip that includes frontend/prebuilt."
)


class CacheControlStaticFiles(StaticFiles):
    """StaticFiles variant that adds a fixed Cache-Control header."""

    def __init__(self, *args, cache_control: str, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.cache_control = cache_control

    def file_response(self, *args, **kwargs):
        response = super().file_response(*args, **kwargs)
        response.headers["Cache-Control"] = self.cache_control
        return response


def _file_response(path: Path, *, cache_control: str) -> FileResponse:
    return FileResponse(path, headers={"Cache-Control": cache_control})


def _is_index_file(path: Path, index_file: Path) -> bool:
    """Return True when the requested file is the SPA shell index.html."""
    return path == index_file


def _resolve_request_origin(request: Request) -> str:
    """Resolve the external origin, honoring common reverse-proxy headers."""
    forwarded_proto = request.headers.get("x-forwarded-proto")
    forwarded_host = request.headers.get("x-forwarded-host")

    if forwarded_proto and forwarded_host:
        proto = forwarded_proto.split(",")[0].strip()
        host = forwarded_host.split(",")[0].strip()
        if proto and host:
            return f"{proto}://{host}"

    return str(request.base_url).rstrip("/")


def _validate_frontend_dir(frontend_dir: Path, *, log_failures: bool = True) -> tuple[bool, Path]:
    """Resolve and validate a built frontend directory."""
    frontend_dir = frontend_dir.resolve()
    index_file = frontend_dir / "index.html"

    if not frontend_dir.exists():
        if log_failures:
            logger.error("Frontend build directory not found at %s.", frontend_dir)
        return False, frontend_dir

    if not frontend_dir.is_dir():
        if log_failures:
            logger.error("Frontend build path is not a directory: %s.", frontend_dir)
        return False, frontend_dir

    if not index_file.exists():
        if log_failures:
            logger.error("Frontend index file not found at %s.", index_file)
        return False, frontend_dir

    return True, frontend_dir


def register_frontend_static_routes(app: FastAPI, frontend_dir: Path) -> bool:
    """Register frontend static file routes if a built frontend is available."""
    valid, frontend_dir = _validate_frontend_dir(frontend_dir)
    if not valid:
        return False

    index_file = frontend_dir / "index.html"
    assets_dir = frontend_dir / "assets"

    if assets_dir.exists() and assets_dir.is_dir():
        app.mount(
            "/assets",
            CacheControlStaticFiles(directory=assets_dir, cache_control=ASSET_CACHE_CONTROL),
            name="assets",
        )
    else:
        logger.warning(
            "Frontend assets directory missing at %s; /assets files will not be served",
            assets_dir,
        )

    @app.get("/")
    async def serve_index():
        """Serve the frontend index.html."""
        return _file_response(index_file, cache_control=INDEX_CACHE_CONTROL)

    @app.get("/site.webmanifest")
    async def serve_webmanifest(request: Request):
        """Serve a dynamic web manifest using the active request origin."""
        origin = _resolve_request_origin(request)
        manifest = {
            "name": "RemoteTerm for MeshCore",
            "short_name": "RemoteTerm",
            "id": f"{origin}/",
            "start_url": f"{origin}/",
            "scope": f"{origin}/",
            "display": "standalone",
            "display_override": ["window-controls-overlay", "standalone", "fullscreen"],
            "theme_color": "#111419",
            "background_color": "#111419",
            "icons": [
                {
                    "src": f"{origin}/web-app-manifest-192x192.png",
                    "sizes": "192x192",
                    "type": "image/png",
                    "purpose": "maskable",
                },
                {
                    "src": f"{origin}/web-app-manifest-512x512.png",
                    "sizes": "512x512",
                    "type": "image/png",
                    "purpose": "maskable",
                },
            ],
        }
        return JSONResponse(
            manifest,
            media_type="application/manifest+json",
            headers={"Cache-Control": "no-store"},
        )

    @app.get("/{path:path}")
    async def serve_frontend(path: str):
        """Serve frontend files, falling back to index.html for SPA routing."""
        if path == "api" or path.startswith("api/"):
            return JSONResponse(
                status_code=404,
                content={
                    "detail": (
                        "API endpoint not found. If you are seeing this in response to a "
                        "frontend request, you may be running a newer frontend with an older "
                        "backend or vice versa. A full update is suggested."
                    )
                },
            )

        file_path = (frontend_dir / path).resolve()
        try:
            file_path.relative_to(frontend_dir)
        except ValueError:
            raise HTTPException(status_code=404, detail="Not found") from None

        if file_path.exists() and file_path.is_file():
            cache_control = (
                INDEX_CACHE_CONTROL
                if _is_index_file(file_path, index_file)
                else STATIC_FILE_CACHE_CONTROL
            )
            return _file_response(file_path, cache_control=cache_control)

        return _file_response(index_file, cache_control=INDEX_CACHE_CONTROL)

    logger.info("Serving frontend from %s", frontend_dir)
    return True


def register_first_available_frontend_static_routes(
    app: FastAPI, frontend_dirs: list[Path]
) -> Path | None:
    """Register frontend routes from the first valid build directory."""
    for i, candidate in enumerate(frontend_dirs):
        valid, resolved_candidate = _validate_frontend_dir(candidate, log_failures=False)
        if not valid:
            continue

        if register_frontend_static_routes(app, resolved_candidate):
            logger.info("Selected frontend build directory %s", resolved_candidate)
            return resolved_candidate

        if i < len(frontend_dirs) - 1:
            logger.warning("Frontend build at %s was unusable; trying fallback", resolved_candidate)

    logger.error(
        "No usable frontend build found. Searched: %s. %s API will continue without frontend routes.",
        ", ".join(str(path.resolve()) for path in frontend_dirs),
        FRONTEND_BUILD_INSTRUCTIONS,
    )
    return None


def register_frontend_missing_fallback(app: FastAPI) -> None:
    """Register a fallback route that tells the user to build the frontend."""

    @app.get("/", include_in_schema=False)
    async def frontend_not_built():
        return JSONResponse(
            status_code=404,
            content={"detail": f"Frontend not built. {FRONTEND_BUILD_INSTRUCTIONS}"},
        )
