"""Cloud infrastructure resource handler for CleverAgents.

Provides the :class:`CloudResourceHandler` for cloud infrastructure
resource types.  The handler supports a hierarchical type model where
provider-specific types (``aws-account``, ``aws-vpc``, etc.) inherit
from generic ``cloud-*`` base types.

This handler validates configuration and resolves credentials from
environment variables and profile names but does **not** execute any
cloud SDK operations -- actual execution raises
:exc:`NotImplementedError`.

Cloud resource types are registered as built-in types at bootstrap and
use ``sandbox_strategy = "none"`` because cloud sandbox isolation is
not yet implemented.

## Provider Detection

The handler extracts the cloud provider from the resource type name:

- ``aws-*`` or ``aws`` → AWS provider
- ``gcp-*`` or ``gcp`` → GCP provider
- ``azure-*`` or ``azure`` → Azure provider
- ``cloud-*`` → generic (no provider-specific validation)

## Credential Resolution

Credentials are resolved in this priority order:

1. Explicit configuration values (passed via ``properties``)
2. Environment variables
3. Profile names (AWS only -- ``AWS_PROFILE``)

No cloud SDK dependencies are required.  Credential values are never
logged; the existing :mod:`cleveragents.shared.redaction` patterns
handle masking.

Based on:
    - Issue #343: Cloud Infrastructure Resources
    - implementation_plan.md group M7.post-resource-cloud
"""

from __future__ import annotations

import hashlib
import logging
import os
from dataclasses import dataclass, field
from typing import Any

from cleveragents.domain.models.core.resource import Resource
from cleveragents.infrastructure.sandbox.manager import SandboxManager
from cleveragents.resource.handlers._base import EMPTY_CONTENT_HASH
from cleveragents.resource.handlers.protocol import (
    Content,
    DeleteResult,
    DiffResult,
    WriteResult,
)
from cleveragents.shared.redaction import REDACTED, is_sensitive_key
from cleveragents.tool.context import BoundResource

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Cloud credential field definitions
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class _CredentialField:
    """Metadata for a single credential or configuration field."""

    name: str
    env_var: str
    required: bool = True
    sensitive: bool = False
    description: str = ""


@dataclass(frozen=True)
class CloudProviderSpec:
    """Specification of a cloud provider's credential and config fields."""

    provider: str
    description: str
    credential_fields: tuple[_CredentialField, ...]
    metadata_fields: tuple[_CredentialField, ...] = ()
    profile_env_var: str | None = None
    required_fields: frozenset[str] = field(default_factory=frozenset)

    @property
    def all_fields(self) -> tuple[_CredentialField, ...]:
        """Return all fields (credential + metadata)."""
        return self.credential_fields + self.metadata_fields


# ---------------------------------------------------------------------------
# Provider specifications
# ---------------------------------------------------------------------------


AWS_SPEC = CloudProviderSpec(
    provider="aws",
    description="Amazon Web Services cloud resource.",
    credential_fields=(
        _CredentialField(
            name="access-key-id",
            env_var="AWS_ACCESS_KEY_ID",
            required=True,
            sensitive=True,
            description="AWS access key ID.",
        ),
        _CredentialField(
            name="secret-access-key",
            env_var="AWS_SECRET_ACCESS_KEY",
            required=True,
            sensitive=True,
            description="AWS secret access key.",
        ),
        _CredentialField(
            name="session-token",
            env_var="AWS_SESSION_TOKEN",
            required=False,
            sensitive=True,
            description="AWS session token (optional, for temporary credentials).",
        ),
    ),
    metadata_fields=(
        _CredentialField(
            name="region",
            env_var="AWS_REGION",
            required=False,
            sensitive=False,
            description="AWS region (e.g. us-east-1).",
        ),
        _CredentialField(
            name="profile",
            env_var="AWS_PROFILE",
            required=False,
            sensitive=False,
            description="AWS profile name from ~/.aws/credentials.",
        ),
    ),
    profile_env_var="AWS_PROFILE",
    required_fields=frozenset({"access-key-id", "secret-access-key"}),
)

GCP_SPEC = CloudProviderSpec(
    provider="gcp",
    description="Google Cloud Platform resource.",
    credential_fields=(
        _CredentialField(
            name="service-account-json-path",
            env_var="GOOGLE_APPLICATION_CREDENTIALS",
            required=True,
            sensitive=True,
            description="Path to GCP service account JSON key file.",
        ),
    ),
    metadata_fields=(
        _CredentialField(
            name="project-id",
            env_var="GCLOUD_PROJECT",
            required=True,
            sensitive=False,
            description="GCP project ID.",
        ),
        _CredentialField(
            name="region",
            env_var="GCP_REGION",
            required=False,
            sensitive=False,
            description="GCP region (e.g. us-central1).",
        ),
    ),
    required_fields=frozenset({"service-account-json-path", "project-id"}),
)

AZURE_SPEC = CloudProviderSpec(
    provider="azure",
    description="Microsoft Azure cloud resource.",
    credential_fields=(
        _CredentialField(
            name="subscription-id",
            env_var="AZURE_SUBSCRIPTION_ID",
            required=True,
            sensitive=True,
            description="Azure subscription ID.",
        ),
        _CredentialField(
            name="tenant-id",
            env_var="AZURE_TENANT_ID",
            required=True,
            sensitive=True,
            description="Azure Active Directory tenant ID.",
        ),
        _CredentialField(
            name="client-id",
            env_var="AZURE_CLIENT_ID",
            required=True,
            sensitive=True,
            description="Azure service principal client ID.",
        ),
        _CredentialField(
            name="client-secret",
            env_var="AZURE_CLIENT_SECRET",
            required=True,
            sensitive=True,
            description="Azure service principal client secret.",
        ),
    ),
    metadata_fields=(
        _CredentialField(
            name="region",
            env_var="AZURE_REGION",
            required=False,
            sensitive=False,
            description="Azure region (e.g. eastus).",
        ),
    ),
    required_fields=frozenset(
        {"subscription-id", "tenant-id", "client-id", "client-secret"}
    ),
)


#: Mapping from provider name to its specification.
CLOUD_PROVIDERS: dict[str, CloudProviderSpec] = {
    "aws": AWS_SPEC,
    "gcp": GCP_SPEC,
    "azure": AZURE_SPEC,
}

#: Provider prefixes for hierarchical type name detection.
_PROVIDER_PREFIXES: tuple[str, ...] = ("aws-", "gcp-", "azure-")


def extract_provider(type_name: str) -> str | None:
    """Extract the cloud provider key from a resource type name.

    Supports both flat names (``aws``, ``gcp``, ``azure``) and
    hierarchical names (``aws-account``, ``aws-vpc``, etc.).
    Returns ``None`` for generic ``cloud-*`` base types.

    Args:
        type_name: Resource type name.

    Returns:
        Provider key (``aws``, ``gcp``, ``azure``) or ``None``.
    """
    # Direct match (flat provider names)
    if type_name in CLOUD_PROVIDERS:
        return type_name
    # Hierarchical names: aws-*, gcp-*, azure-*
    for prefix in _PROVIDER_PREFIXES:
        if type_name.startswith(prefix):
            return prefix.rstrip("-")
    # Generic cloud-* base types have no specific provider
    return None


# ---------------------------------------------------------------------------
# Credential resolution
# ---------------------------------------------------------------------------


def _safe_value_repr(field_def: _CredentialField, value: str | None) -> str:
    """Return a safe representation of a credential value for logging."""
    if value is None:
        return "<not set>"
    if field_def.sensitive or is_sensitive_key(field_def.name):
        return REDACTED
    return value


def resolve_credentials(
    provider: str,
    properties: dict[str, Any],
) -> dict[str, str | None]:
    """Resolve credentials for a cloud provider from properties and env vars.

    Resolution priority:
    1. Explicit values in ``properties``
    2. Environment variables

    Args:
        provider: Cloud provider name (``aws``, ``gcp``, ``azure``).
        properties: Resource properties dict with explicit values.

    Returns:
        Dict mapping field names to resolved values (``None`` if unresolved).

    Raises:
        ValueError: If the provider name is not recognised.
    """
    spec = CLOUD_PROVIDERS.get(provider)
    if spec is None:
        raise ValueError(
            f"Unknown cloud provider '{provider}'. "
            f"Supported providers: {', '.join(sorted(CLOUD_PROVIDERS))}."
        )

    resolved: dict[str, str | None] = {}
    for field_def in spec.all_fields:
        # Priority 1: explicit property
        explicit = properties.get(field_def.name)
        if explicit is not None and str(explicit).strip():
            resolved[field_def.name] = str(explicit)
            continue

        # Priority 2: environment variable
        env_val = os.environ.get(field_def.env_var)
        if env_val is not None and env_val.strip():
            resolved[field_def.name] = env_val
            continue

        resolved[field_def.name] = None

    return resolved


def validate_credentials(
    provider: str,
    resolved: dict[str, str | None],
) -> list[str]:
    """Validate that all required credentials are present.

    Args:
        provider: Cloud provider name.
        resolved: Dict from :func:`resolve_credentials`.

    Returns:
        List of error messages (empty if valid).
    """
    spec = CLOUD_PROVIDERS.get(provider)
    if spec is None:
        return [
            f"Unknown cloud provider '{provider}'. "
            f"Supported providers: {', '.join(sorted(CLOUD_PROVIDERS))}."
        ]

    # Check if a profile is set (AWS only) -- profile can satisfy
    # required credential fields
    has_profile = False
    if spec.profile_env_var is not None:
        profile_val = resolved.get("profile")
        if profile_val is not None and profile_val.strip():
            has_profile = True

    errors: list[str] = []
    for field_def in spec.all_fields:
        if not field_def.required:
            continue
        value = resolved.get(field_def.name)
        # If a profile is configured, skip credential-field checks
        # (the SDK will resolve them from the profile).
        if has_profile and field_def in spec.credential_fields:
            continue
        if value is None:
            safe_repr = _safe_value_repr(field_def, value)
            errors.append(
                f"Required field '{field_def.name}' for provider "
                f"'{provider}' is missing (current value: {safe_repr}). "
                f"Set it via resource properties or the "
                f"{field_def.env_var} environment variable."
            )

    return errors


# ---------------------------------------------------------------------------
# CloudResourceHandler
# ---------------------------------------------------------------------------


class CloudResourceHandler:
    """Handler for cloud infrastructure resource types.

    Validates cloud resource configuration and resolves credentials
    from environment variables and profile names.  Actual cloud
    operations are **not** implemented -- calling :meth:`resolve`
    raises :exc:`NotImplementedError` after validation.

    Supports hierarchical type names (``aws-account``, ``aws-vpc``,
    etc.) in addition to flat provider names (``aws``, ``gcp``,
    ``azure``).  Generic ``cloud-*`` base types skip provider-specific
    credential validation.

    This handler satisfies the
    :class:`~cleveragents.resource.handlers.protocol.ResourceHandler`
    protocol.
    """

    def resolve(
        self,
        *,
        resource: Resource,
        plan_id: str,
        slot_name: str,
        sandbox_manager: SandboxManager,
        access: str = "read_only",
    ) -> BoundResource:
        """Validate cloud resource configuration and raise.

        Performs full credential validation and resolution, then
        raises :exc:`NotImplementedError` because cloud sandbox
        execution is not yet implemented.

        Args:
            resource: A cloud resource instance.
            plan_id: The plan requesting the sandbox.
            slot_name: Name of the tool resource slot being filled.
            sandbox_manager: The sandbox lifecycle manager (unused).
            access: Access mode (unused).

        Raises:
            ValueError: If required credentials are missing or the
                resource type is not a supported cloud provider.
            NotImplementedError: Always, after successful validation.
        """
        type_name = resource.resource_type_name
        provider = extract_provider(type_name)

        if provider is None:
            # Generic cloud-* base type — no provider-specific validation.
            raise NotImplementedError(
                f"Cloud resource execution for generic type '{type_name}' "
                f"is not yet implemented. Resource '{resource.resource_id}' "
                f"(plan={plan_id}, slot={slot_name}). "
                f"Generic cloud base types cannot be resolved directly."
            )

        if provider not in CLOUD_PROVIDERS:
            raise ValueError(
                f"Resource type '{type_name}' maps to unknown provider "
                f"'{provider}'. "
                f"Supported: {', '.join(sorted(CLOUD_PROVIDERS))}."
            )

        # Resolve credentials
        resolved = resolve_credentials(provider, dict(resource.properties))

        # Log resolution (with redaction)
        for field_def in CLOUD_PROVIDERS[provider].all_fields:
            safe = _safe_value_repr(field_def, resolved.get(field_def.name))
            logger.debug(
                "Cloud credential %s/%s = %s",
                provider,
                field_def.name,
                safe,
            )

        # Validate credentials (only for account-level types that carry creds)
        is_account_type = type_name in (
            "aws",
            "gcp",
            "azure",
            "aws-account",
        )
        if is_account_type:
            errors = validate_credentials(provider, resolved)
            if errors:
                raise ValueError(
                    f"Cloud resource validation failed for '{type_name}' "
                    f"(provider={provider}): " + "; ".join(errors)
                )

        raise NotImplementedError(
            f"Cloud resource execution for '{type_name}' "
            f"(provider={provider}) is not yet implemented. "
            f"Resource '{resource.resource_id}' "
            f"(plan={plan_id}, slot={slot_name}) validated successfully "
            f"but sandbox provisioning for cloud resources is pending."
        )

    # -- CRUD stubs (required by ResourceHandler protocol) -----------------

    def read(self, *, resource: Resource, path: str = "") -> Content:
        """Not supported by the cloud handler."""
        raise NotImplementedError("Cloud handler does not support read()")

    def write(self, *, resource: Resource, path: str, data: bytes) -> WriteResult:
        """Not supported by the cloud handler."""
        raise NotImplementedError("Cloud handler does not support write()")

    def delete(self, *, resource: Resource, path: str = "") -> DeleteResult:
        """Not supported by the cloud handler."""
        raise NotImplementedError("Cloud handler does not support delete()")

    def list_children(self, *, resource: Resource) -> list[str]:
        """Not supported by the cloud handler."""
        raise NotImplementedError("Cloud handler does not support list_children()")

    def diff(self, *, resource: Resource, other_location: str) -> DiffResult:
        """Not supported by the cloud handler."""
        raise NotImplementedError("Cloud handler does not support diff()")

    def discover_children(self, *, resource: Resource) -> list[Resource]:
        """Not supported for cloud resources."""
        raise NotImplementedError("Cloud handler does not support discover_children()")

    # -- Lifecycle stubs (issue #836) --------------------------------------

    def create_sandbox(
        self,
        *,
        resource: Resource,
        plan_id: str,
        sandbox_manager: Any,
    ) -> Any:
        """Not supported for cloud resources."""
        raise NotImplementedError("Cloud handler does not support create_sandbox()")

    def create_checkpoint(
        self,
        *,
        resource: Resource,
        plan_id: str,
        sandbox_manager: Any,
        phase: str = "execution",
    ) -> Any:
        """Not supported for cloud resources."""
        raise NotImplementedError("Cloud handler does not support create_checkpoint()")

    def rollback_to(
        self,
        *,
        resource: Resource,
        plan_id: str,
        checkpoint_id: str,
        sandbox_manager: Any,
    ) -> Any:
        """Not supported for cloud resources."""
        raise NotImplementedError("Cloud handler does not support rollback_to()")

    def project_access(
        self,
        *,
        resource: Resource,
        principal: str,
        action: str = "read",
        project_id: str = "",
    ) -> Any:
        """Not supported for cloud resources."""
        raise NotImplementedError("Cloud handler does not support project_access()")

    def content_hash(
        self,
        resource: Resource,
        *,
        algorithm: str = "sha256",
    ) -> str:
        """Compute content hash for cloud resources.

        Cloud resources do not have local content.  Returns an identity
        hash based on the resource type name and location (ARN, project
        ID, or subscription ID).

        Returns :data:`EMPTY_CONTENT_HASH` if no location is set.
        """
        if not resource.location:
            return EMPTY_CONTENT_HASH

        h = hashlib.new(algorithm)
        h.update(resource.resource_type_name.encode("utf-8"))
        h.update(b"\0")
        h.update(resource.location.encode("utf-8"))
        return h.hexdigest()


# ---------------------------------------------------------------------------
# Stubbed sandbox strategies
# ---------------------------------------------------------------------------


class CloudSandboxStrategy:
    """Stubbed sandbox strategy for cloud resources.

    Validates configuration but raises :exc:`NotImplementedError` for
    all lifecycle operations (create, commit, rollback).  This is the
    expected behaviour per the issue specification.
    """

    def __init__(self, provider: str) -> None:
        self._provider = provider

    def validate(self, properties: dict[str, Any]) -> list[str]:
        """Validate cloud resource configuration.

        Args:
            properties: Resource properties dict.

        Returns:
            List of validation error messages (empty if valid).
        """
        resolved = resolve_credentials(self._provider, properties)
        return validate_credentials(self._provider, resolved)

    def create(self, resource_id: str, plan_id: str) -> None:
        """Create a cloud sandbox (not implemented).

        Raises:
            NotImplementedError: Always.
        """
        raise NotImplementedError(
            f"Cloud sandbox creation for provider '{self._provider}' "
            f"is not yet implemented (resource={resource_id}, plan={plan_id})."
        )

    def commit(self, resource_id: str, plan_id: str) -> None:
        """Commit a cloud sandbox (not implemented).

        Raises:
            NotImplementedError: Always.
        """
        raise NotImplementedError(
            f"Cloud sandbox commit for provider '{self._provider}' "
            f"is not yet implemented (resource={resource_id}, plan={plan_id})."
        )

    def rollback(self, resource_id: str, plan_id: str) -> None:
        """Rollback a cloud sandbox (not implemented).

        Raises:
            NotImplementedError: Always.
        """
        raise NotImplementedError(
            f"Cloud sandbox rollback for provider '{self._provider}' "
            f"is not yet implemented (resource={resource_id}, plan={plan_id})."
        )
