from __future__ import annotations

import os
import random
import secrets
from contextlib import asynccontextmanager
from html import escape
from pathlib import Path

from fastapi import FastAPI, HTTPException, Query, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse

from .db import Database
from .models import (ApiTokenIn, ClientIn, ExecutionIn, LoginIn, ProjectIn, ProjectUpdateIn,
                     ReleaseIn, SignupIn, StatusIn, TelemetryIn)
from .service import Conflict, Invalid, NotFound, Service


def create_app(database_path: str | None = None) -> FastAPI:
    db = Database(database_path or os.getenv("POLYFORM_DB", "polyform.db"))

    @asynccontextmanager
    async def lifespan(app):
        db.initialize()
        yield

    app = FastAPI(
        title="Polyform Control Plane",
        version="0.1.0",
        lifespan=lifespan,
        root_path=os.getenv("POLYFORM_ROOT_PATH", ""),
        docs_url="/api/docs",
        redoc_url=None,
        openapi_url="/api/openapi.json",
    )
    assignment_seed = os.getenv("POLYFORM_RANDOM_SEED")
    app.state.service = Service(db, random.Random(assignment_seed) if assignment_seed else None)
    origins=[x.strip() for x in os.getenv("POLYFORM_CORS_ORIGINS","http://localhost:5173,http://127.0.0.1:5173").split(",") if x.strip()]
    app.add_middleware(CORSMiddleware,allow_origins=origins,allow_methods=["GET","POST","PUT"],allow_headers=["Content-Type","X-Polyform-Admin-Token"])

    @app.exception_handler(NotFound)
    async def not_found(_, exc): return __import__("fastapi").responses.JSONResponse(status_code=404,content={"detail":str(exc)})
    @app.exception_handler(Conflict)
    async def conflict(_, exc): return __import__("fastapi").responses.JSONResponse(status_code=409,content={"detail":str(exc)})
    @app.exception_handler(Invalid)
    async def invalid(_, exc): return __import__("fastapi").responses.JSONResponse(status_code=422,content={"detail":str(exc)})

    def svc(request: Request) -> Service: return request.app.state.service

    def require_admin(request: Request) -> None:
        expected = os.getenv("POLYFORM_ADMIN_TOKEN")
        supplied = request.headers.get("X-Polyform-Admin-Token", "")
        if expected and not secrets.compare_digest(supplied, expected):
            raise HTTPException(status_code=401, detail="valid Polyform administrator token required")

    def request_user(request: Request, required=True):
        authorization = request.headers.get("Authorization", "")
        bearer = authorization[7:] if authorization.lower().startswith("bearer ") else None
        user = svc(request).api_token_user(bearer) if bearer else svc(request).session_user(request.cookies.get("polyform_session"))
        if required and not user: raise HTTPException(status_code=401, detail="sign in required")
        return user

    def set_session_cookie(response: Response, token: str):
        root_path = os.getenv("POLYFORM_ROOT_PATH", "")
        response.set_cookie("polyform_session", token, max_age=30*24*60*60, httponly=True,
                            secure=bool(root_path), samesite="lax", path=(root_path.rstrip("/")+"/") or "/")

    @app.get("/health")
    def health(): return {"status":"ok"}

    @app.post("/api/v1/auth/signup", status_code=201)
    def signup(body: SignupIn, response: Response, request: Request):
        user = svc(request).create_user(body)
        set_session_cookie(response, svc(request).create_session(user["id"]))
        return user

    @app.post("/api/v1/auth/login")
    def login(body: LoginIn, response: Response, request: Request):
        user = svc(request).authenticate(body.username, body.password)
        set_session_cookie(response, svc(request).create_session(user["id"]))
        return user

    @app.post("/api/v1/auth/logout", status_code=204)
    def logout(response: Response, request: Request):
        svc(request).delete_session(request.cookies.get("polyform_session"))
        root_path = os.getenv("POLYFORM_ROOT_PATH", "")
        response.delete_cookie("polyform_session", path=(root_path.rstrip("/")+"/") or "/")

    @app.get("/api/v1/auth/me")
    def me(request: Request): return request_user(request)

    @app.post("/api/v1/auth/tokens", status_code=201)
    def create_api_token(body: ApiTokenIn, request: Request):
        user = request_user(request)
        return {"token":svc(request).create_api_token(user["id"],body.label),"label":body.label}

    @app.get("/api/v1/projects")
    def public_projects(request: Request, limit: int = Query(24,ge=1,le=100)):
        return svc(request).list_public_projects(limit)

    @app.get("/api/v1/users/{username}")
    def public_user(username: str, request: Request):
        return svc(request).public_user_profile(username)

    @app.get("/api/v1/me/projects")
    def my_projects(request: Request):
        user = request_user(request)
        return svc(request).list_user_projects(user["id"])

    @app.post("/api/v1/projects", status_code=201)
    def create_project(body: ProjectIn, request: Request):
        user = request_user(request)
        return svc(request).create_project(user["id"],body)

    @app.get("/api/v1/projects/{owner}/{slug}")
    def project(owner: str, slug: str, request: Request): return svc(request).project(owner,slug)

    @app.put("/api/v1/projects/{owner}/{slug}")
    def update_project(owner: str, slug: str, body: ProjectUpdateIn, request: Request):
        user = request_user(request)
        return svc(request).update_project(user["id"],owner,slug,body)

    @app.post("/api/v1/projects/{owner}/{slug}/releases", status_code=201)
    def publish_project_release(owner: str, slug: str, body: ReleaseIn, request: Request):
        user = request_user(request)
        return svc(request).publish_project_release(user["id"],owner,slug,body)

    @app.get("/api/v1/projects/{owner}/{slug}/catalog")
    def project_catalog(owner: str, slug: str, request: Request, version: str | None = None):
        return svc(request).project_catalog(owner,slug,version)

    @app.post("/api/v1/releases", status_code=201)
    def create_release(body: ReleaseIn, request: Request):
        require_admin(request)
        return svc(request).create_release(body)

    @app.get("/api/v1/releases")
    def releases(request: Request): return svc(request).list_releases()

    @app.get("/api/v1/releases/{release_id}")
    def release(release_id: int, request: Request): return svc(request).release(release_id)

    @app.get("/api/v1/catalog")
    def catalog(request: Request): return svc(request).catalog()

    @app.get("/api/v1/catalog/{application}")
    def catalog_release(application: str, request: Request, version: str | None = None):
        return svc(request).catalog(application, version)

    @app.post("/api/v1/clients", status_code=201)
    def register(body: ClientIn, request: Request): return svc(request).register_client(body)

    @app.get("/api/v1/clients/{client_id}/composition")
    def composition(client_id: str, request: Request): return svc(request).composition(client_id)

    @app.post("/api/v1/executions", status_code=201)
    def execution(body: ExecutionIn, request: Request): return svc(request).execution(body)

    @app.post("/api/v1/telemetry", status_code=201)
    def telemetry(body: TelemetryIn, request: Request): return svc(request).telemetry(body)

    @app.get("/api/v1/telemetry")
    def telemetry_events(request: Request, limit: int = Query(100,ge=1,le=1000), release_id: int | None = None):
        return svc(request).events(limit,release_id)

    @app.put("/api/v1/implementations/{implementation_id}/status")
    def status(implementation_id: str, body: StatusIn, request: Request, release_id: int | None = None):
        user = request_user(request,required=False)
        owner = svc(request).release_project_owner(release_id) if release_id is not None else None
        if not (user and owner and user["id"] == owner["user_id"]): require_admin(request)
        return svc(request).set_status(implementation_id,body,release_id)

    @app.get("/api/v1/stats/implementations")
    def implementation_stats(request: Request, release_id: int | None = None): return svc(request).implementation_stats(release_id)

    @app.get("/api/v1/stats/interactions")
    def interactions(request: Request, release_id: int | None = None, min_executions: int = Query(3,ge=1)):
        return svc(request).pairwise_stats(release_id,min_executions)

    @app.get("/api/v1/dashboard/summary")
    def dashboard(request: Request, release_id: int | None = None): return svc(request).dashboard(release_id)

    dashboard_dir = os.getenv("POLYFORM_DASHBOARD_DIR")
    download_dir = os.getenv("POLYFORM_DOWNLOAD_DIR")
    if download_dir and os.path.isdir(download_dir):
        published_downloads = {
            "install.sh", "polyform-aarch64-apple-darwin",
            "polyform-aarch64-apple-darwin.zip",
            "polyform-x86_64-unknown-linux-gnu", "polyform-source.tar.gz",
        }

        @app.get("/downloads/{filename}", include_in_schema=False)
        def download(filename: str):
            if filename not in published_downloads:
                raise HTTPException(status_code=404, detail="download not found")
            return FileResponse(os.path.join(download_dir, filename), filename=filename)
    if dashboard_dir and os.path.isdir(dashboard_dir):
        assets = os.path.join(dashboard_dir,"assets")
        if os.path.isdir(assets):
            @app.get("/assets/{filename}", include_in_schema=False)
            def dashboard_asset(filename: str):
                if filename != os.path.basename(filename):
                    raise HTTPException(status_code=404, detail="asset not found")
                asset_path = os.path.join(assets, filename)
                if not os.path.isfile(asset_path):
                    raise HTTPException(status_code=404, detail="asset not found")
                return FileResponse(asset_path)

        def public_metadata(filename: str):
            path = os.path.join(dashboard_dir, filename)
            if not os.path.isfile(path):
                raise HTTPException(status_code=404, detail="file not found")
            return FileResponse(path)

        @app.get("/llms.txt", include_in_schema=False)
        def llms(): return public_metadata("llms.txt")

        @app.get("/robots.txt", include_in_schema=False)
        def robots(): return public_metadata("robots.txt")

        @app.get("/sitemap.xml", include_in_schema=False)
        def sitemap(): return public_metadata("sitemap.xml")

        root_path = os.getenv("POLYFORM_ROOT_PATH", "").rstrip("/")

        def href(path: str) -> str:
            return escape(f"{root_path}{path}", quote=True)

        def html_page(markup: str) -> HTMLResponse:
            shell_path = os.path.join(dashboard_dir, "shell.html")
            if not os.path.isfile(shell_path):
                shell_path = os.path.join(dashboard_dir, "index.html")
            template = Path(shell_path).read_text(encoding="utf-8")
            return HTMLResponse(
                template.replace('<div id="root"></div>', f'<div id="root">{markup}</div>', 1),
                headers={"Cache-Control": "no-store, max-age=0"},
            )

        def html_file(path: str) -> FileResponse:
            return FileResponse(path, headers={"Cache-Control": "no-store, max-age=0"})

        def project_markup(project: dict) -> str:
            owner = escape(project["owner"])
            slug = escape(project["slug"])
            name = escape(project["name"])
            description = escape(project.get("description") or "")
            repository = project.get("repository_url")
            spec_path = project.get("spec_path")
            links = []
            if repository:
                repository_attr = escape(repository, quote=True)
                links.append(f'<a href="{repository_attr}">View the open-source repository</a>')
                if spec_path:
                    spec_url = f'{repository.rstrip("/")}/blob/main/{spec_path}'
                    links.append(f'<a href="{escape(spec_url, quote=True)}">Read the specification</a>')
            release_count = len(project.get("releases", []))
            return (
                '<main class="public-page"><nav><a href="' + href("/") + '">Polyform</a></nav>'
                f'<p><a href="{href(f"/{project["owner"]}")}">@{owner}</a> / {slug}</p>'
                f'<h1>{name}</h1><p>{description}</p>'
                f'<p>{" · ".join(links)}</p>'
                '<section><h2>Published software</h2>'
                f'<p>{release_count} releases, {project.get("spec_functions", 0)} specified functions, '
                f'and {project.get("implementations", 0)} implementations.</p></section></main>'
            )

        def profile_markup(profile: dict) -> str:
            username = escape(profile["username"])
            cards = []
            for project in profile.get("projects", []):
                cards.append(
                    f'<article><h2><a href="{href(f"/{project["owner"]}/{project["slug"]}")}">'
                    f'{escape(project["name"])}</a></h2><p>{escape(project.get("description") or "")}</p></article>'
                )
            projects = "".join(cards) or "<p>No public projects yet.</p>"
            return (
                '<main class="public-page"><nav><a href="' + href("/") + '">Polyform</a></nav>'
                f'<h1>@{username}</h1><p>Open-source Polyform projects by {username}.</p>'
                f'<section><h2>Projects</h2>{projects}</section></main>'
            )

        @app.get("/{full_path:path}", include_in_schema=False)
        def spa(full_path: str, request: Request):
            prerendered = os.path.join(dashboard_dir, "prerender", full_path, "index.html")
            if full_path and os.path.isfile(prerendered):
                return html_file(prerendered)
            if not full_path:
                return html_file(os.path.join(dashboard_dir, "index.html"))

            parts = [part for part in full_path.split("/") if part]
            try:
                if len(parts) == 1:
                    return html_page(profile_markup(svc(request).public_user_profile(parts[0])))
                if len(parts) == 2:
                    return html_page(project_markup(svc(request).project(parts[0], parts[1])))
            except NotFound:
                pass
            shell = os.path.join(dashboard_dir, "shell.html")
            return html_file(shell if os.path.isfile(shell) else os.path.join(dashboard_dir,"index.html"))

    return app
