"""
Attachment Registry - Manages policy attachments from YAML config.

Attachments define WHERE policies apply, separate from the policy definitions.
This allows the same policy to be attached to multiple scopes.
"""

from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Dict, List, Optional

from litellm._logging import verbose_proxy_logger
from litellm.repositories.table_repositories import PolicyAttachmentRepository
from litellm.types.proxy.policy_engine import (
    PolicyAttachment,
    PolicyAttachmentCreateRequest,
    PolicyAttachmentDBResponse,
    PolicyMatchContext,
)

if TYPE_CHECKING:
    from litellm.proxy.utils import PrismaClient


class AttachmentRegistry:
    """
    In-memory registry for storing and managing policy attachments.

    Attachments define the relationship between policies and their scopes.
    A single policy can have multiple attachments (applied to different scopes).

    Example YAML:
    ```yaml
    attachments:
      - policy: global-baseline
        scope: "*"
      - policy: healthcare-compliance
        teams: [healthcare-team]
      - policy: dev-safety
        keys: ["dev-key-*"]
    ```
    """

    def __init__(self):
        self._attachments: List[PolicyAttachment] = []
        self._initialized: bool = False

    def load_attachments(self, attachments_config: List[Dict[str, Any]]) -> None:
        """
        Load attachments from a configuration list.

        Args:
            attachments_config: List of attachment dictionaries from YAML.
        """
        self._attachments = []

        for attachment_data in attachments_config:
            try:
                attachment = self._parse_attachment(attachment_data)
                self._attachments.append(attachment)
                verbose_proxy_logger.debug(
                    f"Loaded attachment for policy: {attachment.policy}"
                )
            except Exception as e:
                verbose_proxy_logger.error(f"Error loading attachment: {str(e)}")
                raise ValueError(f"Invalid attachment: {str(e)}") from e

        self._initialized = True
        verbose_proxy_logger.info(f"Loaded {len(self._attachments)} policy attachments")

    def _parse_attachment(self, attachment_data: Dict[str, Any]) -> PolicyAttachment:
        """
        Parse an attachment from raw configuration data.

        Args:
            attachment_data: Raw attachment configuration

        Returns:
            Parsed PolicyAttachment object
        """
        return PolicyAttachment(
            policy=attachment_data.get("policy", ""),
            scope=attachment_data.get("scope"),
            teams=attachment_data.get("teams"),
            keys=attachment_data.get("keys"),
            models=attachment_data.get("models"),
            tags=attachment_data.get("tags"),
        )

    def get_attached_policies(self, context: PolicyMatchContext) -> List[str]:
        """
        Get list of policy names attached to the given context.

        Args:
            context: The request context to match against

        Returns:
            List of policy names that are attached to matching scopes
        """
        return [
            r["policy_name"] for r in self.get_attached_policies_with_reasons(context)
        ]

    def get_attached_policies_with_reasons(
        self, context: PolicyMatchContext
    ) -> List[Dict[str, Any]]:
        """
        Get list of policy names and match reasons for the given context.

        Returns a list of dicts with 'policy_name' and 'matched_via' keys.
        The 'matched_via' describes which dimension caused the match.
        """
        from litellm.proxy.policy_engine.policy_matcher import PolicyMatcher

        results: List[Dict[str, Any]] = []
        seen_policies: set = set()

        for attachment in self._attachments:
            scope = attachment.to_policy_scope()
            if PolicyMatcher.scope_matches(scope=scope, context=context):
                if attachment.policy not in seen_policies:
                    seen_policies.add(attachment.policy)
                    matched_via = self._describe_match_reason(attachment, context)
                    results.append(
                        {
                            "policy_name": attachment.policy,
                            "matched_via": matched_via,
                        }
                    )
                    verbose_proxy_logger.debug(
                        f"Attachment matched: policy={attachment.policy}, "
                        f"matched_via={matched_via}, "
                        f"context=(team={context.team_alias}, key={context.key_alias}, model={context.model})"
                    )

        return results

    @staticmethod
    def _describe_match_reason(
        attachment: PolicyAttachment, context: PolicyMatchContext
    ) -> str:
        """Describe why an attachment matched the context."""
        from litellm.proxy.policy_engine.policy_matcher import PolicyMatcher

        if attachment.is_global():
            return "scope:*"

        reasons = []
        if attachment.tags and context.tags:
            matching_tags = [
                t
                for t in context.tags
                if PolicyMatcher.matches_pattern(t, attachment.tags)
            ]
            if matching_tags:
                reasons.append(f"tag:{matching_tags[0]}")
        if attachment.teams and context.team_alias:
            reasons.append(f"team:{context.team_alias}")
        if attachment.keys and context.key_alias:
            reasons.append(f"key:{context.key_alias}")
        if attachment.models and context.model:
            reasons.append(f"model:{context.model}")

        return "+".join(reasons) if reasons else "scope:default"

    def is_policy_attached(self, policy_name: str, context: PolicyMatchContext) -> bool:
        """
        Check if a specific policy is attached to the given context.

        Args:
            policy_name: Name of the policy to check
            context: The request context to match against

        Returns:
            True if the policy is attached to a matching scope
        """
        attached = self.get_attached_policies(context)
        return policy_name in attached

    def get_all_attachments(self) -> List[PolicyAttachment]:
        """
        Get all loaded attachments.

        Returns:
            List of all PolicyAttachment objects
        """
        return self._attachments.copy()

    def get_attachments_for_policy(self, policy_name: str) -> List[PolicyAttachment]:
        """
        Get all attachments for a specific policy.

        Args:
            policy_name: Name of the policy

        Returns:
            List of attachments for the policy
        """
        return [a for a in self._attachments if a.policy == policy_name]

    def is_initialized(self) -> bool:
        """
        Check if the registry has been initialized with attachments.

        Returns:
            True if attachments have been loaded, False otherwise
        """
        return self._initialized

    def clear(self) -> None:
        """
        Clear all attachments from the registry.
        """
        self._attachments = []
        self._initialized = False

    def add_attachment(self, attachment: PolicyAttachment) -> None:
        """
        Add a single attachment.

        Args:
            attachment: PolicyAttachment object to add
        """
        self._attachments.append(attachment)
        self._initialized = True
        verbose_proxy_logger.debug(f"Added attachment for policy: {attachment.policy}")

    def remove_attachments_for_policy(self, policy_name: str) -> int:
        """
        Remove all attachments for a specific policy.

        Args:
            policy_name: Name of the policy

        Returns:
            Number of attachments removed
        """
        original_count = len(self._attachments)
        self._attachments = [a for a in self._attachments if a.policy != policy_name]
        removed_count = original_count - len(self._attachments)
        if removed_count > 0:
            verbose_proxy_logger.debug(
                f"Removed {removed_count} attachment(s) for policy: {policy_name}"
            )
        return removed_count

    def remove_attachment_by_id(self, attachment_id: str) -> bool:
        """
        Remove an attachment by its ID (for DB-synced attachments).

        Args:
            attachment_id: The ID of the attachment to remove

        Returns:
            True if removed, False if not found
        """
        # Note: In-memory attachments don't have IDs, so this is primarily
        # for consistency after DB operations
        return False

    # ─────────────────────────────────────────────────────────────────────────
    # Database CRUD Methods
    # ─────────────────────────────────────────────────────────────────────────

    async def add_attachment_to_db(
        self,
        attachment_request: PolicyAttachmentCreateRequest,
        prisma_client: "PrismaClient",
        created_by: Optional[str] = None,
    ) -> PolicyAttachmentDBResponse:
        """
        Add a policy attachment to the database.

        Args:
            attachment_request: The attachment creation request
            prisma_client: The Prisma client instance
            created_by: User who created the attachment

        Returns:
            PolicyAttachmentDBResponse with the created attachment
        """
        try:
            created_attachment = await PolicyAttachmentRepository(
                prisma_client
            ).table.create(
                data={
                    "policy_name": attachment_request.policy_name,
                    "scope": attachment_request.scope,
                    "teams": attachment_request.teams or [],
                    "keys": attachment_request.keys or [],
                    "models": attachment_request.models or [],
                    "tags": attachment_request.tags or [],
                    "created_at": datetime.now(timezone.utc),
                    "updated_at": datetime.now(timezone.utc),
                    "created_by": created_by,
                    "updated_by": created_by,
                }
            )

            # Also add to in-memory registry
            attachment = PolicyAttachment(
                policy=attachment_request.policy_name,
                scope=attachment_request.scope,
                teams=attachment_request.teams,
                keys=attachment_request.keys,
                models=attachment_request.models,
                tags=attachment_request.tags,
            )
            self.add_attachment(attachment)

            return PolicyAttachmentDBResponse(
                attachment_id=created_attachment.attachment_id,
                policy_name=created_attachment.policy_name,
                scope=created_attachment.scope,
                teams=created_attachment.teams or [],
                keys=created_attachment.keys or [],
                models=created_attachment.models or [],
                tags=created_attachment.tags or [],
                created_at=created_attachment.created_at,
                updated_at=created_attachment.updated_at,
                created_by=created_attachment.created_by,
                updated_by=created_attachment.updated_by,
            )
        except Exception as e:
            verbose_proxy_logger.exception(f"Error adding attachment to DB: {e}")
            raise Exception(f"Error adding attachment to DB: {str(e)}")

    async def delete_attachment_from_db(
        self,
        attachment_id: str,
        prisma_client: "PrismaClient",
    ) -> Dict[str, str]:
        """
        Delete a policy attachment from the database.

        Args:
            attachment_id: The ID of the attachment to delete
            prisma_client: The Prisma client instance

        Returns:
            Dict with success message
        """
        try:
            # Get attachment before deleting
            attachment = await PolicyAttachmentRepository(
                prisma_client
            ).table.find_unique(where={"attachment_id": attachment_id})

            if attachment is None:
                raise Exception(f"Attachment with ID {attachment_id} not found")

            # Delete from DB
            await PolicyAttachmentRepository(prisma_client).table.delete(
                where={"attachment_id": attachment_id}
            )

            # Note: In-memory attachments don't have IDs, so we need to sync from DB
            # to properly update in-memory state
            await self.sync_attachments_from_db(prisma_client)

            return {"message": f"Attachment {attachment_id} deleted successfully"}
        except Exception as e:
            verbose_proxy_logger.exception(f"Error deleting attachment from DB: {e}")
            raise Exception(f"Error deleting attachment from DB: {str(e)}")

    async def get_attachment_by_id_from_db(
        self,
        attachment_id: str,
        prisma_client: "PrismaClient",
    ) -> Optional[PolicyAttachmentDBResponse]:
        """
        Get a policy attachment by ID from the database.

        Args:
            attachment_id: The ID of the attachment to retrieve
            prisma_client: The Prisma client instance

        Returns:
            PolicyAttachmentDBResponse if found, None otherwise
        """
        try:
            attachment = await PolicyAttachmentRepository(
                prisma_client
            ).table.find_unique(where={"attachment_id": attachment_id})

            if attachment is None:
                return None

            return PolicyAttachmentDBResponse(
                attachment_id=attachment.attachment_id,
                policy_name=attachment.policy_name,
                scope=attachment.scope,
                teams=attachment.teams or [],
                keys=attachment.keys or [],
                models=attachment.models or [],
                tags=attachment.tags or [],
                created_at=attachment.created_at,
                updated_at=attachment.updated_at,
                created_by=attachment.created_by,
                updated_by=attachment.updated_by,
            )
        except Exception as e:
            verbose_proxy_logger.exception(f"Error getting attachment from DB: {e}")
            raise Exception(f"Error getting attachment from DB: {str(e)}")

    async def get_all_attachments_from_db(
        self,
        prisma_client: "PrismaClient",
    ) -> List[PolicyAttachmentDBResponse]:
        """
        Get all policy attachments from the database.

        Args:
            prisma_client: The Prisma client instance

        Returns:
            List of PolicyAttachmentDBResponse objects
        """
        try:
            attachments = await PolicyAttachmentRepository(
                prisma_client
            ).table.find_many(
                order={"created_at": "desc"},
            )

            return [
                PolicyAttachmentDBResponse(
                    attachment_id=a.attachment_id,
                    policy_name=a.policy_name,
                    scope=a.scope,
                    teams=a.teams or [],
                    keys=a.keys or [],
                    models=a.models or [],
                    tags=a.tags or [],
                    created_at=a.created_at,
                    updated_at=a.updated_at,
                    created_by=a.created_by,
                    updated_by=a.updated_by,
                )
                for a in attachments
            ]
        except Exception as e:
            verbose_proxy_logger.exception(f"Error getting attachments from DB: {e}")
            raise Exception(f"Error getting attachments from DB: {str(e)}")

    async def sync_attachments_from_db(
        self,
        prisma_client: "PrismaClient",
    ) -> None:
        """
        Sync policy attachments from the database to in-memory registry.

        Args:
            prisma_client: The Prisma client instance
        """
        try:
            attachments = await self.get_all_attachments_from_db(prisma_client)

            # Clear existing attachments and reload from DB
            self._attachments = []

            for attachment_response in attachments:
                attachment = PolicyAttachment(
                    policy=attachment_response.policy_name,
                    scope=attachment_response.scope,
                    teams=(
                        attachment_response.teams if attachment_response.teams else None
                    ),
                    keys=attachment_response.keys if attachment_response.keys else None,
                    models=(
                        attachment_response.models
                        if attachment_response.models
                        else None
                    ),
                    tags=attachment_response.tags if attachment_response.tags else None,
                )
                self._attachments.append(attachment)

            self._initialized = True
            verbose_proxy_logger.info(
                f"Synced {len(attachments)} attachments from DB to in-memory registry"
            )
        except Exception as e:
            verbose_proxy_logger.exception(f"Error syncing attachments from DB: {e}")
            raise Exception(f"Error syncing attachments from DB: {str(e)}")


# Global singleton instance
_attachment_registry: Optional[AttachmentRegistry] = None


def get_attachment_registry() -> AttachmentRegistry:
    """
    Get the global AttachmentRegistry singleton.

    Returns:
        The global AttachmentRegistry instance
    """
    global _attachment_registry
    if _attachment_registry is None:
        _attachment_registry = AttachmentRegistry()
    return _attachment_registry
