"""Resolve a project's persisted invariants + real platform capability availability
into one machine-readable contract, injected into both the generated files and the
AI's own prompt.

This is deliberately a *resolver*, not a second policy engine: every capability check
here reuses the same data sources `src/domain/cloud/email_delivery.py` uses for the
real, authoritative decision at job-send time. The difference is scope, not policy —
this module only answers "is X available and what should codegen call it", it never
reserves quota or picks a connection for an actual send. That stays exactly where it
is today, in `resolve_email_delivery` at job-authorize time.

Never expose raw vendor names (``ses``, AWS keys) or connector secrets here — only the
labels in ``_EMAIL_UNAVAILABLE`` / ``p2hs-platform-email`` / ``p2hs-connector``.
"""
from __future__ import annotations

from typing import Any
from uuid import UUID

import structlog
from pydantic import BaseModel, Field

from src.domain.projects.runtime_invariants import ProjectRuntimeInvariants

logger = structlog.get_logger()

# Same fixed rules PlatformBackendCapabilities already advertises as prose; exposed
# here too as short machine-checkable strings for a future validator. Not a fork —
# `platform_backend.py` keeps owning the authoritative prose list.
_MUST = [
    "use P2HS Cloud for persistent application data",
    "use the resolved project email capability",
    "use the configured admin route everywhere",
    "preserve explicit user requirements across regeneration",
]
_MUST_NOT = [
    "create an independent backend when Cloud satisfies the requirement",
    "hardcode /admin when a custom admin route exists",
    "bypass the platform email resolver",
    "implement alternative persistence without explicit permission",
]


def _required_auth_modes(project_type: str) -> set[str]:
    """Capabilities required by platform-seeded composition, independent of
    ``invariants.auth_modes`` (which records *requested* capabilities inferred
    from prompt text). Effective capability = requested | required.

    ``app_sdk.staff_shell_files`` seeds a staff admin surface for every project
    of these types the first time one doesn't already exist, and every variant
    of that seeded surface (staff-admin.html, staff-admin.vue,
    P2hsStaffDashboard.jsx) implements sign-in via magic link. So any project
    of these types structurally requires ``magic_link`` once seeded, whether
    or not a prompt ever mentioned it — a prompt-only signal previously let a
    project ship a scaffold its own contract disallowed. See backend_guard.py's
    ``magic_link_disabled_by_runtime_contract`` check, which this makes moot
    for the platform-seeded shell rather than requiring a source migration.
    """
    from src.domain.codegen.app_sdk import _PAGES_TYPES

    return {"magic_link"} if project_type in _PAGES_TYPES else set()


class ProjectRuntimeContract(BaseModel):
    contract_version: int = 1
    runtime: dict[str, Any] = Field(default_factory=lambda: {
        "backend": "p2hs-cloud", "allow_custom_backend": False,
    })
    routes: dict[str, str] = Field(default_factory=lambda: {
        "admin_panel_path": "/admin", "customer_login_path": "/login",
    })
    project: dict[str, Any] = Field(default_factory=dict)
    capabilities: dict[str, Any] = Field(default_factory=dict)
    presentation: dict[str, Any] = Field(default_factory=dict)
    requirements: dict[str, Any] = Field(default_factory=dict)
    constraints: dict[str, list[str]] = Field(default_factory=lambda: {
        "must": list(_MUST), "must_not": list(_MUST_NOT),
    })


async def resolve_email_capability(
    *,
    connector_service,
    workspace_id: UUID,
    project_id: UUID,
    invariants: ProjectRuntimeInvariants,
    enabled_capabilities: set,
    email_domain_repo=None,
    settings=None,
) -> dict[str, Any]:
    """Availability only -- read-only mirror of email_delivery.py's precedence.

    Dependencies are injected (same convention as resolve_email_delivery) so this is
    testable with fakes, not a real DB/connector round-trip. Tier 2 (connector) reuses
    ConnectorService.resolve_email_send, which only inspects + decides, never
    authorizes/reserves. Tier 3 (managed domain) checks verification status directly
    and deliberately skips check_and_reserve -- a prompt being built must never
    consume send quota.
    """
    from src.config import get_settings
    from src.integrations.email_provider import cloud_email_transport_configured

    email_invoke = {
        "capability": "email.send",
        "request_type": "email.send",
        "invoke": "POST /api/jobs/email.send",
        "provider_actions_role": "escape_hatch",
    }
    unavailable = {"provider": None, "binding": None, "available": False, **email_invoke}

    def _resolved(result: dict[str, Any], tier: str) -> dict[str, Any]:
        # One log line regardless of which tier decided, distinguishing "legitimately
        # unavailable" from a resolver exception (logged separately by the caller,
        # resolve_runtime_contract) -- both used to return the identical `unavailable`
        # dict with zero logging, making an outage indistinguishable from "this
        # project just has no email configured."
        merged = {**result, **email_invoke}
        logger.info(
            "capability.email.resolved",
            project_id=str(project_id),
            workspace_id=str(workspace_id),
            tier=tier,
            available=merged["available"],
            provider=merged["provider"],
        )
        return merged

    decision = await connector_service.resolve_email_send(
        workspace_id=workspace_id,
        project_id=project_id,
        enabled_capabilities=enabled_capabilities,
        provider_hint=invariants.email_preference,
    )
    if decision and decision.allowed:
        key = decision.connector.key if decision.connector else invariants.email_preference
        return _resolved({"provider": "p2hs-connector", "binding": key, "available": True}, "connector")

    if email_domain_repo is not None:
        domain = await email_domain_repo.get_by_workspace(workspace_id)
        if domain and domain.status == "verified":
            return _resolved(
                {"provider": "p2hs-platform-email", "binding": "managed_domain", "available": True},
                "managed_domain",
            )

    if cloud_email_transport_configured(settings or get_settings()):
        return _resolved(
            {"provider": "p2hs-platform-email", "binding": "platform_default", "available": True},
            "platform_default",
        )

    return _resolved(unavailable, "none")


_ENTITY_INGEST_INSTALLED_STATUSES = {"installed", "configured", "deployed", "verified"}


async def _installed_entity_ingest_bindings(db, project_id) -> list[dict[str, Any]]:
    """Every entity_ingest_binding on a pack actually installed for this
    project, paired with its forward_path -- what entity_ingest_adapter_files
    needs to install the matching runtime route(s). Re-reads load_pack_contract
    live (not the project's stored composition snapshot) so a since-updated
    catalog_v2.py overlay takes effect without a re-seed, same as
    EntityIngestService.ingest."""
    from sqlalchemy import select

    from src.domain.feature_packs.models import FeaturePack, ProjectFeaturePack
    from src.domain.feature_packs.normalize import load_pack_contract

    installed = await db.execute(
        select(ProjectFeaturePack.pack_slug).where(
            ProjectFeaturePack.project_id == project_id,
            ProjectFeaturePack.status.in_(_ENTITY_INGEST_INSTALLED_STATUSES),
        )
    )
    pack_slugs = {row[0] for row in installed.all()}
    if not pack_slugs:
        return []

    packs = await db.execute(select(FeaturePack).where(FeaturePack.slug.in_(pack_slugs)))
    bindings: list[dict[str, Any]] = []
    for pack_row in packs.scalars().all():
        contract = load_pack_contract(pack_row.slug, pack_row.composition or {})
        if not contract.entity_ingest_bindings:
            continue
        webhooks_by_provider = {w.provider: w for w in contract.webhook_bindings}
        for binding in contract.entity_ingest_bindings:
            webhook = webhooks_by_provider.get(binding.webhook_provider)
            if webhook is None:
                continue
            bindings.append({
                "pack_slug": pack_row.slug,
                "provider": binding.webhook_provider,
                "forward_path": webhook.forward_path,
            })
    return bindings


async def resolve_runtime_contract(db, project) -> ProjectRuntimeContract:
    """Build the contract for one project from a live DB session.

    Never raises -- callers must not let a resolution failure block codegen; falls
    back to today's literal defaults (email unavailable) on any error.
    """
    from src.domain.connectors.repository import ConnectorRepository
    from src.domain.connectors.service import ConnectorService
    from src.domain.email_domains.repository import EmailDomainRepository
    from src.domain.projects.repository import ProjectSettingsRepository
    from src.integrations.cloud_control_client import CloudControlClient
    from src.integrations.platform_ports import enabled_capabilities as project_capabilities

    invariants = ProjectRuntimeInvariants.model_validate(project.runtime_invariants or {})
    configured_auth_methods: set[str] = set()
    try:
        from sqlalchemy import select
        from src.domain.project_auth.models import ProjectAuthConfig

        auth_config = None
        if db is not None:
            auth_config = (
                await db.execute(select(ProjectAuthConfig).where(ProjectAuthConfig.project_id == project.id))
            ).scalar_one_or_none()
        if auth_config:
            configured_auth_methods = {
                str(value).strip().lower()
                for value in (getattr(auth_config, "auth_methods", None) or [])
                if str(value).strip().lower() in {"password", "passwordless", "oidc", "ldap", "google"}
            }
            if not configured_auth_methods:
                providers = {str(value).strip().lower() for value in (auth_config.providers or [])}
                if "password" in providers:
                    configured_auth_methods.add("password")
                if "email" in providers or "magic_link" in providers:
                    configured_auth_methods.add("passwordless")
                if "google" in providers:
                    configured_auth_methods.add("oidc")
    except Exception:
        logger.warning("capability.auth.resolve_failed", project_id=str(project.id), exc_info=True)
    # requested (prompt-inferred, persisted) | required (composition-structural,
    # computed) -- see _required_auth_modes. Never persisted back onto
    # invariants: "required" is a pure function of project.type, so every
    # resolution stays correct without a migration or backfill.
    required_auth_modes = _required_auth_modes(project.type)
    effective_auth_modes = (configured_auth_methods or set(invariants.auth_modes)) | required_auth_modes
    composition_only = required_auth_modes - set(invariants.auth_modes)
    if composition_only:
        # Observability only, not provenance storage: shows how often the
        # composition-required union actually contributes something no prompt
        # ever requested, without persisting anything new.
        logger.info(
            "capability.auth.required_by_composition",
            project_id=str(project.id),
            auth_modes=sorted(composition_only),
        )
    contract = ProjectRuntimeContract(
        runtime={"backend": "p2hs-cloud", "allow_custom_backend": invariants.allow_custom_backend},
        routes={
            "admin_panel_path": invariants.admin_panel_path,
            "customer_login_path": invariants.customer_login_path,
        },
        project={"name": getattr(project, "name", ""), "type": project.type},
        capabilities={
            "data": {"provider": "p2hs-cloud", "available": True},
            "auth": {
                "provider": "p2hs-auth",
                "methods": sorted(effective_auth_modes),
                "password": "password" in effective_auth_modes,
                "password_reset": "password" in effective_auth_modes,
                # ``passwordless`` is the persisted canonical mode; the
                # generated runtime still consumes the explicit magic-link
                # capability flag.
                "magic_link": bool({"magic_link", "passwordless"} & effective_auth_modes),
            },
        },
        presentation={},
        requirements={
            "persistence_required": invariants.persistence_required,
            "admin_must_read_persisted": invariants.admin_must_read_persisted,
        },
    )
    try:
        settings_row = await ProjectSettingsRepository(db).get_or_create(project.id)
        search_adapters = await ProjectSettingsRepository(db).get_search_index_adapters(project.id)
        contract.capabilities["records.search.external_index"] = {
            "available": bool(search_adapters),
            "adapter_count": len(search_adapters),
            "provider": "core-authorized-cloud-adapter" if search_adapters else None,
        }
        email_capability = await resolve_email_capability(
            connector_service=ConnectorService(repo=ConnectorRepository(db), cloud_client=CloudControlClient()),
            workspace_id=project.workspace_id,
            project_id=project.id,
            invariants=invariants,
            enabled_capabilities=project_capabilities(settings_row.settings or {}),
            email_domain_repo=EmailDomainRepository(db),
        )
        contract.capabilities["email"] = email_capability
        # Provider-binding ids are the stable manifest lookup key; keep the
        # historical compact `email` projection for existing generated apps.
        contract.capabilities["email.send"] = email_capability
    except Exception:
        # Distinct from the "legitimately unavailable" capability.email.resolved log
        # above (tier="none") -- this is a resolver failure (DB outage, a connector
        # service throwing), failing closed to the same unavailable dict, but that
        # must not be silently indistinguishable from "no email configured" in logs.
        logger.warning(
            "capability.email.resolve_failed",
            project_id=str(project.id),
            exc_info=True,
        )
        email_unavailable = {
            "provider": None,
            "binding": None,
            "available": False,
            "capability": "email.send",
            "request_type": "email.send",
            "invoke": "POST /api/jobs/email.send",
            "provider_actions_role": "escape_hatch",
        }
        contract.capabilities["email"] = email_unavailable
        contract.capabilities["email.send"] = email_unavailable
    try:
        contract.capabilities["entity_ingest_bindings"] = await _installed_entity_ingest_bindings(
            db, project.id
        )
    except Exception:
        # Fail closed to "no ingest bindings installed" -- a resolver outage here
        # must not be silently indistinguishable from "this project has none",
        # so this is logged separately, same convention as the email capability.
        logger.warning(
            "capability.entity_ingest.resolve_failed", project_id=str(project.id), exc_info=True
        )
        contract.capabilities["entity_ingest_bindings"] = []
    from src.domain.codegen.admin_presentation import build_admin_presentation_contract

    contract.presentation = build_admin_presentation_contract(contract.model_dump())
    return contract
