"""Provider interface protocols for Flow SDK.

This module defines the contract between Flow SDK's core domain and
cloud provider implementations. All protocols use structural typing
via Python's Protocol class, enabling duck typing with static analysis.

Architectural Role:
    - Dependency Inversion: Core depends on abstractions, not implementations
    - Open/Closed: New providers extend without modifying core
    - Interface Segregation: Separate compute, storage, and frontend concerns

Protocol Categories:
    1. IHttpClient: HTTP communication abstraction
    2. IComputeProvider: GPU instance lifecycle management
    3. IStorageProvider: Persistent volume operations
    4. IProvider: Combined compute + storage + user management
    5. IFrontendAdapter: Compatibility layers (SLURM, Submitit)
    6. Support Protocols: Job mapping, storage management

Implementation Guidelines:
    - All methods must be thread-safe (providers may be shared)
    - Errors should raise FlowError subclasses with context
    - Authentication handled internally (not in method signatures)
    - Resource cleanup must be idempotent
    - Network operations should implement retry logic

Examples:
    Minimal provider implementation:
        >>> class MinimalProvider:
        ...     def find_instances(self, requirements, limit=10):
        ...         # Return instances matching requirements
        ...         return [Instance(...)]
        ...     
        ...     def submit_task(self, instance_id, config, volume_ids=None):
        ...         # Launch task on instance
        ...         return Task(...)
        ...     
        ...     # ... implement remaining required methods
    
    Example AWS provider with error handling:
        >>> class AWSProvider:
        ...     def __init__(self, region="us-east-1"):
        ...         self.ec2 = boto3.client("ec2", region_name=region)
        ...         self.s3 = boto3.client("s3", region_name=region)
        ...     
        ...     def find_instances(self, requirements, limit=10):
        ...         try:
        ...             filters = self._build_filters(requirements)
        ...             response = self.ec2.describe_spot_price_history(
        ...                 Filters=filters,
        ...                 MaxResults=limit
        ...             )
        ...             return [self._parse_instance(r) for r in response["SpotPriceHistory"]]
        ...         except ClientError as e:
        ...             raise ProviderError(f"Failed to find instances: {e}")
        ...     
        ...     def submit_task(self, instance_id, config, volume_ids=None):
        ...         startup_script = self._build_cloud_init(config)
        ...         try:
        ...             response = self.ec2.run_instances(
        ...                 ImageId=self._get_ami(config.image),
        ...                 InstanceType=instance_id,
        ...                 MinCount=1, MaxCount=1,
        ...                 UserData=base64.b64encode(startup_script.encode()),
        ...                 BlockDeviceMappings=self._map_volumes(volume_ids),
        ...                 IamInstanceProfile={"Arn": self.instance_profile},
        ...                 SecurityGroupIds=[self.security_group],
        ...                 TagSpecifications=[{
        ...                     "ResourceType": "instance",
        ...                     "Tags": [
        ...                         {"Key": "flow-task-id", "Value": config.name},
        ...                         {"Key": "flow-project", "Value": self.project}
        ...                     ]
        ...                 }]
        ...             )
        ...             instance_id = response["Instances"][0]["InstanceId"]
        ...             return Task(
        ...                 task_id=instance_id,
        ...                 name=config.name,
        ...                 status=TaskStatus.PENDING,
        ...                 config=config,
        ...                 created_at=datetime.utcnow(),
        ...                 instance_type=instance_id,
        ...                 region=self.region,
        ...                 _provider=self
        ...             )
        ...         except ClientError as e:
        ...             if e.response["Error"]["Code"] == "InsufficientInstanceCapacity":
        ...                 raise ResourceNotAvailableError(
        ...                     f"No {instance_id} capacity in {self.region}",
        ...                     suggestions=["Try a different region", "Use a different instance type"]
        ...                 )
        ...             raise ProviderError(f"Failed to launch instance: {e}")
"""

from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Protocol

from flow.api.models import Instance, Task, TaskConfig, TaskStatus, User, Volume

if TYPE_CHECKING:
    from flow.providers.base import ProviderCapabilities


class IHttpClient(Protocol):
    """HTTP client abstraction for provider communication.

    Defines a minimal HTTP interface enabling different implementations
    (requests, httpx, aiohttp) while maintaining consistent behavior.
    Implementations must handle authentication, retries, and connection
    pooling transparently.

    Implementation Requirements:
        - Automatic retry with exponential backoff for transient failures
        - Connection pooling for performance (100+ connections)
        - Request/response logging for debugging (debug level)
        - Timeout handling (30s default, configurable)
        - Error mapping to FlowError hierarchy

    Thread Safety:
        Implementations MUST be thread-safe for concurrent requests.
    """

    def request(
        self,
        method: str,
        url: str,
        headers: Optional[Dict[str, str]] = None,
        json: Optional[Any] = None,
        params: Optional[Dict[str, str]] = None,
    ) -> Dict[str, Any]:
        """Execute HTTP request with automatic retries.

        Performs HTTP request with built-in retry logic, connection pooling,
        and comprehensive error handling. All network failures are mapped
        to appropriate FlowError subclasses.

        Args:
            method: HTTP method (GET, POST, PUT, DELETE, PATCH).
                Case-insensitive.
            url: URL path relative to base URL configured at client creation.
                Leading slash optional. Example: "spot/bids" or "/spot/bids".
            headers: Additional HTTP headers. Base headers (auth, user-agent)
                are added automatically. Common headers:
                - Content-Type (auto-set for json parameter)
                - Accept (defaults to application/json)
                - X-Request-ID (auto-generated for tracing)
            json: Request body for POST/PUT/PATCH. Automatically serialized
                and Content-Type set to application/json.
            params: URL query parameters. Values converted to strings.
                None values are omitted. Lists become repeated params.

        Returns:
            Dict[str, Any]: Parsed JSON response body. Empty dict for 204.
                Raw response stored in debug logs.

        Raises:
            ValidationError: 400 Bad Request with field-level errors
            AuthenticationError: 401 Unauthorized (invalid API key)
            ResourceNotFoundError: 404 Not Found
            ValidationAPIError: 422 Unprocessable Entity with details
            APIError: Other 4xx/5xx errors with status code
            NetworkError: Connection failures, DNS errors
            TimeoutError: Request exceeded timeout (30s default)

        Retry Logic:
            - Retries: 3 attempts with exponential backoff
            - Retriable: 5xx, connection errors, timeouts
            - Non-retriable: 4xx client errors
            - Backoff: 1s, 2s, 4s with jitter
        """
        ...

    def close(self) -> None:
        """Release HTTP client resources gracefully.

        Closes connection pools, cancels pending requests, and frees
        resources. Safe to call multiple times. Should be called on
        application shutdown or provider cleanup.

        Implementation Notes:
            - Close all connection pools
            - Cancel in-flight requests (best effort)
            - Clear authentication tokens
            - Flush logs and metrics
            - No exception on double-close
        """
        ...


class IComputeProvider(Protocol):
    """GPU compute infrastructure provider interface.

    Defines the contract for cloud provider integrations, enabling
    uniform task submission and monitoring across heterogeneous
    infrastructure (FCP, AWS, GCP, Azure, Lambda Labs).

    Design Principles:
        - Task-centric: All operations use task_id as primary key
        - Fail-fast: Validate early, error clearly
        - Progressive disclosure: Simple operations simple
        - Cloud-agnostic: No provider specifics in signatures

    Invariants:
        - Task IDs are globally unique and stable
        - Status queries complete within 500ms
        - Log streaming handles arbitrary output sizes
        - All methods are idempotent where possible
        - Resource cleanup is provider's responsibility

    Performance Requirements:
        - find_instances: <1s for catalog query
        - submit_task: <10s for instance provisioning
        - get_task_status: <500ms for status check
        - stream_logs: <100ms latency per chunk

    Error Handling:
        All methods must map provider-specific errors to FlowError
        hierarchy for consistent client experience.
    """

    def find_instances(
        self,
        requirements: Dict[str, Any],
        limit: int = 10,
    ) -> List[Instance]:
        """Find available GPU instances matching requirements.

        Searches provider inventory for instances satisfying all specified
        constraints. Returns best matches first based on price/performance.
        Used by Flow.run() for automatic instance selection.

        Args:
            requirements: Constraint dictionary with optional keys:
                - instance_type (str): Exact instance type to match.
                    Provider-specific format (e.g., "p3.2xlarge", "a100-80gb").
                    If specified, other constraints are hints.
                - min_gpu_count (int): Minimum number of GPUs required.
                    Range: 1-16 typical, up to 64 for special instances.
                - max_price (float): Maximum hourly price in USD.
                    Spot/preemptible pricing. None means any price.
                - region (str): Target region/zone for deployment.
                    Provider-specific format ("us-east-1", "us-central1-a").
                - gpu_memory_gb (int): Minimum GPU memory per device.
                    Common: 16, 24, 40, 80. Used for capability matching.
                - gpu_type (str): GPU model hint ("a100", "v100", "h100").
                    Lowercase, no vendor prefix.
            limit: Maximum results to return. Range: 1-100.
                Lower limits reduce response time.

        Returns:
            List[Instance]: Available instances sorted by preference:
                1. Price (ascending) - cheapest first
                2. GPU memory (descending) - most capable
                3. Availability (descending) - most capacity
                Empty list if no instances match all requirements.

        Raises:
            ValidationError: Invalid requirements (e.g., impossible combo)
            ProviderError: Provider API failure or permission issue

        Example:
            >>> # Find cheapest A100 instance
            >>> instances = provider.find_instances({
            ...     "gpu_type": "a100",
            ...     "max_price": 10.0,
            ...     "region": "us-east-1"
            ... })
            >>> if instances:
            ...     cheapest = instances[0]
        """
        ...

    def submit_task(
        self,
        instance_type: str,
        config: TaskConfig,
        volume_ids: Optional[List[str]] = None,
    ) -> Task:
        """Submit task with automatic instance and region selection.

        Launches computational workload on best available instance matching
        the requested type. Provider handles all translation from user-friendly
        instance types to internal IDs and selects optimal region automatically.

        Args:
            instance_type: User-friendly instance type specification.
                Examples: "a100", "4xa100", "8xh100", "2xv100".
                Provider handles all format translation and validation.
            config: Complete task specification including:
                - command/script/shell: Execution target
                - image: Container image with GPU support
                - env: Environment variables
                - ssh_keys: Authorized keys for access
                - max_run_time_hours: Auto-termination timeout
                - region: Optional region preference (auto-selected if not specified)
            volume_ids: List of volume IDs to attach. Volumes must:
                - Not be attached to other instances
                - Match mount paths in config.volumes
                Provider ensures region compatibility.

        Returns:
            Task: Handle with methods for monitoring and control:
                - task_id: Unique identifier for all operations
                - status: Current state (initially PENDING)
                - ssh_host/port: Connection details (when RUNNING)
                - logs(): Output retrieval
                - cancel(): Termination
                - region: Selected region for execution

        Raises:
            ResourceNotAvailableError: No instances of requested type available
            ValidationError: Invalid instance type or configuration
            VolumeError: Volume attachment failed
            ProviderError: Launch failure (quota, permissions)

        Provider Responsibilities:
            - Translate instance_type to internal format
            - Check availability across all regions
            - Select optimal region (capacity, price, volume location)
            - Handle all provider-specific details internally
            - Map provider statuses to TaskStatus enum
        """
        ...

    def get_task(self, task_id: str) -> Task:
        """Retrieve complete task state with fresh data.

        Fetches current task information from provider, including
        status, connection details, and cost information. More
        expensive than get_task_status() but provides full metadata.

        Args:
            task_id: Task identifier from submit_task().
                Must be valid task owned by current project.

        Returns:
            Task: Complete task object with current state:
                - All original fields from submission
                - Updated status and timestamps
                - SSH connection info (if RUNNING)
                - Cost accumulation
                - Error messages (if FAILED)

        Raises:
            TaskNotFoundError: Task doesn't exist or access denied
            ProviderError: API communication failure

        Performance:
            Typical: 200-500ms (includes instance metadata)
        """
        ...

    def get_task_status(self, task_id: str) -> TaskStatus:
        """Query task status efficiently.

        Lightweight status check optimized for polling. Returns only
        status enum without full task metadata. Use for wait loops
        and progress monitoring.

        Args:
            task_id: Task identifier to check

        Returns:
            TaskStatus: Current state enum:
                - PENDING: Queued or provisioning
                - RUNNING: Executing on GPU
                - COMPLETED: Finished (exit 0)
                - FAILED: Error or non-zero exit
                - CANCELLED: User termination

        Raises:
            TaskNotFoundError: Invalid or inaccessible task

        Performance:
            Target: <200ms (may use short-lived cache)
        """
        ...

    def stop_task(self, task_id: str) -> bool:
        """Terminate task execution gracefully.

        Initiates shutdown sequence: SIGTERM → wait → SIGKILL.
        Attempts graceful termination before force-killing.
        Cleans up resources and stops billing.

        Args:
            task_id: Task to terminate. Must be PENDING or RUNNING.

        Returns:
            bool: True if termination initiated, False if already terminal

        Termination Sequence:
            1. Send SIGTERM to process group
            2. Wait 30s for graceful shutdown
            3. Force SIGKILL if still running
            4. Unmount volumes and cleanup
            5. Update status to CANCELLED

        Side Effects:
            - Stops billing immediately
            - Preserves logs for retrieval
            - Detaches but preserves volumes
            - Releases GPU allocation
        """
        ...

    def cancel_task(self, task_id: str) -> bool:
        """Cancel task execution (deprecated).

        Deprecated:
            Use stop_task() instead. This method exists only
            for backward compatibility and will be removed.

        Args:
            task_id: Task to cancel

        Returns:
            bool: Result from stop_task()
        """
        ...

    def get_task_logs(
        self,
        task_id: str,
        tail: int = 100,
        log_type: str = "stdout",  # stdout, stderr
    ) -> str:
        """Fetch recent task output logs.

        Retrieves last N lines of task stdout/stderr. Handles
        large outputs efficiently with server-side tail.

        Args:
            task_id: Task whose logs to retrieve
            tail: Number of recent lines to return.
                Range: 1-10000. Large values may be slow.
            log_type: Output stream to fetch:
                - "stdout": Standard output (default)
                - "stderr": Standard error
                - "combined": Both streams interleaved

        Returns:
            str: Log lines joined with newlines.
                Empty string if no output yet.
                May include partial lines at boundaries.

        Raises:
            TaskNotFoundError: Invalid task ID
            ProviderError: Log retrieval failed

        Performance:
            - 100 lines: ~200ms
            - 1000 lines: ~500ms
            - 10000 lines: ~2s
        """
        ...

    def stream_task_logs(
        self,
        task_id: str,
        log_type: str = "stdout",  # stdout, stderr
    ) -> Iterator[str]:
        """Stream task logs in real-time.

        Returns iterator that yields new log lines as they arrive.
        Handles reconnection and backpressure automatically.
        Suitable for long-running tasks with continuous output.

        Args:
            task_id: Task to stream logs from
            log_type: Stream selection:
                - "stdout": Standard output
                - "stderr": Standard error
                - "combined": Both interleaved

        Yields:
            str: Individual log lines without newlines.
                Yields empty string for keepalive.
                Stops yielding when task terminates.

        Raises:
            TaskNotFoundError: Invalid task
            ProviderError: Streaming setup failed

        Connection Behavior:
            - Auto-reconnects on network failure
            - Buffers up to 1MB during disconnection
            - Applies backpressure if consumer is slow
            - Closes cleanly when iterator is destroyed

        Example:
            >>> for line in provider.stream_task_logs(task_id):
            ...     if "ERROR" in line:
            ...         print(f"Error detected: {line}")
            ...     if task.is_terminal:
            ...         break
        """
        ...

    def list_tasks(
        self,
        status: Optional[TaskStatus] = None,
        limit: int = 100,
    ) -> List[Task]:
        """List tasks with optional status filter.

        Retrieves recent tasks for current project/user. Results
        ordered by creation time (newest first). Supports pagination
        via limit parameter.

        Args:
            status: Filter by specific status. None returns all.
            limit: Maximum tasks to return. Range: 1-1000.
                Large limits increase response time.

        Returns:
            List[Task]: Task objects with full metadata.
                Empty list if no matching tasks.
                Ordered by created_at descending.

        Performance:
            O(limit) response time, typically 2-5ms per task
        """
        ...

    def prepare_task_config(self, config: TaskConfig) -> TaskConfig:
        """Apply provider-specific defaults to task configuration.

        Enriches user configuration with provider requirements and
        optimizations. Called automatically by Flow.run() before
        instance selection. Modifications are provider-scoped.

        Args:
            config: User-provided configuration

        Returns:
            TaskConfig: Enhanced configuration with:
                - SSH keys added if missing
                - Region set from provider default
                - Image adjusted for provider
                - Startup scripts injected
                - Security settings applied

        Modifications:
            - Must not alter user-specified values
            - Should be idempotent
            - Must maintain backward compatibility

        Example Enhancements:
            - Add provider SSH key to config.ssh_keys
            - Set optimal image for GPU type
            - Configure provider-specific env vars
            - Add monitoring/logging agents
        """
        ...

    def get_task_instances(self, task_id: str) -> List[Instance]:
        """Fetch detailed instance information for task.

        Retrieves connection details for all instances in a multi-node
        task. Essential for distributed training coordination and
        debugging multi-instance deployments.

        Args:
            task_id: Task to query. Must exist and be accessible.

        Returns:
            List[Instance]: Instance details for each node:
                - instance_id: Cloud provider instance ID
                - public_ip: External IP address
                - private_ip: VPC-internal IP
                - ssh_host/port: SSH connection info
                - status: Instance state
                Order matches num_instances from config.

        Raises:
            TaskNotFoundError: Task doesn't exist
            ProviderError: Instance query failed

        Use Cases:
            - Get IPs for distributed training setup
            - SSH to specific nodes in multi-instance
            - Monitor individual instance health
            - Configure node-to-node communication
        """
        ...


class IStorageProvider(Protocol):
    """Persistent storage volume management interface.

    Defines operations for durable block storage that persists across
    task lifecycles. Volumes provide high-performance storage for
    datasets, checkpoints, and shared state.

    Storage Model:
        - Volumes are project-scoped and region-specific
        - Support concurrent reads, exclusive writes
        - Automatically formatted on first use (ext4)
        - Encrypted at rest (provider-managed keys)

    Performance Tiers:
        - Standard: 3 IOPS/GB, suitable for sequential
        - Performance: Up to 64K IOPS for databases
    """

    def create_volume(
        self,
        size_gb: int,
        name: Optional[str] = None,
        interface: str = "block",
    ) -> Volume:
        """Create new persistent storage volume.

        Provisions network-attached storage in the current region.
        Volume is immediately available for attachment but may take
        time to reach full performance.

        Args:
            size_gb: Volume size in gigabytes.
                Range: 1-16384 (16TB). No online resize.
                Common sizes: 100 (datasets), 500 (models).
            name: Human-readable identifier.
                Optional but recommended for management.
                Constraints: 3-63 chars, lowercase alphanumeric + dash.
                Must be unique within project.
            interface: Storage type - "block" (default) or "file".
                - block: High-performance exclusive access
                - file: Shared access across instances (NFS)

        Returns:
            Volume: Created volume with metadata:
                - volume_id: Unique identifier for operations
                - name: As provided or auto-generated
                - size_gb: Allocated capacity
                - region: Deployment location
                - status: "available" or "creating"

        Raises:
            ValidationError: Invalid size or name
            QuotaExceededError: Storage limit reached
            ProviderError: Provisioning failed

        Cost:
            ~$0.10/GB/month plus IOPS charges
        """
        ...

    def delete_volume(self, volume_id: str) -> bool:
        """Delete storage volume permanently.

        Immediately destroys volume and all data. Cannot be undone.
        Fails if volume is currently attached to running tasks.

        Args:
            volume_id: Volume to delete (from create_volume)

        Returns:
            bool: True if deleted, False if not found

        Raises:
            VolumeInUseError: Volume attached to task
            ProviderError: Deletion failed

        Warning:
            No recovery after deletion. Backup critical data first.
        """
        ...

    def list_volumes(
        self,
        limit: int = 100,
    ) -> List[Volume]:
        """List storage volumes in project.

        Enumerates all volumes accessible to current credentials.
        Includes attached and detached volumes across all regions.

        Args:
            limit: Maximum results. Range: 1-1000.

        Returns:
            List[Volume]: Volumes ordered by created_at (newest first).
                Includes size, region, and attachment status.

        Performance:
            Linear in limit, ~1ms per volume
        """
        ...

    def upload_file(
        self,
        volume_id: str,
        local_path: Path,
        remote_path: Optional[str] = None,
    ) -> bool:
        """Upload local file to volume.

        Transfers file to specified path within volume. Volume must
        be detached or attached to a stopped instance. Creates parent
        directories as needed.

        Args:
            volume_id: Target volume ID
            local_path: Source file path
            remote_path: Destination path in volume.
                Relative to volume root. Defaults to filename.

        Returns:
            bool: True if uploaded successfully

        Raises:
            FileNotFoundError: Local file doesn't exist
            VolumeNotFoundError: Invalid volume ID
            VolumeInUseError: Volume attached to running task
            ProviderError: Transfer failed

        Note:
            For large files, consider mounting volume and using
            task-based transfer for better performance.
        """
        ...

    def upload_directory(
        self,
        volume_id: str,
        local_path: Path,
        remote_path: Optional[str] = None,
    ) -> bool:
        """Upload directory tree to volume.

        Recursively transfers directory contents preserving structure.
        Handles nested directories and maintains permissions.

        Args:
            volume_id: Target volume
            local_path: Source directory
            remote_path: Destination in volume.
                Created if not exists.

        Returns:
            bool: True if all files uploaded

        Performance:
            ~10MB/s typical, varies by file count/size
        """
        ...

    def download_file(
        self,
        volume_id: str,
        remote_path: str,
        local_path: Path,
    ) -> bool:
        """Download file from volume."""
        ...

    def download_directory(
        self,
        volume_id: str,
        remote_path: str,
        local_path: Path,
    ) -> bool:
        """Download directory recursively."""
        ...

    def is_volume_id(self, identifier: str) -> bool:
        """Determine if identifier is volume ID or name.

        Distinguishes between volume IDs (provider-generated) and
        human-friendly names. Used for volume resolution in commands.

        Args:
            identifier: String to check

        Returns:
            bool: True if ID format, False if name format

        ID Formats:
            - FCP: "vol-" + random (e.g., "vol-abc123def456")
            - AWS: "vol-" + hex (e.g., "vol-0a1b2c3d4e5f6")
            - GCP: Numeric (e.g., "1234567890123456789")

        Name Format:
            - 3-63 characters
            - Lowercase alphanumeric + dash
            - No "vol-" prefix
        """
        ...


class IFrontendAdapter(Protocol):
    """Job scheduler compatibility interface.

    Enables Flow SDK to act as a drop-in replacement for traditional
    HPC job schedulers (SLURM, PBS) and ML frameworks (Submitit).
    Translates scheduler concepts to cloud-native execution.

    Adaptation Strategy:
        - Job ID mapping for status queries
        - Script execution via containers
        - Output redirection to cloud logs
        - State translation (PENDING→RUNNING→COMPLETE)
    """

    def submit_job(
        self,
        config: TaskConfig,
        script_path: Optional[str] = None,
        script_content: Optional[str] = None,
        env: Optional[Dict[str, str]] = None,
        working_dir: Optional[str] = None,
    ) -> str:
        """Submit job, return ID."""
        ...

    def get_job_status(self, job_id: str) -> TaskStatus:
        """Query job status."""
        ...

    def list_jobs(
        self,
        user: Optional[str] = None,
        states: Optional[List[str]] = None,
        limit: int = 100,
    ) -> List[Dict[str, Any]]:
        """List jobs with filters."""
        ...

    def cancel_job(self, job_id: str) -> bool:
        """Cancel job execution."""
        ...

    def get_job_output(self, job_id: str, output_type: str = "stdout") -> str:  # stdout, stderr
        """Fetch job output."""
        ...


class IJobMappingStore(Protocol):
    """Bidirectional job ID mapping storage.

    Maintains persistent mappings between frontend job IDs
    (SLURM job numbers, Submitit UUIDs) and Flow task IDs.

    Storage Requirements:
        - Persistent across sessions
        - Fast lookup in both directions
        - Automatic expiration (30 days)
    """

    def store_mapping(
        self,
        frontend_id: str,
        flow_task_id: str,
        frontend_type: str,  # "slurm", "submitit", "native"
        metadata: Optional[Dict[str, Any]] = None,
    ) -> None:
        """Persist ID mapping."""
        ...

    def get_flow_id(
        self,
        frontend_id: str,
        frontend_type: str,
    ) -> Optional[str]:
        """Resolve frontend ID to Flow ID."""
        ...

    def get_frontend_id(
        self,
        flow_task_id: str,
        frontend_type: str,
    ) -> Optional[str]:
        """Resolve Flow ID to frontend ID."""
        ...

    def list_mappings(
        self,
        frontend_type: Optional[str] = None,
        limit: int = 100,
    ) -> List[Dict[str, Any]]:
        """Query stored mappings."""
        ...


class IJobStorageManager(Protocol):
    """Job input/output data management.

    Provides durable storage for job artifacts, enabling
    result retrieval and job restart capabilities. Used
    primarily by frontend adapters.

    Storage Model:
        - Job-scoped namespaces
        - Automatic lifecycle (delete after 7 days)
        - Direct volume/S3 access for efficiency
    """

    def store_input(
        self,
        job_id: str,
        data: bytes,
        filename: str = "input.pkl",
    ) -> str:
        """Store input, return path."""
        ...

    def store_output(
        self,
        job_id: str,
        data: bytes,
        filename: str = "output.pkl",
    ) -> str:
        """Store output data."""
        ...

    def retrieve(
        self,
        job_id: str,
        filename: str,
    ) -> bytes:
        """Retrieve stored data."""
        ...

    def list_files(
        self,
        job_id: str,
    ) -> List[str]:
        """List job files."""
        ...


class IProvider(IComputeProvider, IStorageProvider):
    """Complete provider interface combining compute and storage.

    Primary abstraction for cloud providers, combining GPU compute
    and persistent storage operations. Implementations provide
    full lifecycle management for tasks and data.

    Additional Capabilities:
        - User identity resolution
        - Provider feature detection
        - Cross-service integration
    """

    def get_user(self, user_id: str) -> "User":
        """Resolve opaque user ID to identity information.

        Maps provider-specific user identifiers to human-readable
        information. Used for audit trails and collaboration features.

        Args:
            user_id: Provider user identifier.
                Format varies: "user_abc123", email, UUID.

        Returns:
            User: Identity information:
                - user_id: Same as input
                - username: Human-readable name
                - email: Contact email

        Raises:
            UserNotFoundError: Invalid or inaccessible user
            ProviderError: Identity service failure

        Privacy:
            Only returns users in same organization/project
        """
        ...

    def get_capabilities(self) -> "ProviderCapabilities":
        """Query provider feature support and limitations.

        Returns static capability descriptor enabling SDK to adapt
        behavior to provider-specific features and constraints.
        Used for graceful degradation and optimal feature usage.

        Returns:
            ProviderCapabilities: Feature flags and limits:
                - supports_spot_instances: Has preemptible compute
                - supports_persistent_volumes: Volume attachment
                - supports_ssh_access: Direct instance SSH
                - max_instances_per_task: Multi-node limit
                - max_volume_size_gb: Storage constraints
                - supported_regions: Available locations
                - gpu_types: Available accelerators

        Usage:
            Checked by SDK to conditionally enable features
            and provide appropriate error messages.
        """
        ...
