# Copyright 2026 Guildhouse Dev
# SPDX-License-Identifier: Apache-2.0

"""Entra credential backend — resolves credentials via Microsoft Entra ID.

For OAuth:
  Uses MSAL on-behalf-of (OBO) flow to exchange the operator's Entra
  identity (from the AC's identity_proof) for a scoped, short-lived
  token targeting a specific resource.  The broker never sees or stores
  the operator's password — only the OBO assertion.

For Kerberos:
  Uses Entra cloud Kerberos trust or on-behalf-of flow to an on-prem
  KDC proxy.  The actual Kerberos ticket bytes are acquired from the
  KDC and returned to the transport.  Stubbed in this sprint — hybrid
  environments need site-specific KDC configuration.

For Bascule:
  The AC is the credential.  Bascule validates ACs natively, so no
  external secrets source is involved.  The backend simply wraps the
  AC dict in a ``BasculeCredential``.

Rust port note:
  MSAL has no Rust equivalent.  The Rust port should use ``reqwest``
  against the Entra OAuth2 endpoints directly (the OBO flow is a
  single POST to the token endpoint).
"""

import logging
from datetime import datetime, timedelta, UTC

from gsap_broker.credentials.resolver import (
    BasculeCredential,
    Credential,
    CredentialBackend,
    CredentialResolutionError,
    KerberosCredential,
    OAuthCredential,
)

logger = logging.getLogger(__name__)


class EntraCredentialBackend(CredentialBackend):
    """Resolves credentials via Microsoft Entra ID."""

    def __init__(
        self,
        tenant_id: str,
        client_id: str,
        client_secret: str,
    ):
        self.tenant_id = tenant_id
        self.client_id = client_id
        self.client_secret = client_secret

    @property
    def supported_types(self) -> list[str]:
        return ["bascule_ac", "oauth", "kerberos"]

    async def resolve(
        self,
        credential_type: str,
        target: str,
        ac_context: dict,
    ) -> Credential:
        if credential_type == "bascule_ac":
            return self._resolve_bascule(target, ac_context)

        if credential_type == "oauth":
            return await self._resolve_oauth(target, ac_context)

        if credential_type == "kerberos":
            return await self._resolve_kerberos(target, ac_context)

        raise CredentialResolutionError(
            f"EntraCredentialBackend does not support '{credential_type}'"
        )

    async def revoke(self, credential: Credential) -> None:
        # OAuth tokens issued via OBO are short-lived (5 min) and
        # cannot be individually revoked via Entra.  Kerberos tickets
        # expire naturally.  Bascule ACs are revoked via the broker's
        # AC lifecycle, not the credential backend.
        logger.debug(
            "Revoke requested for %s (no-op — TTL is primary revocation)",
            credential.credential_type,
        )

    # ── Private ──────────────────────────────────────────────────

    def _resolve_bascule(self, target: str, ac_context: dict) -> BasculeCredential:
        """Bascule accepts ACs directly — no secret needed."""
        expires_raw = ac_context.get("expires_at")
        if isinstance(expires_raw, str):
            expires_at = datetime.fromisoformat(expires_raw)
        elif isinstance(expires_raw, datetime):
            expires_at = expires_raw
        else:
            expires_at = datetime.now(UTC) + timedelta(minutes=5)

        return BasculeCredential(
            target=target,
            expires_at=expires_at,
            scoped_to=ac_context.get("accord", {}).get("template", ""),
            authorization_context=ac_context,
        )

    async def _resolve_oauth(self, target: str, ac_context: dict) -> OAuthCredential:
        """On-behalf-of flow: exchange operator identity for scoped token.

        In production this calls MSAL's ``acquire_token_on_behalf_of``
        using the operator's assertion (id_token or access_token from
        the identity_proof in the AC).  For this sprint we use the
        app-level client_credentials flow as a stand-in, since the OBO
        flow requires the operator's token to be available at invocation
        time (which means the transport layer needs to forward it —
        wired in a future sprint).
        """
        try:
            import msal
        except ImportError:
            raise CredentialResolutionError(
                "msal package required for Entra OAuth resolution"
            )

        app = msal.ConfidentialClientApplication(
            client_id=self.client_id,
            client_credential=self.client_secret,
            authority=f"https://login.microsoftonline.com/{self.tenant_id}",
        )
        result = app.acquire_token_for_client(
            scopes=["https://graph.microsoft.com/.default"]
        )
        if "access_token" not in result:
            raise CredentialResolutionError(
                f"Entra token acquisition failed: "
                f"{result.get('error_description', result.get('error', 'unknown'))}"
            )

        return OAuthCredential(
            target=target,
            expires_at=datetime.now(UTC) + timedelta(minutes=5),
            scoped_to=target,
            access_token=result["access_token"],
        )

    async def _resolve_kerberos(self, target: str, ac_context: dict) -> KerberosCredential:
        """Acquire Kerberos ticket via Entra cloud trust.

        Fix H-3: raises instead of returning stub ticket. Actual
        implementation depends on the hybrid environment:
          - Pure Entra: use Entra Kerberos proxy (preview API)
          - Hybrid with on-prem AD: use OBO to get a token, then
            exchange for a Kerberos ticket via the KDC proxy
          - Direct KDC: use kinit with the OBO token

        Implementation deferred to hybrid environment sprint.
        """
        raise CredentialResolutionError(
            "Kerberos credential resolution not yet implemented. "
            "Configure an on-premises KDC or use OAuth credentials."
        )
