"""
dockrion Dockfile Schema v1.0

This module defines Pydantic models for validating Dockfile configurations.
It provides type-safe validation for all Dockfile sections.

Design Principles:
- Pure validation: Receives dicts, validates structure, returns typed objects
- No file I/O: File reading/writing is SDK's responsibility
- Extensible: Accepts unknown fields for future expansion
- Security-first: Critical validations prevent code injection

Usage:
    from dockrion_schema import DockSpec

    # SDK passes parsed dict to schema for validation
    data = {"version": "1.0", "agent": {...}, ...}
    spec = DockSpec.model_validate(data)
"""

import re
from typing import Any, Dict, List, Literal, Optional

# Import validation utilities and constants from common package
from dockrion_common import (
    LOG_LEVELS,
    PERMISSIONS,
    SUPPORTED_AUTH_MODES,
    SUPPORTED_DOCKFILE_VERSIONS,
    SUPPORTED_FRAMEWORKS,
    SUPPORTED_STREAMING,
    RuntimeDefaults,
    ValidationError,
    parse_rate_limit,
    validate_agent_name,
    validate_entrypoint,
    validate_handler,
    validate_port,
)
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
from typing_extensions import Self

# =============================================================================
# I/O SCHEMA MODELS
# =============================================================================


class IOSubSchema(BaseModel):
    """
    JSON Schema definition for input or output.

    Defines the structure of data that agents receive or return.
    Supports basic JSON Schema types: object, string, number, integer, boolean, array.

    Note: Properties should be valid JSON Schema definitions. Nested objects
    and arrays are supported through recursive schema definitions.
    """

    type: str = "object"  # Validated against JSON_SCHEMA_TYPES
    properties: Dict[str, Any] = {}  # Can contain nested schemas
    required: List[str] = []
    items: Optional[Dict[str, Any]] = None  # For array types
    description: Optional[str] = None

    model_config = ConfigDict(extra="allow")

    @field_validator("type")
    @classmethod
    def validate_type(cls, v: str) -> str:
        """Validate JSON Schema type is supported"""
        # Common JSON Schema types
        SUPPORTED_TYPES = ["object", "string", "number", "integer", "boolean", "array", "null"]
        if v not in SUPPORTED_TYPES:
            raise ValidationError(
                f"Unsupported JSON Schema type: '{v}'. "
                f"Supported types: {', '.join(SUPPORTED_TYPES)}"
            )
        return v

    @field_validator("properties")
    @classmethod
    def validate_properties(cls, v: Dict[str, Any]) -> Dict[str, Any]:
        """Validate properties are valid JSON Schema definitions"""
        if not isinstance(v, dict):
            raise ValidationError("properties must be a dictionary")

        # Basic validation: each property should have a type
        for prop_name, prop_schema in v.items():
            if not isinstance(prop_schema, dict):
                raise ValidationError(
                    f"Property '{prop_name}' must be a JSON Schema object (dict), got {type(prop_schema).__name__}"
                )

            # Validate property name is not empty
            if not prop_name or not prop_name.strip():
                raise ValidationError("Property names cannot be empty or whitespace")

            # If property has a type, validate it's supported
            if "type" in prop_schema:
                prop_type = prop_schema["type"]
                SUPPORTED_TYPES = [
                    "object",
                    "string",
                    "number",
                    "integer",
                    "boolean",
                    "array",
                    "null",
                ]
                if prop_type not in SUPPORTED_TYPES:
                    raise ValidationError(
                        f"Property '{prop_name}' has unsupported type: '{prop_type}'. "
                        f"Supported types: {', '.join(SUPPORTED_TYPES)}"
                    )

                # If type is array, should have items
                if prop_type == "array" and "items" not in prop_schema:
                    raise ValidationError(
                        f"Property '{prop_name}' is type 'array' but missing 'items' definition"
                    )

        return v

    @field_validator("required")
    @classmethod
    def validate_required_fields(cls, v: List[str], info) -> List[str]:
        """Validate required fields exist in properties"""
        if not isinstance(v, list):
            raise ValidationError("required must be a list")

        # Check for duplicates
        if len(v) != len(set(v)):
            duplicates = [item for item in v if v.count(item) > 1]
            raise ValidationError(f"Duplicate fields in required list: {duplicates}")

        # Note: We can't validate against properties here because properties
        # might not be set yet during validation. This will be checked in
        # model_validator if needed.

        return v

    @model_validator(mode="after")
    def validate_required_in_properties(self) -> Self:
        """Validate all required fields are defined in properties"""
        if self.type == "object" and self.required:
            # Only validate if we have properties defined
            if self.properties:
                for required_field in self.required:
                    if required_field not in self.properties:
                        raise ValidationError(
                            f"Required field '{required_field}' is not defined in properties. "
                            f"Available properties: {', '.join(self.properties.keys())}"
                        )
        return self

    @model_validator(mode="after")
    def validate_array_has_items(self) -> Self:
        """Validate array types have items definition"""
        if self.type == "array" and not self.items:
            raise ValidationError(
                "JSON Schema type 'array' requires 'items' field to define array element type"
            )
        return self


class IOSchema(BaseModel):
    """
    Input/Output schema for agent invocation.

    Defines the contract for what the agent accepts and returns.
    Runtime uses this to validate requests and format responses.
    """

    input: Optional[IOSubSchema] = None
    output: Optional[IOSubSchema] = None

    model_config = ConfigDict(extra="allow")


# =============================================================================
# AGENT CONFIGURATION
# =============================================================================


class AgentConfig(BaseModel):
    """
    Agent metadata and code location.

    Supports two invocation modes:

    1. **Entrypoint Mode** (Framework Agents):
       - Uses `entrypoint` field pointing to a factory function
       - Factory returns an agent object with `.invoke()` method
       - Requires `framework` field (langgraph, langchain, etc.)

    2. **Handler Mode** (Service Functions):
       - Uses `handler` field pointing to a direct callable
       - Callable receives payload dict, returns response dict
       - Framework defaults to "custom"

    At least one of `entrypoint` or `handler` must be provided.
    If both are provided, `handler` takes precedence for invocation.

    Examples:
        # Entrypoint mode (LangGraph agent)
        agent:
          name: my-agent
          entrypoint: app.graph:build_graph
          framework: langgraph

        # Handler mode (custom service)
        agent:
          name: my-service
          handler: app.service:process_request
          framework: custom  # optional, defaults to custom
    """

    name: str
    description: Optional[str] = None

    # Entrypoint mode: factory function returning agent with .invoke()
    entrypoint: Optional[str] = None

    # Handler mode: direct callable function(payload) -> response
    handler: Optional[str] = None

    # Framework (required for entrypoint, defaults to "custom" for handler)
    framework: Optional[str] = None

    model_config = ConfigDict(extra="allow")

    @field_validator("name")
    @classmethod
    def validate_name_format(cls, v: str) -> str:
        """Validate agent name format (lowercase, alphanumeric, hyphens)"""
        validate_agent_name(v)
        return v

    @field_validator("entrypoint")
    @classmethod
    def validate_entrypoint_format(cls, v: Optional[str]) -> Optional[str]:
        """
        Validate entrypoint format and prevent code injection.

        Format: 'module.path:callable'
        Prevents: os.system:eval, ../../../etc/passwd:read
        """
        if v is not None:
            validate_entrypoint(v)
        return v

    @field_validator("handler")
    @classmethod
    def validate_handler_format(cls, v: Optional[str]) -> Optional[str]:
        """
        Validate handler format.

        Format: 'module.path:callable'
        Handler must be a callable: def handler(payload: dict) -> dict
        """
        if v is not None:
            validate_handler(v)
        return v

    @field_validator("framework")
    @classmethod
    def validate_framework_supported(cls, v: Optional[str]) -> Optional[str]:
        """Validate framework is supported (uses SUPPORTED_FRAMEWORKS from common)"""
        if v is not None and v not in SUPPORTED_FRAMEWORKS:
            raise ValidationError(
                f"Unsupported framework: '{v}'. "
                f"Supported frameworks: {', '.join(SUPPORTED_FRAMEWORKS)}"
            )
        return v

    @model_validator(mode="after")
    def validate_entrypoint_or_handler(self) -> Self:
        """Ensure at least one of entrypoint or handler is provided."""
        if not self.entrypoint and not self.handler:
            raise ValidationError(
                "Agent must specify either 'entrypoint' (for framework agents) "
                "or 'handler' (for service functions). Neither was provided."
            )

        # Set default framework based on mode
        # Handler takes precedence when both are provided
        if self.framework is None:
            if self.handler:
                # Handler mode (or both specified): default to "custom"
                # When both are provided, handler takes precedence for invocation
                object.__setattr__(self, "framework", "custom")
            else:
                # Entrypoint-only mode requires explicit framework
                raise ValidationError(
                    "Agent with 'entrypoint' must specify 'framework'. "
                    f"Supported frameworks: {', '.join(SUPPORTED_FRAMEWORKS)}"
                )

        return self


# =============================================================================
# POLICY MODELS (Future - Phase 2)
# =============================================================================


class ToolPolicy(BaseModel):
    """
    Tool access control policy.

    NOTE: Policy enforcement happens in policy-engine package.
    Schema only validates the configuration.
    """

    allowed: List[str] = []
    deny_by_default: bool = True

    model_config = ConfigDict(extra="allow")


class SafetyPolicy(BaseModel):
    """
    Output safety and content filtering policy.

    NOTE: Redaction and filtering happen in policy-engine package.
    Schema only validates the configuration.
    """

    redact_patterns: List[str] = []
    max_output_chars: Optional[int] = None
    block_prompt_injection: bool = True
    halt_on_violation: bool = False

    model_config = ConfigDict(extra="allow")

    @field_validator("max_output_chars")
    @classmethod
    def validate_max_output_positive(cls, v: Optional[int]) -> Optional[int]:
        """Validate max_output_chars is positive"""
        if v is not None and v <= 0:
            raise ValidationError(f"max_output_chars must be positive. Got: {v}")
        return v


class Policies(BaseModel):
    """
    Security and safety policies.

    NOTE: These are optional in MVP. When policy-engine service is ready,
    these will be enforced at runtime.
    """

    tools: Optional[ToolPolicy] = None
    safety: Optional[SafetyPolicy] = None

    model_config = ConfigDict(extra="allow")


# =============================================================================
# AUTH CONFIGURATION
# =============================================================================


class RoleConfig(BaseModel):
    """
    Role-based access control configuration.

    Defines a named role with associated permissions.
    Roles can be assigned to API keys or extracted from JWT claims.

    Example:
        ```yaml
        roles:
          - name: admin
            permissions: [invoke, view_metrics, deploy, rollback]
          - name: operator
            permissions: [invoke, view_metrics]
        ```
    """

    name: str
    permissions: List[str]

    model_config = ConfigDict(extra="allow")

    @field_validator("permissions")
    @classmethod
    def validate_permissions(cls, v: List[str]) -> List[str]:
        """Validate all permissions are recognized"""
        for perm in v:
            if perm not in PERMISSIONS:
                raise ValidationError(
                    f"Unknown permission: '{perm}'. Valid permissions: {', '.join(PERMISSIONS)}"
                )
        return v


class ApiKeysConfig(BaseModel):
    """
    API key authentication configuration.

    Supports two modes:
    - Single key: One key from env_var
    - Multi-key: Multiple keys matching prefix pattern

    Example (single key):
        ```yaml
        api_keys:
          env_var: MY_AGENT_KEY
          header: X-API-Key
        ```

    Example (multi-key):
        ```yaml
        api_keys:
          prefix: AGENT_KEY_  # Loads AGENT_KEY_PROD, AGENT_KEY_DEV, etc.
          header: X-API-Key
        ```
    """

    # Key source
    env_var: str = "DOCKRION_API_KEY"
    prefix: Optional[str] = None  # For multi-key mode

    # Request config
    header: str = "X-API-Key"
    allow_bearer: bool = True  # Allow Authorization: Bearer <key>

    # Key management (informational)
    enabled: bool = True
    rotation_days: Optional[int] = 30

    model_config = ConfigDict(extra="allow")

    @field_validator("rotation_days")
    @classmethod
    def validate_rotation_days_positive(cls, v: Optional[int]) -> Optional[int]:
        """Validate rotation_days is positive"""
        if v is not None and v <= 0:
            raise ValidationError(f"rotation_days must be positive. Got: {v}")
        return v

    @field_validator("header")
    @classmethod
    def validate_header_name(cls, v: str) -> str:
        """Validate header name is reasonable"""
        if not v or len(v) > 64:
            raise ValidationError("Header name must be 1-64 characters")
        return v


class JWTClaimsConfig(BaseModel):
    """
    JWT claim mapping configuration.

    Maps JWT claims to identity context fields.
    Supports nested claim paths with dot notation.

    Example:
        ```yaml
        claims:
          user_id: sub
          email: email
          roles: permissions
          tenant_id: org.tenant_id  # Nested claim
        ```
    """

    user_id: str = "sub"
    email: str = "email"
    name: str = "name"
    roles: str = "roles"
    permissions: str = "permissions"
    scopes: str = "scope"
    tenant_id: Optional[str] = None

    model_config = ConfigDict(extra="allow")


class JWTConfig(BaseModel):
    """
    JWT authentication configuration.

    Supports JWKS (recommended) or static public key.

    Example (JWKS - recommended):
        ```yaml
        jwt:
          jwks_url: https://auth.company.com/.well-known/jwks.json
          issuer: https://auth.company.com/
          audience: my-agent-api
          claims:
            user_id: sub
            roles: permissions
        ```

    Example (static key):
        ```yaml
        jwt:
          public_key_env: JWT_PUBLIC_KEY
          issuer: my-issuer
          audience: my-agent-api
        ```
    """

    # Key source (one required)
    jwks_url: Optional[str] = None
    public_key_env: Optional[str] = None

    # Validation
    issuer: Optional[str] = None
    audience: Optional[str] = None
    algorithms: List[str] = ["RS256"]
    leeway_seconds: int = 30  # Clock skew tolerance

    # Claim mappings
    claims: Optional[JWTClaimsConfig] = None

    model_config = ConfigDict(extra="allow")

    @field_validator("algorithms")
    @classmethod
    def validate_algorithms(cls, v: List[str]) -> List[str]:
        """Validate algorithms are supported"""
        supported = [
            "RS256",
            "RS384",
            "RS512",
            "ES256",
            "ES384",
            "ES512",
            "HS256",
            "HS384",
            "HS512",
        ]
        for alg in v:
            if alg not in supported:
                raise ValidationError(
                    f"Unsupported algorithm: '{alg}'. Supported: {', '.join(supported)}"
                )
        return v

    @field_validator("leeway_seconds")
    @classmethod
    def validate_leeway(cls, v: int) -> int:
        """Validate leeway is reasonable"""
        if v < 0 or v > 300:
            raise ValidationError("leeway_seconds must be 0-300")
        return v


class OAuth2Config(BaseModel):
    """
    OAuth2 token introspection configuration.

    Used for validating opaque tokens by calling the authorization server.

    Example:
        ```yaml
        oauth2:
          introspection_url: https://auth.company.com/oauth/introspect
          client_id_env: AGENT_CLIENT_ID
          client_secret_env: AGENT_CLIENT_SECRET
          required_scopes: [agent:invoke]
        ```

    Note: This is a future feature planned for Phase 2.
    """

    introspection_url: Optional[str] = None
    client_id_env: Optional[str] = None
    client_secret_env: Optional[str] = None
    required_scopes: List[str] = []

    model_config = ConfigDict(extra="allow")


class AuthConfig(BaseModel):
    """
    Authentication and authorization configuration.

    Supports multiple authentication modes:
    - **none**: No authentication (development/trusted networks)
    - **api_key**: API key authentication (single or multi-key)
    - **jwt**: JWT with JWKS support (enterprise SSO)
    - **oauth2**: OAuth2 token introspection (future)

    Example (API Key - simple):
        ```yaml
        auth:
          mode: api_key
        ```

    Example (JWT - enterprise):
        ```yaml
        auth:
          mode: jwt
          jwt:
            jwks_url: https://auth.company.com/.well-known/jwks.json
            issuer: https://auth.company.com/
            audience: my-agent-api
            claims:
              user_id: sub
              roles: permissions
          roles:
            - name: admin
              permissions: [invoke, view_metrics, deploy]
            - name: operator
              permissions: [invoke, view_metrics]
          rate_limits:
            admin: "1000/minute"
            operator: "100/minute"
        ```
    """

    # Auth mode
    mode: str = "api_key"

    # Mode-specific configuration
    api_keys: Optional[ApiKeysConfig] = None
    jwt: Optional[JWTConfig] = None
    oauth2: Optional[OAuth2Config] = None

    # RBAC
    roles: List[RoleConfig] = []

    # Rate limiting by role
    rate_limits: Dict[str, str] = {}

    model_config = ConfigDict(extra="allow")

    @field_validator("mode")
    @classmethod
    def validate_auth_mode_supported(cls, v: str) -> str:
        """Validate auth mode is supported (uses SUPPORTED_AUTH_MODES from common)"""
        if v not in SUPPORTED_AUTH_MODES:
            raise ValidationError(
                f"Unsupported auth mode: '{v}'. Supported modes: {', '.join(SUPPORTED_AUTH_MODES)}"
            )
        return v

    @field_validator("rate_limits")
    @classmethod
    def validate_rate_limit_formats(cls, v: Dict[str, str]) -> Dict[str, str]:
        """Validate all rate limit strings are properly formatted"""
        for role, limit_str in v.items():
            try:
                parse_rate_limit(limit_str)
            except ValidationError as e:
                raise ValidationError(f"Invalid rate limit for role '{role}': {e.message}")
        return v


# =============================================================================
# OBSERVABILITY CONFIGURATION (Future - Phase 2)
# =============================================================================


class Observability(BaseModel):
    """
    Telemetry and monitoring configuration.

    NOTE: These are optional in MVP. When telemetry is fully integrated,
    these settings control logging and metrics collection.

    Note: log_level field uses constants from common package (LOG_LEVELS)
    as the single source of truth for validation.
    """

    langfuse: Optional[Dict[str, str]] = None
    tracing: bool = True
    log_level: str = "info"  # Validated against LOG_LEVELS from common
    metrics: Dict[str, bool] = {"latency": True, "tokens": True, "cost": True}

    model_config = ConfigDict(extra="allow")

    @field_validator("log_level")
    @classmethod
    def validate_log_level(cls, v: str) -> str:
        """Validate log level is recognized (uses LOG_LEVELS from common)"""
        if v not in LOG_LEVELS:
            raise ValidationError(
                f"Invalid log level: '{v}'. Valid levels: {', '.join(LOG_LEVELS)}"
            )
        return v


# =============================================================================
# EXPOSE CONFIGURATION
# =============================================================================


class ExposeConfig(BaseModel):
    """
    API exposure and network configuration.

    Controls how the agent runtime exposes APIs (REST, streaming).

    Note: All defaults use RuntimeDefaults from common package
    as the single source of truth.
    """

    rest: bool = True
    streaming: str = RuntimeDefaults.STREAMING
    port: int = RuntimeDefaults.PORT
    host: str = RuntimeDefaults.HOST
    cors: Optional[Dict[str, List[str]]] = None

    model_config = ConfigDict(extra="allow")

    @field_validator("port")
    @classmethod
    def validate_port_range(cls, v: int) -> int:
        """Validate port is in valid range (1-65535)"""
        validate_port(v)
        return v

    @field_validator("streaming")
    @classmethod
    def validate_streaming_mode(cls, v: str) -> str:
        """Validate streaming mode is supported (uses SUPPORTED_STREAMING from common)"""
        if v not in SUPPORTED_STREAMING:
            raise ValidationError(
                f"Unsupported streaming mode: '{v}'. "
                f"Supported modes: {', '.join(SUPPORTED_STREAMING)}"
            )
        return v

    @model_validator(mode="after")
    def validate_at_least_one_exposure(self) -> Self:
        """Validate at least REST or streaming is enabled"""
        if not self.rest and self.streaming == "none":
            raise ValidationError(
                "At least one exposure method must be enabled. "
                "Either set rest=true or streaming to 'sse' or 'websocket'"
            )
        return self


# =============================================================================
# SECRETS CONFIGURATION
# =============================================================================


class SecretDefinition(BaseModel):
    """
    Definition of a secret/environment variable.

    Used to declare what secrets an agent requires at runtime.
    This enables validation before deployment and clear documentation
    of required configuration.

    Example:
        ```yaml
        secrets:
          required:
            - name: OPENAI_API_KEY
              description: "OpenAI API key for LLM calls"
            - name: MY_AGENT_KEY
              description: "API key for agent authentication"
          optional:
            - name: LANGFUSE_SECRET
              description: "Langfuse telemetry"
              default: ""
        ```
    """

    name: str
    description: Optional[str] = None
    default: Optional[str] = None

    model_config = ConfigDict(extra="allow")

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Validate secret name follows environment variable conventions."""
        if not v or not v.strip():
            raise ValidationError("Secret name cannot be empty")
        # Allow uppercase letters, numbers, and underscores
        # Must start with a letter or underscore
        if not re.match(r"^[A-Z_][A-Z0-9_]*$", v):
            raise ValidationError(
                f"Secret name '{v}' must be uppercase with underscores "
                f"(e.g., MY_API_KEY, OPENAI_KEY_1). "
                f"Must start with a letter or underscore."
            )
        return v


class SecretsConfig(BaseModel):
    """
    Configuration for required and optional secrets.

    Secrets declared here are used for:
    - Validation before run/build to ensure required vars are set
    - Documentation of what environment variables the agent needs
    - Auto-loading from .env files with priority resolution

    Example:
        ```yaml
        secrets:
          required:
            - name: OPENAI_API_KEY
              description: "OpenAI API key for LLM calls"
          optional:
            - name: DEBUG_MODE
              description: "Enable debug logging"
              default: "false"
        ```
    """

    required: List[SecretDefinition] = []
    optional: List[SecretDefinition] = []

    model_config = ConfigDict(extra="allow")

    @model_validator(mode="after")
    def validate_no_duplicate_names(self) -> Self:
        """Ensure no duplicate secret names across required and optional."""
        all_names = [s.name for s in self.required] + [s.name for s in self.optional]
        seen = set()
        duplicates = []
        for name in all_names:
            if name in seen:
                duplicates.append(name)
            seen.add(name)

        if duplicates:
            raise ValidationError(
                f"Duplicate secret names found: {', '.join(duplicates)}. "
                f"Each secret must have a unique name."
            )
        return self


# =============================================================================
# METADATA
# =============================================================================


class Metadata(BaseModel):
    """
    Optional descriptive metadata about the agent.

    Used for documentation and organization purposes.
    """

    maintainer: Optional[str] = None
    version: Optional[str] = None
    tags: List[str] = []

    model_config = ConfigDict(extra="allow")


# =============================================================================
# ROOT DOCKSPEC MODEL
# =============================================================================


class DockSpec(BaseModel):
    """
    Root model for Dockfile v1.0 specification.

    This is the main entry point for validating Dockfile configurations.
    All services use this model to ensure consistent validation.

    Design:
    - Accepts unknown fields (extra="allow") for future extensibility
    - MVP fields are required/validated, future fields are accepted but not validated
    - When new services are ready, corresponding models are added and validated

    Usage:
        # SDK passes parsed YAML dict to schema
        data = yaml.safe_load(file_content)
        spec = DockSpec.model_validate(data)

        # Access validated fields
        agent_name = spec.agent.name
        framework = spec.agent.framework
    """

    version: Literal["1.0"]
    agent: AgentConfig
    io_schema: IOSchema
    arguments: Dict[str, Any] = {}
    policies: Optional[Policies] = None
    auth: Optional[AuthConfig] = None
    observability: Optional[Observability] = None
    expose: ExposeConfig
    metadata: Optional[Metadata] = None
    secrets: Optional[SecretsConfig] = None

    # Allow unknown fields for future expansion (Phase 2+)
    # When new services are built, add their models above and make them optional
    model_config = ConfigDict(extra="allow")

    @field_validator("version")
    @classmethod
    def validate_version_supported(cls, v: str) -> str:
        """Validate Dockfile version is supported"""
        if v not in SUPPORTED_DOCKFILE_VERSIONS:
            raise ValidationError(
                f"Unsupported Dockfile version: '{v}'. "
                f"Supported versions: {', '.join(SUPPORTED_DOCKFILE_VERSIONS)}"
            )
        return v
