"""Context Provider - Security Boundary Layer.

The Context Provider is the CRITICAL SECURITY BOUNDARY between agents
(untrusted business logic) and the blackboard store (infrastructure).

SECURITY FIX (2025-10-16): This module implements the fix for three
critical security vulnerabilities:

- Vulnerability #1 (READ BYPASS): Agents could bypass visibility via ctx.board.list()
- Vulnerability #2 (WRITE BYPASS): Agents could bypass validation via ctx.board.publish()
- Vulnerability #3 (GOD MODE): Agents had unlimited ctx.orchestrator access

Solution: Context Provider enforces visibility filtering BEFORE agents see data.
Agents can NO LONGER bypass security because they don't have direct store access.

References:
- .flock/flock-research/context-provider/SECURITY_ANALYSIS.md
- docs/specs/007-context-provider-security-fix/PLAN.md
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any, Protocol
from uuid import UUID

from flock.core.artifacts import Artifact
from flock.core.store import BlackboardStore, FilterConfig
from flock.core.visibility import AgentIdentity


@dataclass
class ContextRequest:
    """Request for agent context.

    This carries all information needed for providers to filter context
    with mandatory visibility enforcement.

    Attributes:
        agent: Agent instance requesting context
        correlation_id: Workflow identifier for filtering
        store: Blackboard store for querying artifacts
        agent_identity: Agent identity for visibility checks (includes labels, tenant_id)
        exclude_ids: Set of artifact IDs to exclude from context (e.g., input artifacts)
    """

    agent: Any  # Agent type to avoid circular import
    correlation_id: str
    store: BlackboardStore
    agent_identity: AgentIdentity
    exclude_ids: set[UUID] | None = None


class ContextProvider(Protocol):
    """Protocol for context providers.

    Context Providers are the MANDATORY security boundary between agents
    and the blackboard store. All providers MUST enforce visibility filtering.

    SECURITY REQUIREMENT: Every provider implementation MUST call
    artifact.visibility.allows(agent_identity) before returning artifacts.
    Any provider that doesn't enforce visibility is a SECURITY BUG.

    Implementations:
    - DefaultContextProvider: Filters by correlation_id + visibility (default behavior)
    - FilteredContextProvider: Wraps FilterConfig for declarative filtering + visibility

    Usage:
        # Global provider
        flock = Flock(context_provider=MyProvider())

        # Per-agent provider
        agent.with_context(MyProvider())
    """

    async def __call__(self, request: ContextRequest) -> list[Artifact]:
        """Fetch context with MANDATORY visibility enforcement.

        Args:
            request: Context request with agent identity and correlation

        Returns:
            List of Artifact objects that agent is allowed to see.

        SECURITY: Implementation MUST filter by visibility using:
            artifact.visibility.allows(request.agent_identity)
        """
        ...


class BaseContextProvider(ABC):
    """Base class enforcing MANDATORY visibility filtering for all context providers.

    **SECURITY BY DESIGN**: Subclasses implement get_artifacts() to query/filter
    artifacts. Visibility filtering and exclude_ids handling are enforced by this
    base class and CANNOT BE BYPASSED.

    This makes it architecturally impossible to create an insecure provider that
    forgets to check visibility. The security logic is centralized and guaranteed.

    Architecture:
    - Subclass implements: get_artifacts() - custom query/filtering logic
    - Base class enforces: visibility filtering, exclude_ids
    - Result: 75% less code, 100% security coverage

    Example:
        >>> class MyProvider(BaseContextProvider):
        ...     async def get_artifacts(self, request):
        ...         # Just return artifacts - base class handles visibility!
        ...         artifacts, _ = await request.store.query_artifacts(...)
        ...         return artifacts
    """

    @abstractmethod
    async def get_artifacts(self, request: ContextRequest) -> list[Artifact]:
        """Query and filter artifacts (will be visibility-filtered automatically).

        Subclasses implement this to define their filtering logic:
        - DefaultContextProvider: Query all artifacts
        - CorrelatedContextProvider: Query by correlation_id
        - RecentContextProvider: Query all + sort by time + limit
        - TimeWindowContextProvider: Query all + filter by time window
        - EmptyContextProvider: Return empty list

        Args:
            request: Context request with agent identity, store, correlation_id

        Returns:
            List of artifacts (will be visibility-filtered by base class)
        """

    async def __call__(self, request: ContextRequest) -> list[Artifact]:
        """Fetch context with MANDATORY visibility enforcement (cannot be bypassed).

        SECURITY IMPLEMENTATION (enforced by base class):
        1. Get artifacts from subclass (custom filtering logic)
        2. Filter by visibility (security filtering) - MANDATORY, CANNOT BE BYPASSED
        3. Exclude specific artifacts (if requested)

        Args:
            request: Context request with agent identity

        Returns:
            List of Artifact objects agent can see (visibility-filtered)
        """
        # Step 1: Get artifacts from subclass implementation
        artifacts = await self.get_artifacts(request)

        # Step 2: CRITICAL SECURITY STEP - Filter by visibility (ENFORCED BY BASE CLASS)
        # This is the FIX for Vulnerability #1 (READ BYPASS)
        # Subclasses CANNOT bypass this - it's architecturally impossible
        visible_artifacts = [
            artifact
            for artifact in artifacts
            if artifact.visibility.allows(request.agent_identity)
        ]

        # Step 3: Exclude specific artifacts (e.g., input artifacts to avoid duplication)
        if request.exclude_ids:
            visible_artifacts = [
                artifact
                for artifact in visible_artifacts
                if artifact.id not in request.exclude_ids
            ]

        return visible_artifacts


class DefaultContextProvider(BaseContextProvider):
    """Default context provider - shows ALL artifacts on blackboard with MANDATORY visibility enforcement.

    **EXPLICIT IS BETTER THAN IMPLICIT**: This provider shows agents everything on the
    blackboard they're allowed to see (visibility-filtered). No magic correlation filtering!

    If you want correlation-based filtering, use CorrelatedContextProvider explicitly.

    This provider implements the secure replacement for the old vulnerable pattern:
        Old (INSECURE): all_artifacts = await ctx.board.list()
        New (SECURE): context = await provider(request)

    Security Properties:
    - ✅ Shows ALL artifacts on blackboard (no hidden filtering)
    - ✅ Enforces visibility (security boundary) - CANNOT BE BYPASSED (via BaseContextProvider)
    - ✅ Returns only artifacts agent is allowed to see
    - ✅ No direct store access exposed to agents

    This fixes Vulnerability #1 (READ BYPASS) where agents could access
    any artifact regardless of visibility by calling ctx.board.list().

    Example:
        >>> # Global: All agents see everything they're allowed to
        >>> flock = Flock(context_provider=DefaultContextProvider())
        >>>
        >>> # Per-agent: This agent sees full blackboard
        >>> agent.context_provider = DefaultContextProvider()
    """

    async def get_artifacts(self, request: ContextRequest) -> list[Artifact]:
        """Query ALL artifacts from blackboard (no filtering).

        Visibility filtering is enforced by BaseContextProvider automatically.

        Args:
            request: Context request with store access

        Returns:
            All artifacts from blackboard (will be visibility-filtered by base class)
        """
        artifacts, _ = await request.store.query_artifacts(
            FilterConfig(),  # Empty filter = get everything
            limit=-1,  # Get all artifacts
        )
        return artifacts


class FilteredContextProvider(BaseContextProvider):
    """Context provider with declarative filtering + MANDATORY visibility enforcement.

    This provider combines declarative filtering (FilterConfig) with security
    enforcement (visibility). It implements Phase 4 of the security fix.

    Security Properties:
    - ✅ Filters by FilterConfig (declarative filtering: tags, types, correlation, etc.)
    - ✅ Enforces visibility (security boundary) - CANNOT BE BYPASSED (via BaseContextProvider)
    - ✅ Returns only artifacts matching BOTH filters AND visibility
    - ✅ No direct store access exposed to agents

    Example:
        >>> # Filter by tags + enforce visibility
        >>> provider = FilteredContextProvider(
        ...     FilterConfig(tags={"important", "urgent"}), limit=10
        ... )
        >>> agent.with_context(provider)

        >>> # Filter by type + enforce visibility
        >>> provider = FilteredContextProvider(
        ...     FilterConfig(type_names={"Task", "Report"}), limit=50
        ... )
    """

    def __init__(self, filter_config: FilterConfig, limit: int = 50):
        """Initialize FilteredContextProvider with declarative filters.

        Args:
            filter_config: FilterConfig specifying declarative filters
            limit: Maximum number of artifacts to return (default: 50)
        """
        self.filter_config = filter_config
        self.limit = limit

    async def get_artifacts(self, request: ContextRequest) -> list[Artifact]:
        """Query artifacts using FilterConfig (declarative filtering).

        Visibility filtering is enforced by BaseContextProvider automatically.

        Args:
            request: Context request with store access

        Returns:
            Artifacts matching FilterConfig (will be visibility-filtered by base class)
        """
        artifacts, _ = await request.store.query_artifacts(
            self.filter_config,
            limit=self.limit,
        )
        return artifacts


class BoundContextProvider:
    """Security wrapper that binds a provider to a specific agent identity.

    SECURITY FIX (2025-10-17): This wrapper prevents engines from forging
    Context objects with fake agent_identity values. Even if an engine creates
    a fake Context with a different agent_identity, this wrapper will use the
    trusted identity that was bound at creation time by the orchestrator.

    The orchestrator creates a BoundContextProvider for each agent execution,
    binding it to the agent's true identity. Engines cannot bypass this because
    they would need to create a fake BoundContextProvider, but they don't have
    access to the real bound identity.

    Example Attack (prevented):
        >>> # Malicious engine tries to escalate privileges
        >>> fake_ctx = Context(
        ...     ...
        ...     agent_identity=AgentIdentity(name="admin", labels={"admin"}),  # FAKE
        ... )
        >>> # Provider ignores fake identity, uses bound identity instead
        >>> context = await bound_provider(
        ...     request
        ... )  # Still filters as original agent
    """

    def __init__(
        self, inner_provider: ContextProvider, bound_agent_identity: AgentIdentity
    ):
        """Create provider bound to specific agent identity.

        Args:
            inner_provider: Wrapped provider (e.g., DefaultContextProvider)
            bound_agent_identity: Trusted agent identity from orchestrator
        """
        self._inner = inner_provider
        self._bound_identity = bound_agent_identity

    async def __call__(self, request: ContextRequest) -> list[Artifact]:
        """Fetch context using BOUND agent identity (ignoring request.agent_identity).

        SECURITY: This method ignores request.agent_identity because it could
        come from untrusted engine code. Instead, it uses the bound identity
        that was set by the orchestrator at Context creation time.

        Args:
            request: Context request (agent_identity field is IGNORED)

        Returns:
            List of Artifact objects filtered by BOUND identity (not request identity)
        """
        # SECURITY: Replace untrusted agent_identity with trusted bound identity
        secure_request = ContextRequest(
            agent=request.agent,
            correlation_id=request.correlation_id,
            store=request.store,
            agent_identity=self._bound_identity,  # Use trusted identity, ignore request
            exclude_ids=request.exclude_ids,
        )
        return await self._inner(secure_request)


class CorrelatedContextProvider(BaseContextProvider):
    """Context provider that filters by correlation_id + visibility.

    **EXPLICIT WORKFLOW ISOLATION**: Use this when you want agents to see only
    artifacts from their specific workflow (correlation_id).

    This is the explicit version of what DefaultContextProvider used to do implicitly.
    Now you choose: full blackboard (DefaultContextProvider) or workflow-scoped
    (CorrelatedContextProvider).

    Security Properties:
    - ✅ Filters by correlation_id (workflow boundary)
    - ✅ Enforces visibility (security boundary) - CANNOT BE BYPASSED (via BaseContextProvider)
    - ✅ Returns only workflow artifacts agent is allowed to see

    Example:
        >>> # Global: All agents only see their workflow
        >>> flock = Flock(context_provider=CorrelatedContextProvider())
        >>>
        >>> # Per-agent: This agent only sees workflow artifacts
        >>> agent.context_provider = CorrelatedContextProvider()
        >>>
        >>> # Use case: Multi-tenant SaaS with workflow isolation
        >>> # Each workflow (correlation_id) is isolated from others
    """

    async def get_artifacts(self, request: ContextRequest) -> list[Artifact]:
        """Query artifacts by correlation_id (workflow filtering).

        Visibility filtering is enforced by BaseContextProvider automatically.

        Args:
            request: Context request with correlation_id

        Returns:
            Workflow artifacts (will be visibility-filtered by base class)
        """
        artifacts, _ = await request.store.query_artifacts(
            FilterConfig(correlation_id=request.correlation_id),
            limit=-1,  # Get all workflow artifacts
        )
        return artifacts


class RecentContextProvider(BaseContextProvider):
    """Context provider that shows only the N most recent artifacts.

    **TOKEN COST CONTROL**: Perfect for keeping context small and relevant by
    showing only the most recent artifacts (sorted by creation time).

    Security Properties:
    - ✅ Limits context to N most recent artifacts
    - ✅ Enforces visibility (security boundary) - CANNOT BE BYPASSED (via BaseContextProvider)
    - ✅ Reduces token costs by limiting context size

    Example:
        >>> # Global: All agents see only last 10 artifacts
        >>> flock = Flock(context_provider=RecentContextProvider(limit=10))
        >>>
        >>> # Per-agent: This agent sees only last 50 artifacts
        >>> agent.context_provider = RecentContextProvider(limit=50)
        >>>
        >>> # Use case: High-volume systems where full history is too expensive
        >>> # Agent only needs recent context to make decisions
    """

    def __init__(self, limit: int = 50):
        """Initialize RecentContextProvider with artifact limit.

        Args:
            limit: Maximum number of recent artifacts to return (default: 50)
        """
        self.limit = limit

    async def get_artifacts(self, request: ContextRequest) -> list[Artifact]:
        """Query all artifacts and return N most recent.

        Visibility filtering is enforced by BaseContextProvider automatically.

        Args:
            request: Context request with store access

        Returns:
            N most recent artifacts (will be visibility-filtered by base class)
        """
        artifacts, _ = await request.store.query_artifacts(
            FilterConfig(),
            limit=-1,  # Get all artifacts
        )

        # Sort by creation time (most recent first) and limit
        artifacts.sort(key=lambda a: a.created_at, reverse=True)
        return artifacts[: self.limit]


class TimeWindowContextProvider(BaseContextProvider):
    """Context provider that shows only artifacts from the last X hours.

    **TIME-BASED FILTERING**: Perfect for real-time monitoring or event-driven
    systems where only recent data is relevant.

    Security Properties:
    - ✅ Filters artifacts by time window (last X hours)
    - ✅ Enforces visibility (security boundary) - CANNOT BE BYPASSED (via BaseContextProvider)
    - ✅ Automatic cleanup of old context (no manual pruning needed)

    Example:
        >>> # Global: All agents see only last hour
        >>> flock = Flock(context_provider=TimeWindowContextProvider(hours=1))
        >>>
        >>> # Per-agent: This agent sees last 24 hours
        >>> agent.context_provider = TimeWindowContextProvider(hours=24)
        >>>
        >>> # Use case: Real-time monitoring dashboard
        >>> # Only show events from last hour, ignore old data
    """

    def __init__(self, hours: int = 1):
        """Initialize TimeWindowContextProvider with time window.

        Args:
            hours: Number of hours to look back (default: 1)
        """
        self.hours = hours

    async def get_artifacts(self, request: ContextRequest) -> list[Artifact]:
        """Query all artifacts and filter by time window.

        Visibility filtering is enforced by BaseContextProvider automatically.

        Args:
            request: Context request with store access

        Returns:
            Artifacts within time window (will be visibility-filtered by base class)
        """
        cutoff = datetime.now() - timedelta(hours=self.hours)

        artifacts, _ = await request.store.query_artifacts(
            FilterConfig(),
            limit=-1,  # Get all artifacts
        )

        # Filter by time window
        return [artifact for artifact in artifacts if artifact.created_at >= cutoff]


class EmptyContextProvider(BaseContextProvider):
    """Context provider that returns NO historical context.

    **STATELESS AGENTS**: Use this for purely functional agents that only
    transform input → output without needing any historical context.

    This is the ultimate token saver - zero context overhead!

    Security Properties:
    - ✅ Returns empty context (no artifacts)
    - ✅ Enforces visibility (N/A - no artifacts to filter)
    - ✅ Maximum token savings (zero context tokens)

    Example:
        >>> # Global: All agents are stateless (no context)
        >>> flock = Flock(context_provider=EmptyContextProvider())
        >>>
        >>> # Per-agent: This agent is purely functional
        >>> translator.context_provider = EmptyContextProvider()
        >>>
        >>> # Use case: Simple transformation agents
        >>> # Agent: English → Spanish (no history needed)
        >>> # Agent: Markdown → HTML (no history needed)
        >>> # Agent: Image → Thumbnail (no history needed)
    """

    async def get_artifacts(self, request: ContextRequest) -> list[Artifact]:
        """Return no artifacts (stateless agent).

        Visibility filtering is enforced by BaseContextProvider automatically
        (though there's nothing to filter).

        Args:
            request: Context request (ignored)

        Returns:
            Empty list (no artifacts)
        """
        return []


__all__ = [
    "BaseContextProvider",
    "BoundContextProvider",
    "ContextProvider",
    "ContextRequest",
    "CorrelatedContextProvider",
    "DefaultContextProvider",
    "EmptyContextProvider",
    "FilteredContextProvider",
    "RecentContextProvider",
    "TimeWindowContextProvider",
]
