"""The public website — pages anyone can open without signing in.

    /site/<site>/                 the workspace's landing page
    /site/<site>/<page>           a public page
    /site/<site>/sitemap.xml      every public page, for search engines
    /site/_root                   "/" — the landing page of the site this host
                                  belongs to (SiteSettings.domains, else the
                                  platform's default site), else the web app
    /site/_root/robots.txt        "/robots.txt" and "/sitemap.xml" likewise
    /site/_files/<tenant>/<id>/…  an image or other asset a public page uses
    /public/sites/<site>/page     a blocks page as JSON, for the web app to draw

The web server (frontend/nginx.conf, and Vite's dev proxy) sends /site/, "/",
/robots.txt and /sitemap.xml here. What is rendered how: app/crud/public_site.py.
Nothing here needs a session; each request sets the site's tenant for
row-level security itself.
"""

from __future__ import annotations

import uuid
from pathlib import Path

from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse, RedirectResponse, Response
from sqlalchemy import select

from app.core.config import settings
from app.core.database import AsyncSessionLocal
from app.crud import public_site as site_crud
from app.models.upload import UploadedFile
from app.schemas.public_site import PublicPageRead, PublicSiteInfo, SiteRootRead

router = APIRouter(tags=["public site"])

_HTML_PAGE_CSP = "script-src 'none'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'"
_FILE_CSP = "default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; font-src 'self'; media-src 'self'; sandbox"
# Served inline; anything else is a download.
_INLINE_TYPES = ("image/", "font/", "video/", "audio/", "text/css", "application/pdf", "application/font", "application/x-font", "application/vnd.ms-fontobject")


def _origin(request: Request) -> str:
    if settings.app_base_url.strip():
        return settings.app_base_url.strip().rstrip("/")
    proto = request.headers.get("x-forwarded-proto", request.url.scheme).split(",")[0].strip()
    host = request.headers.get("x-forwarded-host") or request.headers.get("host") or request.url.netloc
    return f"{proto}://{host.split(',')[0].strip()}"


def _html(body: str, status: int = 200, *, csp: str | None = None, cache: bool = True) -> HTMLResponse:
    headers = {
        "X-Content-Type-Options": "nosniff",
        "Referrer-Policy": "strict-origin-when-cross-origin",
        "Cache-Control": "public, max-age=60" if cache and status == 200 else "no-store",
    }
    if csp:
        headers["Content-Security-Policy"] = csp
    return HTMLResponse(body, status_code=status, headers=headers)


def _not_found(message: str = "This page doesn't exist, or isn't public.") -> HTMLResponse:
    body = (
        '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">'
        '<meta name="robots" content="noindex"><title>Page not found</title>'
        "<style>body{font-family:system-ui,sans-serif;display:grid;place-items:center;min-height:90vh;margin:0;color:#334155}"
        "main{text-align:center;padding:24px}h1{font-size:22px;margin:0 0 8px}a{color:#2563eb}</style></head>"
        f'<body><main><h1>Page not found</h1><p>{message}</p><p><a href="/">Go to the home page</a></p></main></body></html>'
    )
    return _html(body, 404, csp=_HTML_PAGE_CSP, cache=False)


async def _render(request: Request, site: site_crud.Site, page_slug: str | None, db) -> Response:
    found = await site_crud.site_page(db, site, page_slug)
    if found is None:
        return _not_found()
    page, version = found.page, found.version
    origin = _origin(request)
    links = await site_crud.link_map(db, site)
    is_home = bool(site.settings.home_page_id) and site.settings.home_page_id == page.id
    url = site_crud.absolute(origin, site_crud.page_path(site, page.id, page.slug)) or origin
    if version.content_mode == "html":
        document = version.html_document or ""
        seo = site_crud.build_seo(
            site, page_name=version.name, seo=version.seo, url=url, origin=origin,
            fallback_text=site_crud.text_content(document), is_home=is_home, updated_at=version.published_at,
        )
        return _html(site_crud.render_html_page(document, seo, links), csp=_HTML_PAGE_CSP)
    content = site_crud.render_blocks_text(version.blocks, links)
    seo = site_crud.build_seo(
        site, page_name=version.name, seo=version.seo, url=url, origin=origin,
        fallback_text=site_crud.text_content(content), is_home=is_home, updated_at=version.published_at,
    )
    index_html = await site_crud.app_index_html()
    shell = site_crud.render_app_shell(index_html, seo, site_slug=site.slug, page_slug=page.slug or "", content=content)
    return _html(shell)


@router.get("/site/_root", response_class=HTMLResponse, include_in_schema=False)
async def site_root(request: Request) -> Response:
    """"/" — this host's site's landing page, or the web app when there's none."""
    async with AsyncSessionLocal() as db, db.begin():
        site = await site_crud.site_for_host(db, request.headers.get("x-forwarded-host") or request.headers.get("host"))
        if site is not None and site.settings.home_page_id:
            response = await _render(request, site, None, db)
            if response.status_code == 200:
                return response
    index_html = await site_crud.app_index_html()
    if index_html:
        return _html(index_html, cache=False)
    return RedirectResponse("/home", status_code=302)


@router.get("/site/_root/robots.txt", response_class=PlainTextResponse, include_in_schema=False)
async def root_robots(request: Request) -> PlainTextResponse:
    async with AsyncSessionLocal() as db, db.begin():
        site = await site_crud.site_for_host(db, request.headers.get("x-forwarded-host") or request.headers.get("host"))
    return PlainTextResponse(site_crud.robots_txt([site] if site else [], _origin(request)))


@router.get("/site/_root/sitemap.xml", include_in_schema=False)
async def root_sitemap(request: Request) -> Response:
    async with AsyncSessionLocal() as db, db.begin():
        site = await site_crud.site_for_host(db, request.headers.get("x-forwarded-host") or request.headers.get("host"))
        if site is None:
            raise HTTPException(status_code=404, detail="No public website here.")
        pages = await site_crud.site_pages(db, site)
    return Response(site_crud.sitemap_xml(site, pages, _origin(request)), media_type="application/xml")


@router.get("/site/_files/{tenant_id}/{file_id}/{filename}", include_in_schema=False)
async def public_file(tenant_id: uuid.UUID, file_id: uuid.UUID, filename: str) -> Response:
    """An asset uploaded for the public site (UploadedFile.is_public) — no
    other upload is ever served here."""
    async with AsyncSessionLocal() as db, db.begin():
        await site_crud.set_tenant(db, tenant_id)
        record = (
            await db.execute(select(UploadedFile).where(UploadedFile.tenant_id == tenant_id, UploadedFile.id == file_id, UploadedFile.is_public.is_(True)))
        ).scalar_one_or_none()
    if record is None:
        raise HTTPException(status_code=404, detail="Not found")
    path = Path(record.storage_path)
    if not path.is_file():
        raise HTTPException(status_code=404, detail="Not found")
    content_type = (record.content_type or "application/octet-stream").lower()
    inline = content_type.startswith(_INLINE_TYPES)
    headers = {
        "Cache-Control": "public, max-age=86400",
        "X-Content-Type-Options": "nosniff",
        "Content-Security-Policy": _FILE_CSP,
    }
    return FileResponse(
        path,
        media_type=content_type if inline else "application/octet-stream",
        headers=headers,
        filename=record.filename,
        content_disposition_type="inline" if inline else "attachment",
    )


@router.get("/site/{slug}/sitemap.xml", include_in_schema=False)
async def site_sitemap(slug: str, request: Request) -> Response:
    async with AsyncSessionLocal() as db, db.begin():
        site = await site_crud.site_by_slug(db, slug)
        if site is None:
            raise HTTPException(status_code=404, detail="No public website here.")
        pages = await site_crud.site_pages(db, site)
    return Response(site_crud.sitemap_xml(site, pages, _origin(request)), media_type="application/xml")


@router.get("/site/{slug}/robots.txt", response_class=PlainTextResponse, include_in_schema=False)
async def site_robots(slug: str, request: Request) -> PlainTextResponse:
    async with AsyncSessionLocal() as db, db.begin():
        site = await site_crud.site_by_slug(db, slug)
    return PlainTextResponse(site_crud.robots_txt([site] if site else [], _origin(request)))


@router.get("/site/{slug}", response_class=HTMLResponse, include_in_schema=False)
@router.get("/site/{slug}/", response_class=HTMLResponse, include_in_schema=False)
async def site_home(slug: str, request: Request) -> Response:
    async with AsyncSessionLocal() as db, db.begin():
        site = await site_crud.site_by_slug(db, slug)
        if site is None:
            return _not_found("There's no public website at this address.")
        return await _render(request, site, None, db)


@router.get("/site/{slug}/{page_slug}", response_class=HTMLResponse, include_in_schema=False)
async def site_page(slug: str, page_slug: str, request: Request) -> Response:
    async with AsyncSessionLocal() as db, db.begin():
        site = await site_crud.site_by_slug(db, slug)
        if site is None:
            return _not_found("There's no public website at this address.")
        return await _render(request, site, page_slug, db)


# ---- JSON for the web app -----------------------------------------------------------------------------------


@router.get("/public/site-root", response_model=SiteRootRead)
async def public_site_root(host: str | None = None) -> SiteRootRead:
    """Which site's landing page "/" shows on this host — for the web app,
    which asks with its own host (it may call the API on another one)."""
    async with AsyncSessionLocal() as db, db.begin():
        site = await site_crud.site_for_host(db, host)
        if site is None or not site.settings.home_page_id or await site_crud.site_page(db, site, None) is None:
            return SiteRootRead()
        return SiteRootRead(slug=site.slug)


@router.get("/public/sites/{slug}/page", response_model=PublicPageRead)
async def public_site_page(slug: str, path: str = "") -> PublicPageRead:
    """A published public page (path = its slug; blank = the landing page)."""
    async with AsyncSessionLocal() as db, db.begin():
        site = await site_crud.site_by_slug(db, slug)
        found = await site_crud.site_page(db, site, path or None) if site else None
        if site is None or found is None:
            raise HTTPException(status_code=404, detail="This page doesn't exist, or isn't public.")
        links = await site_crud.link_map(db, site)
        home = await site_crud.site_page(db, site, None)
    page, version = found.page, found.version
    meta = version.seo or {}
    title = (meta.get("title") or version.name or "").strip()
    is_home = bool(site.settings.home_page_id) and site.settings.home_page_id == page.id
    full_title = site.name if is_home and not meta.get("title") else site.settings.title_template.replace("{page}", title).replace("{site}", site.name).strip(" |-–—")
    return PublicPageRead(
        id=page.id,
        name=version.name,
        slug=page.slug,
        content_mode=version.content_mode,  # type: ignore[arg-type]
        blocks=version.blocks if version.content_mode == "blocks" else [],
        canvas_width=version.canvas_width,
        fit_mode=version.fit_mode,
        title=full_title,
        seo=meta,
        site=PublicSiteInfo(slug=site.slug, name=site.name, home_slug=home.page.slug if home else None, language=site.settings.language),
        links=links,
        theme=site.tenant.theme or {},
        updated_at=version.published_at,
    )
