"""Deploys a Function Library / project function's code as a real Cloudflare Worker.

Uploads into the shared ``p2hs-cloud-functions`` Workers-for-Platforms dispatch
namespace (see P2HSCloud ``apps/pc-functions/wrangler.toml``). The dispatcher Worker
there (``pc-functions``) looks scripts up by name via its ``FUNCTIONS`` dispatch
binding, so the script name computed here must match what Core pushes as
``worker_name`` in the Cloud control-plane metadata push.

Supported runtime: ``js`` only. ``ts``/``python`` require a build step this module
does not implement yet — deploy raises :class:`EdgeFunctionDeployError` for those.
"""
from __future__ import annotations

import json
import re
from typing import Any
from urllib.parse import quote
from uuid import UUID

import httpx
import structlog

from src.config import get_settings

logger = structlog.get_logger()

_CF_API_BASE = "https://api.cloudflare.com/client/v4"


class EdgeFunctionDeployError(RuntimeError):
    pass


def compute_worker_name(project_id: UUID, slug: str) -> str:
    """Deterministic, CF-script-name-safe identifier for a project's deployed function."""
    raw = f"fn-{str(project_id).replace('-', '')[:12]}-{slug}"
    safe = re.sub(r"[^a-z0-9-]+", "-", raw.lower())
    safe = re.sub(r"-{2,}", "-", safe).strip("-")
    return safe[:63].rstrip("-")


def _compat_date() -> str:
    import datetime as dt

    value = (get_settings().cloudflare_worker_compatibility_date or "").strip()
    return value or dt.date.today().isoformat()


async def _cf_request(
    method: str,
    path: str,
    *,
    files: Any = None,
    expected: set[int] | None = None,
) -> Any:
    settings = get_settings()
    token = str(settings.cloudflare_api_token).strip()
    account_id = str(settings.cloudflare_account_id).strip()
    if not token or not account_id:
        raise EdgeFunctionDeployError(
            "CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID are not configured"
        )

    expected = expected or {200}
    url = f"{_CF_API_BASE}/accounts/{account_id}{path}"
    async with httpx.AsyncClient(timeout=30.0) as client:
        resp = await client.request(
            method.upper(), url, files=files, headers={"Authorization": f"Bearer {token}"}
        )
    if resp.status_code not in expected:
        try:
            detail: Any = resp.json()
        except Exception:
            detail = resp.text
        raise EdgeFunctionDeployError(
            f"CF API {method.upper()} {path} -> {resp.status_code}: {detail}"
        )
    return resp.json() if resp.content else {}


def _bindings(env_vars: dict[str, str], secrets: dict[str, str]) -> list[dict[str, str]]:
    bindings = [
        {"type": "plain_text", "name": name, "text": value} for name, value in env_vars.items()
    ]
    bindings += [
        {"type": "secret_text", "name": name, "text": value} for name, value in secrets.items()
    ]
    return bindings


async def deploy_function_script(
    *,
    project_id: UUID,
    workspace_id: UUID,
    slug: str,
    runtime: str,
    code: str,
    gateway_base_url: str,
    project_token: str,
    extra_env_vars: dict[str, str] | None = None,
    extra_secrets: dict[str, str] | None = None,
) -> str:
    """Upload ``code`` as the Worker script for this project's function.

    ``extra_env_vars``/``extra_secrets`` carry a required connector's resolved
    credentials (see :mod:`connector_bindings`) — merged in alongside the platform's
    own bindings.

    Returns the ``worker_name`` the script was deployed under (also the value Core
    must push to Cloud's control-plane metadata for the dispatcher to find it).
    """
    if runtime != "js":
        raise EdgeFunctionDeployError(
            f"Unsupported function runtime for deploy: {runtime!r}. "
            "Only 'js' is deployable until TypeScript/Python build support ships."
        )
    if not code or not code.strip():
        raise EdgeFunctionDeployError("Function has no code to deploy")

    worker_name = compute_worker_name(project_id, slug)
    namespace = get_settings().cloudflare_functions_namespace
    metadata = json.dumps(
        {
            "main_module": "worker.mjs",
            "compatibility_date": _compat_date(),
            "bindings": _bindings(
                env_vars={
                    "PROJECT_ID": str(project_id),
                    "WORKSPACE_ID": str(workspace_id),
                    "GATEWAY_BASE_URL": gateway_base_url,
                    **(extra_env_vars or {}),
                },
                secrets={"PROJECT_FN_TOKEN": project_token, **(extra_secrets or {})},
            ),
        }
    )
    files = {
        "metadata": (None, metadata, "application/json"),
        "worker.mjs": ("worker.mjs", code, "application/javascript+module"),
    }
    script_path = (
        f"/workers/dispatch/namespaces/{quote(namespace, safe='')}"
        f"/scripts/{quote(worker_name, safe='')}"
    )
    await _cf_request("PUT", script_path, files=files, expected={200, 201})
    logger.info(
        "edge_function_script_deployed",
        project_id=str(project_id),
        slug=slug,
        worker_name=worker_name,
    )
    return worker_name


async def delete_function_script(worker_name: str) -> None:
    namespace = get_settings().cloudflare_functions_namespace
    script_path = (
        f"/workers/dispatch/namespaces/{quote(namespace, safe='')}"
        f"/scripts/{quote(worker_name, safe='')}"
    )
    await _cf_request("DELETE", script_path, expected={200, 404})
    logger.info("edge_function_script_deleted", worker_name=worker_name)
