"""Worker task: provision CF cloud resources for a shopify_app project.

Creates per-project:
  - D1 database  (binding: DB)
  - CF Queue     (binding: JOB_QUEUE, producer + consumer)
  - R2 bucket    (binding: FILES)
  - KV namespace (binding: CACHE)
  - P2HS Cloud service token (set as GitHub Actions secret P2HS_CLOUD_TOKEN)

All CF API calls are idempotent: create-or-get by name so retries are safe.
After provisioning, enqueues a wrangler.toml rebuild so bindings land in the repo.
"""
from __future__ import annotations

from datetime import datetime, timezone
from uuid import UUID

import httpx
import structlog

logger = structlog.get_logger()

CF_API = "https://api.cloudflare.com/client/v4"
_RESOURCE_NAME_MAX = 63


def _slug(project_ref: str) -> str:
    s = "".join(c if c.isalnum() or c == "-" else "-" for c in project_ref.lower())
    return f"p2hs-shopify-{s}"[:_RESOURCE_NAME_MAX]


def _resource_names(project_ref: str) -> dict[str, str]:
    base = _slug(project_ref)
    return {
        "d1": base,
        "queue": f"{base}-jobs"[:_RESOURCE_NAME_MAX],
        "r2": f"{base}-files"[:_RESOURCE_NAME_MAX],
        "kv": f"{base}-cache"[:_RESOURCE_NAME_MAX],
    }


def _cf_headers(api_token: str) -> dict[str, str]:
    return {"Authorization": f"Bearer {api_token}", "Content-Type": "application/json"}


async def _create_or_get_d1(client: httpx.AsyncClient, account_id: str, token: str, name: str) -> str:
    """Returns CF D1 database UUID, creating it if absent."""
    r = await client.post(
        f"{CF_API}/accounts/{account_id}/d1/database",
        headers=_cf_headers(token),
        json={"name": name},
    )
    if r.status_code in (200, 201):
        return r.json()["result"]["uuid"]
    if r.status_code == 409:
        # Already exists — find it by name
        r2 = await client.get(
            f"{CF_API}/accounts/{account_id}/d1/database",
            headers=_cf_headers(token),
            params={"per_page": "100"},
        )
        r2.raise_for_status()
        for db in r2.json().get("result", []):
            if db.get("name") == name:
                return db["uuid"]
        raise RuntimeError(f"D1 conflict but could not find existing database '{name}'")
    r.raise_for_status()
    raise RuntimeError(f"Unexpected D1 response {r.status_code}: {r.text[:300]}")


async def _create_or_get_queue(client: httpx.AsyncClient, account_id: str, token: str, name: str) -> str:
    """Returns CF Queue name (idempotent). Returns name on success."""
    r = await client.post(
        f"{CF_API}/accounts/{account_id}/queues",
        headers=_cf_headers(token),
        json={"queue_name": name},
    )
    if r.status_code in (200, 201):
        return r.json()["result"]["queue_name"]
    if r.status_code == 409:
        return name  # Already exists
    r.raise_for_status()
    raise RuntimeError(f"Unexpected Queue response {r.status_code}: {r.text[:300]}")


async def _create_or_get_r2(client: httpx.AsyncClient, account_id: str, token: str, name: str) -> str:
    """Creates R2 bucket if absent. Returns bucket name."""
    r = await client.put(
        f"{CF_API}/accounts/{account_id}/r2/buckets/{name}",
        headers=_cf_headers(token),
    )
    if r.status_code in (200, 201, 409):
        return name
    r.raise_for_status()
    raise RuntimeError(f"Unexpected R2 response {r.status_code}: {r.text[:300]}")


async def _create_or_get_kv(client: httpx.AsyncClient, account_id: str, token: str, title: str) -> tuple[str, str]:
    """Returns (namespace_id, title), creating if absent."""
    r = await client.post(
        f"{CF_API}/accounts/{account_id}/storage/kv/namespaces",
        headers=_cf_headers(token),
        json={"title": title},
    )
    if r.status_code in (200, 201):
        result = r.json()["result"]
        return result["id"], result["title"]
    if r.status_code == 409:
        # Already exists — find by title
        r2 = await client.get(
            f"{CF_API}/accounts/{account_id}/storage/kv/namespaces",
            headers=_cf_headers(token),
            params={"per_page": "100"},
        )
        r2.raise_for_status()
        for ns in r2.json().get("result", []):
            if ns.get("title") == title:
                return ns["id"], ns["title"]
        raise RuntimeError(f"KV conflict but could not find existing namespace '{title}'")
    r.raise_for_status()
    raise RuntimeError(f"Unexpected KV response {r.status_code}: {r.text[:300]}")


async def provision_shopify_app_resources(ctx: dict, project_id: str) -> dict:
    """Provision D1 + Queue + R2 + KV for this shopify_app project, then trigger rebuild.

    The P2HS Cloud client ID (cli_{project_ref}) is stored so the worker can use
    the Cloud handshake flow. No secret token is generated here — the client is
    already registered by cloud_sync; the worker URL becomes its allowed origin
    once the first wrangler deploy completes.
    """
    from src.core.database import async_session_factory
    from src.core.exceptions import NotFoundError
    from src.domain.cloud_infra.repository import (
        CFAccountRepository,
        CloudInstanceRepository,
        WorkspaceCloudAssignmentRepository,
    )
    from src.domain.cloud_infra.service import CloudInfraService
    from src.domain.projects.repository import ProjectRepository, ProjectRepoRepository
    from src.domain.shopify_apps.repository import ShopifyAppResourcesRepository
    from src.domain.projects.setup_step_constants import (
        SETUP_STEP_SHOPIFY_RESOURCE_PROVISION,
        SETUP_STEP_STATUS_ERROR,
        SETUP_STEP_STATUS_OK,
    )
    from src.config import get_settings
    from src.workers.enqueue import enqueue_shopify_app_rebuild

    db_factory = ctx.get("db_factory") or async_session_factory
    pid = UUID(project_id)

    async with db_factory() as db:
        project = await ProjectRepository(db).get_by_id(pid)
        if not project or project.type != "shopify_app":
            logger.warning("shopify_provision_skipped", project_id=project_id, reason="not_shopify_app")
            return {"status": "skipped"}

        repo_row = await ProjectRepoRepository(db).get_by_project_id(pid)
        workspace_id = project.workspace_id
        project_ref = project.project_ref or ""
        repo_full = repo_row.repo_full_name if repo_row else None

        # Resolve CF credentials (workspace pool → settings fallback)
        settings = get_settings()
        cf_token = (settings.cloudflare_api_token or "").strip()
        cf_account = (settings.cloudflare_account_id or "").strip()
        try:
            cf_svc = CloudInfraService(
                cf_account_repo=CFAccountRepository(db),
                instance_repo=CloudInstanceRepository(db),
                assignment_repo=WorkspaceCloudAssignmentRepository(db),
            )
            creds = await cf_svc.get_credentials_for_workspace(workspace_id)
            cf_token = creds.api_token
            cf_account = creds.account_id
        except (NotFoundError, ValueError):
            pass

    if not cf_token or not cf_account:
        logger.error("shopify_provision_failed", project_id=project_id, reason="no_cf_credentials")
        async with db_factory() as db:
            await ShopifyAppResourcesRepository(db).upsert(
                pid, status="failed", last_error="No Cloudflare credentials configured for workspace"
            )
            await ProjectRepository(db).merge_setup_step(
                pid,
                SETUP_STEP_SHOPIFY_RESOURCE_PROVISION,
                {"status": SETUP_STEP_STATUS_ERROR, "last_error": "no_cf_credentials"},
            )
            await db.commit()
        return {"status": "failed", "reason": "no_cf_credentials"}

    names = _resource_names(project_ref)
    logger.info("shopify_provision_start", project_id=project_id, names=names)

    try:
        async with httpx.AsyncClient(timeout=30) as client:
            d1_id = await _create_or_get_d1(client, cf_account, cf_token, names["d1"])
            queue_name = await _create_or_get_queue(client, cf_account, cf_token, names["queue"])
            r2_name = await _create_or_get_r2(client, cf_account, cf_token, names["r2"])
            kv_id, kv_name = await _create_or_get_kv(client, cf_account, cf_token, names["kv"])

        # Cloud client ID — derived deterministically, not a secret.
        # cloud_sync already registers this client with Cloud; the worker URL
        # becomes the allowed_origins entry once the first deploy completes.
        cloud_client_id = f"cli_{project_ref}" if project_ref else f"cli_{project_id}"

        async with db_factory() as db:
            await ShopifyAppResourcesRepository(db).upsert(
                pid,
                d1_database_id=d1_id,
                d1_database_name=names["d1"],
                queue_name=queue_name,
                r2_bucket_name=r2_name,
                kv_namespace_id=kv_id,
                kv_namespace_name=kv_name,
                cloud_client_id=cloud_client_id,
                status="ready",
                last_error=None,
                provisioned_at=datetime.now(timezone.utc),
            )
            await ProjectRepository(db).merge_setup_step(
                pid,
                SETUP_STEP_SHOPIFY_RESOURCE_PROVISION,
                {"status": SETUP_STEP_STATUS_OK},
            )
            await db.commit()

        logger.info(
            "shopify_provision_ok",
            project_id=project_id,
            d1_id=d1_id,
            queue=queue_name,
            r2=r2_name,
            kv_id=kv_id,
        )

    except Exception as exc:
        err = str(exc)[:800]
        logger.error("shopify_provision_error", project_id=project_id, error=err)
        async with db_factory() as db:
            await ShopifyAppResourcesRepository(db).upsert(pid, status="failed", last_error=err)
            await ProjectRepository(db).merge_setup_step(
                pid,
                SETUP_STEP_SHOPIFY_RESOURCE_PROVISION,
                {"status": SETUP_STEP_STATUS_ERROR, "last_error": err},
            )
            await db.commit()
        raise

    # Trigger rebuild so wrangler.toml picks up the new bindings + cloud vars
    await enqueue_shopify_app_rebuild(project_id)
    return {"status": "ok", "d1": d1_id, "queue": queue_name, "r2": r2_name, "kv": kv_id, "client_id": cloud_client_id}
