from __future__ import annotations

import mimetypes
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Awaitable, Callable

import yaml
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse, Response

from .agent import A2AAgent
from .auth import PlatformUserAuth
from .runtime import LLMProvisioning

DEFAULT_FRONTEND_MOUNT = "/app"
DEFAULT_DOCS_URL = "https://docs.a2acloud.io/"


@dataclass(frozen=True)
class FrontendConfig:
    """Manifest declaration for a static frontend packed with an agent."""

    path: str = "frontend"
    dist: str = "dist"
    mount: str = DEFAULT_FRONTEND_MOUNT
    build: str | None = None
    auth: str = "inherit"
    kind: str = "static"
    docs_url: str = DEFAULT_DOCS_URL

    @property
    def enabled(self) -> bool:
        return self.kind in {"static", "static-spa", "spa"}

    def source_dir(self, project: Path) -> Path:
        return _safe_project_path(project, self.path)

    def dist_dir(self, project: Path) -> Path:
        dist = Path(self.dist)
        if dist.is_absolute():
            return dist.resolve()
        return _safe_child_path(self.source_dir(project), self.dist)


@dataclass(frozen=True)
class PackedFrontend:
    """Resolved frontend assets ready to serve from the agent process."""

    dist_dir: Path
    mount: str = DEFAULT_FRONTEND_MOUNT
    auth: str = "inherit"
    docs_url: str = DEFAULT_DOCS_URL

    @property
    def config_path(self) -> str:
        return _mount_path(self.mount, "config.json")

    @property
    def client_path(self) -> str:
        return _mount_path(self.mount, "a2a-client.js")


def load_frontend_config(
    project: Path,
    config: dict[str, Any] | None = None,
) -> FrontendConfig | None:
    """Read the optional ``frontend`` section from ``a2a.yaml``."""

    data = config if config is not None else _read_project_config(project)
    raw = data.get("frontend")
    if raw in (None, False):
        return None
    if raw is True:
        raw = {}
    if isinstance(raw, str):
        raw = {"path": raw}
    if not isinstance(raw, dict):
        raise ValueError("frontend must be an object, string path, true, or false")

    kind = str(raw.get("type") or raw.get("kind") or "static").strip().lower()
    if kind not in {"static", "static-spa", "spa"}:
        raise ValueError("frontend.type currently supports only static frontends")

    return FrontendConfig(
        path=_clean_relpath(str(raw.get("path") or "frontend")),
        dist=_clean_relpath(str(raw.get("dist") or "dist")),
        mount=_normalize_mount(str(raw.get("mount") or DEFAULT_FRONTEND_MOUNT)),
        build=_clean_optional_string(raw.get("build")),
        auth=_clean_auth(raw.get("auth")),
        kind=kind,
        docs_url=_clean_docs_url(raw.get("docs_url") or raw.get("docsUrl")),
    )


def resolve_packed_frontend(
    project: Path,
    config: dict[str, Any] | None = None,
    *,
    require_dist: bool = True,
) -> PackedFrontend | None:
    cfg = load_frontend_config(project, config)
    if cfg is None or not cfg.enabled:
        return None
    dist_dir = cfg.dist_dir(project)
    if require_dist and not (dist_dir / "index.html").is_file():
        return None
    return PackedFrontend(
        dist_dir=dist_dir,
        mount=cfg.mount,
        auth=cfg.auth,
        docs_url=cfg.docs_url,
    )


def packed_frontend_from_env() -> PackedFrontend | None:
    dist = os.environ.get("A2A_FRONTEND_DIST")
    if not dist:
        return None
    return PackedFrontend(
        dist_dir=Path(dist).resolve(),
        mount=_normalize_mount(os.environ.get("A2A_FRONTEND_MOUNT") or DEFAULT_FRONTEND_MOUNT),
        auth=_clean_auth(os.environ.get("A2A_FRONTEND_AUTH")),
        docs_url=_clean_docs_url(os.environ.get("A2A_FRONTEND_DOCS_URL")),
    )


def export_frontend_env(frontend: PackedFrontend | None) -> None:
    """Expose a resolved frontend to ``build_app`` across uvicorn reloads."""

    if frontend is None:
        for key in (
            "A2A_FRONTEND_DIST",
            "A2A_FRONTEND_MOUNT",
            "A2A_FRONTEND_AUTH",
            "A2A_FRONTEND_DOCS_URL",
        ):
            os.environ.pop(key, None)
        return
    os.environ["A2A_FRONTEND_DIST"] = str(frontend.dist_dir)
    os.environ["A2A_FRONTEND_MOUNT"] = frontend.mount
    os.environ["A2A_FRONTEND_AUTH"] = frontend.auth
    os.environ["A2A_FRONTEND_DOCS_URL"] = frontend.docs_url


def frontend_auth_metadata(frontend: PackedFrontend, request: Request | None = None) -> dict[str, Any]:
    requires_session = frontend_requires_session(frontend)
    return {
        "mode": frontend.auth,
        "flow": "platform-browser-session" if requires_session else "public",
        "sessionTransport": "cookie",
        "sessionCookieName": os.environ.get("A2A_SESSION_COOKIE_NAME") or "a2a_session",
        "sessionUrl": _request_url(request, "/auth/session"),
        "loginUrl": _clean_optional_string(os.environ.get("A2A_LOGIN_URL")),
        "requiresSession": requires_session,
    }


def frontend_ui_metadata(frontend: PackedFrontend, request: Request | None = None) -> dict[str, Any]:
    return {
        "url": _request_url(request, frontend.mount),
        "type": "static-spa",
        "entry": _mount_path(frontend.mount, "index.html"),
        "config": _request_url(request, frontend.config_path),
        "client": _request_url(request, frontend.client_path),
        "requiresAuth": frontend_requires_session(frontend),
        "auth": frontend_auth_metadata(frontend, request),
    }


def frontend_config_payload(
    agent: A2AAgent,
    frontend: PackedFrontend,
    request: Request,
) -> dict[str, Any]:
    card = agent.card().model_dump(mode="json")
    skills = card.get("skills") if isinstance(card.get("skills"), list) else []
    return {
        "agent": {
            "name": card.get("name") or type(agent).name,
            "description": card.get("description") or type(agent).description,
            "version": card.get("version") or type(agent).version,
        },
        "mount": frontend.mount,
        "ui": frontend_ui_metadata(frontend, request),
        "auth": {
            **frontend_auth_metadata(frontend, request),
            "invokeRequiresSession": _agent_requires_platform_session(agent),
        },
        "endpoints": {
            "a2a": _request_url(request, "/"),
            "agentCard": _request_url(request, "/.well-known/agent-card.json"),
            "legacyAgentCard": _request_url(request, "/.well-known/agent-card"),
            "skills": _request_url(request, "/.well-known/a2a-skills.json"),
            "invoke": _request_url(request, "/invoke"),
            "mcp": _request_url(request, "/mcp"),
            "session": _request_url(request, "/auth/session"),
        },
        "skills": skills,
        "docs": {
            "base": frontend.docs_url,
            "install": f"{frontend.docs_url.rstrip('/')}/",
        },
    }


def skills_payload(agent: A2AAgent, request: Request | None = None) -> dict[str, Any]:
    card = agent.card().model_dump(mode="json")
    return {
        "agent": {
            "name": card.get("name") or type(agent).name,
            "version": card.get("version") or type(agent).version,
        },
        "skills": card.get("skills") if isinstance(card.get("skills"), list) else [],
        "invokeUrl": _request_url(request, "/invoke"),
        "mcpUrl": _request_url(request, "/mcp"),
        "docsUrl": DEFAULT_DOCS_URL,
    }


def mount_packed_frontend(
    app: FastAPI,
    agent: A2AAgent,
    frontend: PackedFrontend,
    *,
    require_session: Callable[[Request], Awaitable[Response | None]] | None = None,
) -> None:
    app.state.a2a_frontend = frontend
    root = frontend.dist_dir.resolve()

    async def authorize(request: Request) -> Response | None:
        if require_session is not None and frontend_requires_session(frontend):
            return await require_session(request)
        return None

    @app.get(frontend.config_path, include_in_schema=False, response_model=None)
    async def packed_frontend_config(request: Request) -> Any:
        auth_response = await authorize(request)
        if auth_response is not None:
            return auth_response
        return frontend_config_payload(agent, frontend, request)

    @app.get(frontend.client_path, include_in_schema=False)
    async def packed_frontend_client(request: Request) -> Response:
        auth_response = await authorize(request)
        if auth_response is not None:
            return auth_response
        return Response(_FRONTEND_CLIENT_JS, media_type="text/javascript; charset=utf-8")

    async def packed_frontend_index(request: Request) -> Response:
        auth_response = await authorize(request)
        if auth_response is not None:
            return auth_response
        return _index_response(root)

    async def packed_frontend_redirect(request: Request) -> Response:
        auth_response = await authorize(request)
        if auth_response is not None:
            return auth_response
        return RedirectResponse(f"{frontend.mount}/", status_code=307)

    async def packed_frontend_asset(request: Request, path: str) -> Response:
        auth_response = await authorize(request)
        if auth_response is not None:
            return auth_response
        if path in {"", "/", "index.html"}:
            return _index_response(root)
        target = _safe_asset_path(root, path)
        if target.is_file():
            return FileResponse(
                target,
                media_type=mimetypes.guess_type(target.name)[0]
                or "application/octet-stream",
            )
        return _index_response(root)

    if frontend.mount == "/":
        app.add_api_route(
            "/",
            packed_frontend_index,
            methods=["GET"],
            include_in_schema=False,
            response_model=None,
        )
    else:
        app.add_api_route(
            frontend.mount,
            packed_frontend_redirect,
            methods=["GET"],
            include_in_schema=False,
            response_model=None,
        )
        app.add_api_route(
            f"{frontend.mount}/",
            packed_frontend_index,
            methods=["GET"],
            include_in_schema=False,
            response_model=None,
        )
    catchall = "/{path:path}" if frontend.mount == "/" else f"{frontend.mount}/{{path:path}}"
    app.add_api_route(
        catchall,
        packed_frontend_asset,
        methods=["GET"],
        include_in_schema=False,
        response_model=None,
    )


def _read_project_config(project: Path) -> dict[str, Any]:
    yaml_path = project / "a2a.yaml"
    if not yaml_path.exists():
        return {}
    data = yaml.safe_load(yaml_path.read_text()) or {}
    return data if isinstance(data, dict) else {}


def _normalize_mount(raw: str) -> str:
    value = raw.strip() or DEFAULT_FRONTEND_MOUNT
    if not value.startswith("/"):
        value = f"/{value}"
    if len(value) > 1:
        value = value.rstrip("/")
    return value


def _mount_path(mount: str, child: str) -> str:
    clean = child.strip("/")
    if mount == "/":
        return f"/{clean}"
    return f"{mount}/{clean}"


def _request_url(request: Request | None, path: str) -> str:
    if request is None:
        return path
    forwarded_proto = (request.headers.get("x-forwarded-proto") or "").split(",")[0].strip()
    forwarded_host = (request.headers.get("x-forwarded-host") or "").split(",")[0].strip()
    scheme = forwarded_proto or request.url.scheme
    host = forwarded_host or request.headers.get("host") or request.url.netloc
    base = f"{scheme}://{host}".rstrip("/")
    clean = path if path.startswith("/") else f"/{path}"
    return f"{base}{clean}"


def _clean_relpath(raw: str) -> str:
    value = raw.strip().replace("\\", "/").strip("/")
    if not value or value == "." or ".." in Path(value).parts:
        raise ValueError(f"unsafe frontend path: {raw!r}")
    return value


def _safe_project_path(project: Path, relpath: str) -> Path:
    return _safe_child_path(project.resolve(), relpath)


def _safe_child_path(root: Path, relpath: str) -> Path:
    clean = relpath.replace("\\", "/").lstrip("/")
    target = (root / clean).resolve()
    try:
        target.relative_to(root.resolve())
    except ValueError as exc:
        raise ValueError(f"unsafe frontend path: {relpath!r}") from exc
    return target


def _safe_asset_path(root: Path, relpath: str) -> Path:
    clean = relpath.replace("\\", "/").lstrip("/")
    if not clean or clean == ".":
        raise HTTPException(400, "path is required")
    target = (root / clean).resolve()
    try:
        target.relative_to(root)
    except ValueError as exc:
        raise HTTPException(400, "unsafe asset path") from exc
    return target


def _index_response(root: Path) -> HTMLResponse:
    index = root / "index.html"
    if not index.is_file():
        raise HTTPException(404, "frontend index.html not found")
    return HTMLResponse(index.read_text(encoding="utf-8"))


def _clean_optional_string(raw: Any) -> str | None:
    if raw is None:
        return None
    value = str(raw).strip()
    return value or None


def _clean_auth(raw: Any) -> str:
    value = str(raw or "inherit").strip().lower()
    return value if value in {"inherit", "platform", "public"} else "inherit"


def frontend_requires_session(frontend: PackedFrontend | FrontendConfig) -> bool:
    if frontend.auth == "public":
        return False
    if frontend.auth == "platform":
        return True
    public_env = os.environ.get("A2A_AGENT_PUBLIC", "").strip().lower()
    return public_env in {"false", "0", "no"}


def _agent_requires_platform_session(agent: A2AAgent) -> bool:
    if type(agent).auth_model is PlatformUserAuth:
        return True
    provisioning = type(agent).llm_provisioning
    value = (
        provisioning.value
        if isinstance(provisioning, LLMProvisioning)
        else str(provisioning)
    )
    return value in {
        LLMProvisioning.PLATFORM.value,
        LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED.value,
    }


def _clean_docs_url(raw: Any) -> str:
    value = str(raw or DEFAULT_DOCS_URL).strip()
    return value or DEFAULT_DOCS_URL


_FRONTEND_CLIENT_JS = """
export async function loadA2AConfig(configUrl = "./config.json") {
  const response = await fetch(configUrl, { credentials: "same-origin" });
  if (!response.ok) throw new Error(`failed to load A2A config: ${response.status}`);
  return response.json();
}

export async function createA2AClient(options = {}) {
  const config = options.config || await loadA2AConfig(options.configUrl || "./config.json");
  const fetchImpl = options.fetch || fetch;
  const credentials = options.credentials || "same-origin";

  async function requestJson(url, init = {}) {
    const response = await fetchImpl(url, { credentials, ...init });
    const text = await response.text();
    const data = text ? JSON.parse(text) : null;
    if (!response.ok) {
      const message = data && (data.detail || data.message) || `request failed: ${response.status}`;
      throw new Error(message);
    }
    return data;
  }

  async function requireSession() {
    const session = await requestJson(config.endpoints.session);
    if (!session || !session.authenticated) {
      const loginUrl = config.auth && config.auth.loginUrl;
      if (loginUrl && typeof window !== "undefined") {
        const next = encodeURIComponent(window.location.href);
        window.location.assign(`${loginUrl}${loginUrl.includes("?") ? "&" : "?"}next=${next}`);
      }
      throw new Error("sign in required");
    }
    return session;
  }

  return {
    config,
    requireSession,
    skills: Object.fromEntries(
      (config.skills || []).map((skill) => [
        skill.name,
        async (args = {}, opts = {}) => {
          if (opts.requireSession || config.auth?.invokeRequiresSession) {
            await requireSession();
          }
          return requestJson(
            `${config.endpoints.invoke}/${encodeURIComponent(skill.name)}`,
            {
              method: "POST",
              headers: { "content-type": "application/json", ...(opts.headers || {}) },
              body: JSON.stringify({ arguments: args, ...opts.body }),
            },
          );
        },
      ]),
    ),
    async call(skill, args = {}, opts = {}) {
      if (opts.requireSession || config.auth?.invokeRequiresSession) {
        await requireSession();
      }
      return requestJson(
        `${config.endpoints.invoke}/${encodeURIComponent(skill)}`,
        {
          method: "POST",
          headers: { "content-type": "application/json", ...(opts.headers || {}) },
          body: JSON.stringify({ arguments: args, ...opts.body }),
        },
      );
    },
    card() {
      return requestJson(config.endpoints.agentCard);
    },
    session() {
      return requestJson(config.endpoints.session);
    },
  };
}

if (typeof window !== "undefined") {
  window.A2ACloud = { createA2AClient, loadA2AConfig };
}
""".lstrip()
