"""Read-only readiness checks for the project ``ready`` lifecycle state."""

from __future__ import annotations

from uuid import UUID

import httpx
from sqlalchemy import select

from src.domain.capabilities.adapters import FormsProvisioner
from src.domain.capabilities.lifecycle import CapabilityState
from src.domain.capabilities.native_adapters import (
    FilesProvisioner,
    NotificationsProvisioner,
    RecordsProvisioner,
    SecretsProvisioner,
    WebhooksProvisioner,
)
from src.domain.deployments.models import ProjectDeploymentTarget
from src.domain.project_auth.auth_contract import normalize_auth_contract
from src.domain.project_auth.models import ProjectAuthConfig, ProjectAuthKey, ProjectAuthRole
from src.domain.projects.models import Project
from src.domain.projects.runtime_invariants import ProjectRuntimeInvariants, validate_runtime_routes
from src.integrations.cloud_control_client import CloudControlClient

_REQUIRED_AUTH_RUNTIME_PORTS = {
    "auth.magic_link_request",
    "auth.magic_link_consume",
    "auth.me",
    "auth.signout",
    "auth.permissions_check",
}


async def _w4p_runtime_probe(
    db, project: Project, *, required_ports: set[str] | None = None
) -> str | None:
    """Verify that the public host reaches the shared BFF, not static HTML."""
    target = (
        await db.execute(
            select(ProjectDeploymentTarget)
            .where(ProjectDeploymentTarget.project_id == project.id)
            .where(ProjectDeploymentTarget.environment == "production")
        )
    ).scalar_one_or_none()
    if not target or target.routing_provider_kind != "cloudflare_w4p":
        return None
    hostname = (project.live_hostname or project.preview_hostname or "").strip()
    if not hostname:
        return "bff_routing_host_missing"
    if not hostname.startswith("http://") and not hostname.startswith("https://"):
        hostname = "https://" + hostname
    try:
        async with httpx.AsyncClient(timeout=8.0, follow_redirects=False) as client:
            response = await client.get(hostname.rstrip("/") + "/api/__p2hs/health")
        content_type = (response.headers.get("content-type") or "").lower()
        if "text/html" in content_type:
            return "bff_routing_returned_html"
        payload = response.json()
        if response.status_code != 200 or payload.get("bff") is not True:
            return "bff_routing_health_failed"
        if str(payload.get("project_id")) != str(project.id):
            return "bff_routing_project_mismatch"
        if payload.get("runtime_contract") != "p2hs-bff-v1":
            return "bff_runtime_contract_mismatch"
        if required_ports:
            advertised = {
                str(port)
                for capability in (payload.get("capabilities") or {}).values()
                if isinstance(capability, dict)
                for port in (capability.get("ports") or [])
            }
            missing = sorted(required_ports - advertised)
            if missing:
                return "bff_logical_ports_missing=" + ",".join(missing)
    except (httpx.HTTPError, ValueError, TypeError):
        return "bff_routing_unavailable"
    return None


async def project_ready_contract_errors(db, project_id: UUID) -> list[str]:
    """Return local contract contradictions that must block ``status=ready``.

    Cloud reconciliation is idempotent, but readiness must still prove that the
    executable Cloud projection agrees with Core before publishing ``ready``.
    """
    errors: list[str] = []
    project = await db.get(Project, project_id)
    if not project:
        return ["project_not_found"]

    invariants = ProjectRuntimeInvariants.model_validate(project.runtime_invariants or {})
    errors.extend(validate_runtime_routes(invariants))

    config = (
        await db.execute(
            select(ProjectAuthConfig).where(ProjectAuthConfig.project_id == project_id)
        )
    ).scalar_one_or_none()
    if not config or not config.enabled:
        errors.append("auth_config_not_enabled")
        return errors

    active_key = (
        await db.execute(
            select(ProjectAuthKey.id).where(
                ProjectAuthKey.project_id == project_id,
                ProjectAuthKey.status == "active",
            )
        )
    ).scalar_one_or_none()
    if not active_key:
        errors.append("auth_active_key_missing")

    role_names = set(
        (
            await db.execute(
                select(ProjectAuthRole.name).where(ProjectAuthRole.project_id == project_id)
            )
        )
        .scalars()
        .all()
    )
    missing_roles = {"member", "admin", "super_admin"} - role_names
    if missing_roles:
        errors.append("auth_roles_missing=" + ",".join(sorted(missing_roles)))

    # Ready is a cross-plane contract, not merely a Core row state. Read back
    # Cloud's non-secret projection and block contradictory routes, methods, or
    # default roles before publishing readiness.
    try:
        cloud = await CloudControlClient().get_auth_config(str(project_id))
        if cloud.get("admin_panel_path") != invariants.admin_panel_path:
            errors.append("cloud_admin_panel_path_mismatch")
        if cloud.get("customer_login_path") != invariants.customer_login_path:
            errors.append("cloud_customer_login_path_mismatch")
        if cloud.get("admin_panel_path") == cloud.get("customer_login_path"):
            errors.append("cloud_auth_routes_collide")
        normalized_auth = normalize_auth_contract(
            providers=config.providers or [],
            auth_methods=getattr(config, "auth_methods", None),
            audiences=getattr(config, "audiences", None),
        )
        core_methods = set(normalized_auth["auth_methods"])
        if set(cloud.get("auth_methods") or []) != core_methods:
            errors.append("cloud_auth_methods_mismatch")
        if set(cloud.get("audiences") or []) != set(normalized_auth["audiences"]):
            errors.append("cloud_auth_audiences_mismatch")
        cloud_roles = {
            str(role.get("name")) for role in (cloud.get("roles") or []) if isinstance(role, dict)
        }
        if not {"member", "admin", "super_admin"}.issubset(cloud_roles):
            errors.append("cloud_auth_roles_incomplete")
    except Exception:
        errors.append("cloud_auth_projection_unavailable")
    required_runtime_ports = set(_REQUIRED_AUTH_RUNTIME_PORTS)
    forms_requirements = await FormsProvisioner(db).runtime_requirements(project)
    required_runtime_ports.update(port.port_id for port in forms_requirements.ports)
    native_provisioners = (
        FilesProvisioner(db),
        RecordsProvisioner(db),
        NotificationsProvisioner(db),
        WebhooksProvisioner(db),
        SecretsProvisioner(db),
    )
    for provisioner in native_provisioners:
        status = await provisioner.status(project)
        if status.state in {CapabilityState.BLOCKED, CapabilityState.DRIFTED}:
            errors.append(f"{provisioner.capability}_capability_{status.state.value}")
        requirements = await provisioner.runtime_requirements(project)
        required_runtime_ports.update(port.port_id for port in requirements.ports)
    bff_error = await _w4p_runtime_probe(
        db,
        project,
        required_ports=required_runtime_ports,
    )
    if bff_error:
        errors.append(bff_error)
    return errors
