"""Account-integration provider abstractions.

Mirrors the :class:`BillingProvider` shape from
:mod:`alphaswarm_admin.accounts.billing` but for *credential-bearing* per-org
account links (HuggingFaceHub, DockerHub, future image registries,
future dataset registries) where the wizard surface is symmetric:

- single ``connect`` step (PAT in, masked metadata out — never the PAT
  itself);
- ``health`` ping that confirms the persisted credential still works;
- ``disconnect`` that removes the local credential record.

Concrete providers must NEVER read ``settings.<service>_token``
directly (AGENTS rule 26). They resolve through
:class:`CredentialResolver` for reads and call into the in-memory
:class:`AdminAuditSink` (via the route layer) for write paths.

The data type returned by every provider is :class:`IntegrationRecord`.
The token field is always ``None`` on read paths — only the metadata
fields (kind / namespace / connected_at / status / error / health)
ever leave the BFF process.
"""
from __future__ import annotations

import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any

logger = logging.getLogger(__name__)


class IntegrationKind(str, Enum):
    """Set of supported account-integration kinds.

    The kind doubles as the resolver service name: a HuggingFaceHub
    integration for org ``acme`` resolves through
    ``CredentialKey(service='huggingface', purpose='org:acme')`` and a
    Docker Hub integration through
    ``CredentialKey(service='docker_hub', purpose='org:acme')``.

    The four ``CLOUD_*`` kinds carry the same shape but expose four
    extra wizard-only methods (``bootstrap_artifacts``,
    ``validate_identity``, ``validate_permissions``,
    ``enumerate_resources``) implemented by the per-cloud providers
    under :mod:`alphaswarm_admin.providers.cloud_*`. Federated-first by
    design: AWS uses cross-account role + external id, Azure uses
    Workload Identity Federation, GCP uses WIF + impersonation, and
    Cloudflare uses scoped API tokens.
    """

    HUGGINGFACE = "huggingface"
    DOCKER_HUB = "docker_hub"
    GITHUB = "github"
    CLOUD_AWS = "cloud_aws"
    CLOUD_AZURE = "cloud_azure"
    CLOUD_GCP = "cloud_gcp"
    CLOUD_CLOUDFLARE = "cloud_cloudflare"


PLATFORM_ORG_ID: str = "__platform__"
"""Synthetic ``org_id`` used by the admin-tenant cloud onboarding flow.

Lets the same per-org routes and provider implementations service the
flat ``/admin/settings/cloud/*`` legacy surface — the route handler
fixes ``org_id='__platform__'`` and delegates to the same
:class:`AccountIntegrationProvider`. Customer organisations never
collide because real ``Organization.id`` values are UUIDs.
"""


class IntegrationStatus(str, Enum):
    HEALTHY = "healthy"
    DEGRADED = "degraded"
    DISCONNECTED = "disconnected"
    UNKNOWN = "unknown"


@dataclass(frozen=True, slots=True)
class IntegrationRecord:
    """Read-side projection of a connected integration.

    Carries metadata only — the credential bytes never leave the
    resolver layer. ``credential_key`` is the resolver key the provider
    persisted under so callers can re-read it on demand without storing
    a duplicate.
    """

    org_id: str
    kind: IntegrationKind
    namespace: str
    credential_key: str
    status: IntegrationStatus = IntegrationStatus.UNKNOWN
    connected_at: datetime | None = None
    last_health_at: datetime | None = None
    error: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return {
            "org_id": self.org_id,
            "kind": self.kind.value,
            "namespace": self.namespace,
            "credential_key": self.credential_key,
            "status": self.status.value,
            "connected_at": self.connected_at.isoformat() if self.connected_at else None,
            "last_health_at": self.last_health_at.isoformat()
            if self.last_health_at
            else None,
            "error": self.error,
            "metadata": dict(self.metadata),
        }


@dataclass(frozen=True, slots=True)
class IntegrationHealth:
    """Outcome of a :meth:`AccountIntegrationProvider.health` call."""

    status: IntegrationStatus
    checked_at: datetime
    error: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)


class IntegrationProviderError(RuntimeError):
    """Provider-side failure surfaced as a typed error.

    ``code`` mirrors the broker error vocabulary
    (``invalid_payload``, ``upstream_unreachable``, ``unauthorized``,
    ``persist_failed``, ``provider_unavailable``).
    """

    def __init__(
        self,
        message: str,
        *,
        code: str = "provider_error",
        status_code: int | None = None,
    ) -> None:
        super().__init__(message)
        self.code = code
        self.status_code = status_code


class AccountIntegrationProvider(ABC):
    """ABC for per-org account-integration providers.

    Concrete providers self-register through
    :class:`AccountIntegrationService.add_provider`.
    """

    kind: IntegrationKind

    @abstractmethod
    async def connect(
        self,
        org_id: str,
        body: dict[str, Any],
        *,
        bearer: str | None = None,
    ) -> IntegrationRecord:
        """Validate the supplied credential, persist it, and return metadata.

        Implementations MUST:
        - call the upstream API once to validate the credential before
          persisting it (no "trust on first use");
        - persist via :class:`CredentialResolver` under
          :class:`CredentialKey` ``(service=self.kind, purpose=f"org:{org_id}")``;
        - return an :class:`IntegrationRecord` with ``status``
          ``HEALTHY`` and a populated ``namespace``.

        Implementations MUST NEVER:
        - return the raw PAT in the record's ``metadata`` or anywhere
          else in the response surface;
        - log the PAT;
        - persist the PAT outside the resolver chain.
        """

    @abstractmethod
    async def health(self, record: IntegrationRecord) -> IntegrationHealth:
        """Re-check the persisted credential."""

    @abstractmethod
    async def disconnect(
        self,
        record: IntegrationRecord,
        *,
        bearer: str | None = None,
    ) -> None:
        """Drop the local credential record. Vendor-side revocation is the
        operator's responsibility (documented in the runbook)."""

    @abstractmethod
    async def list_for_org(self, org_id: str) -> list[IntegrationRecord]:
        """Return every active integration this provider holds for ``org_id``."""

    # ------------------------------------------------------------------
    # Cloud-wizard extension surface (default raises NotImplementedError;
    # only the per-cloud subclasses override these methods).
    # ------------------------------------------------------------------

    async def bootstrap_artifacts(
        self,
        org_id: str,
        body: dict[str, Any],
    ) -> "BootstrapArtifacts":
        """Generate the IaC artifacts the customer must apply on their side.

        Returns the trust-policy JSON / federated-credential JSON / WIF
        pool config / scoped-token template the operator pastes into
        their cloud console (or applies via the supplied IaC link).
        NEVER mutates AlphaSwarm state and NEVER touches the customer's cloud.
        """
        raise NotImplementedError(
            f"{self.__class__.__name__} does not implement bootstrap_artifacts",
        )

    async def validate_identity(
        self,
        org_id: str,
        body: dict[str, Any],
    ) -> "IdentityProbe":
        """Read-only caller-identity probe.

        AWS: ``sts:GetCallerIdentity`` after ``sts:AssumeRole``.
        Azure: token acquisition for the federated credential.
        GCP: ``tokeninfo`` after impersonation.
        Cloudflare: ``GET /user/tokens/verify``.
        """
        raise NotImplementedError(
            f"{self.__class__.__name__} does not implement validate_identity",
        )

    async def validate_permissions(
        self,
        org_id: str,
        body: dict[str, Any],
    ) -> "PermissionPreview":
        """Dry-run permission preview against the candidate principal.

        AWS: ``iam:SimulatePrincipalPolicy``.
        Azure: list role assignments + effective permissions.
        GCP: ``testIamPermissions`` on the bound resource.
        Cloudflare: scope inspection on the verified token.
        """
        raise NotImplementedError(
            f"{self.__class__.__name__} does not implement validate_permissions",
        )

    async def enumerate_resources(
        self,
        org_id: str,
        body: dict[str, Any],
    ) -> "EnumerationResult":
        """List accounts / subscriptions / projects / zones / regions.

        Used to populate the wizard's downstream dropdowns. Read-only
        against the customer's cloud; no AlphaSwarm-side mutation.
        """
        raise NotImplementedError(
            f"{self.__class__.__name__} does not implement enumerate_resources",
        )


# ---------------------------------------------------------------------------
# Cloud-wizard return types
# ---------------------------------------------------------------------------


@dataclass(frozen=True, slots=True)
class BootstrapArtifacts:
    """Bootstrap step output — IaC templates the customer must apply.

    Carries:

    - ``external_id`` (AWS only) — HMAC-derived from the per-org secret
      so customers cannot enumerate other orgs' external IDs.
    - ``templates`` — name → JSON string the operator can paste into
      the cloud console.
    - ``cli_commands`` — name → shell command the operator can run
      (``az ad app federated-credential create``, ``gcloud iam
      workload-identity-pools providers create-oidc``, etc.).
    - ``iac_links`` — one-click CloudFormation StackSet URL or
      Terraform module GitHub link.
    - ``next_step`` — short human-readable instructions for what to do
      after applying the templates.
    """

    cloud_kind: IntegrationKind
    auth_method: str
    external_id: str | None
    templates: dict[str, str] = field(default_factory=dict)
    cli_commands: dict[str, str] = field(default_factory=dict)
    iac_links: dict[str, str] = field(default_factory=dict)
    metadata: dict[str, Any] = field(default_factory=dict)
    next_step: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "cloud_kind": self.cloud_kind.value,
            "auth_method": self.auth_method,
            "external_id": self.external_id,
            "templates": dict(self.templates),
            "cli_commands": dict(self.cli_commands),
            "iac_links": dict(self.iac_links),
            "metadata": dict(self.metadata),
            "next_step": self.next_step,
        }


@dataclass(frozen=True, slots=True)
class IdentityProbe:
    """Caller-identity probe outcome.

    ``ok`` is True only when the cloud confirmed the principal exists
    and AlphaSwarm could assume / federate to it. ``identity`` is a normalised
    descriptor (``caller_arn`` for AWS, ``subscription_id`` /
    ``tenant_id`` for Azure, ``project_id`` / ``service_account`` for
    GCP, ``token_actor`` for Cloudflare).
    """

    ok: bool
    identity: dict[str, Any] = field(default_factory=dict)
    error: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)
    checked_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    def to_dict(self) -> dict[str, Any]:
        return {
            "ok": self.ok,
            "identity": dict(self.identity),
            "error": self.error,
            "metadata": dict(self.metadata),
            "checked_at": self.checked_at.isoformat(),
        }


@dataclass(frozen=True, slots=True)
class PermissionPreview:
    """Dry-run permission-check outcome.

    ``allowed`` and ``denied`` carry the resolved permissions per the
    cloud's policy simulator. ``missing_required`` flags the
    subset of permissions the wizard expects but the principal lacks
    — surfaces a red badge in the UI so the operator knows exactly
    which trust-policy / role-assignment / IAM-binding change to make.
    """

    allowed: tuple[str, ...] = ()
    denied: tuple[str, ...] = ()
    missing_required: tuple[str, ...] = ()
    error: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)
    checked_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    @property
    def ok(self) -> bool:
        return self.error is None and not self.missing_required

    def to_dict(self) -> dict[str, Any]:
        return {
            "ok": self.ok,
            "allowed": list(self.allowed),
            "denied": list(self.denied),
            "missing_required": list(self.missing_required),
            "error": self.error,
            "metadata": dict(self.metadata),
            "checked_at": self.checked_at.isoformat(),
        }


@dataclass(frozen=True, slots=True)
class EnumerationResult:
    """Resource enumeration outcome — populates wizard dropdowns.

    Keys are cloud-specific but the wrapper shape is uniform:

    - AWS: ``{"accounts": [...], "regions": [...]}``
    - Azure: ``{"subscriptions": [...], "tenants": [...]}``
    - GCP: ``{"projects": [...], "billing_accounts": [...]}``
    - Cloudflare: ``{"accounts": [...], "zones": [...]}``
    """

    resources: dict[str, list[dict[str, Any]]] = field(default_factory=dict)
    error: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)
    checked_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    def to_dict(self) -> dict[str, Any]:
        return {
            "ok": self.error is None,
            "resources": {k: list(v) for k, v in self.resources.items()},
            "error": self.error,
            "metadata": dict(self.metadata),
            "checked_at": self.checked_at.isoformat(),
        }


def now() -> datetime:
    return datetime.now(timezone.utc)


__all__ = [
    "AccountIntegrationProvider",
    "BootstrapArtifacts",
    "EnumerationResult",
    "IdentityProbe",
    "IntegrationHealth",
    "IntegrationKind",
    "IntegrationProviderError",
    "IntegrationRecord",
    "IntegrationStatus",
    "PLATFORM_ORG_ID",
    "PermissionPreview",
    "now",
]
