import re
import tomllib
import warnings
from typing import Any

import toml
from pydantic import BaseModel, Field, field_validator, model_validator

from harbor.constants import ORG_NAME_PATTERN


class Author(BaseModel):
    """Author information for a package or dataset."""

    name: str = Field(..., description="Author name")
    email: str | None = Field(default=None, description="Author email address")


class PackageInfo(BaseModel):
    """Package metadata for the [task] section of task.toml.

    This section identifies the package in the registry with a unique name.
    """

    name: str = Field(
        ...,
        description="Package name in org/name format (e.g., 'harbor/hello-world')",
    )
    description: str | None = Field(
        default=None,
        description="Human-readable description of the task",
    )
    authors: list[Author] = Field(
        default_factory=list,
        description="List of package authors",
    )
    keywords: list[str] = Field(
        default_factory=list,
        description="Keywords for search and categorization",
    )

    @field_validator("name")
    @classmethod
    def validate_name_format(cls, v: str) -> str:
        """Validate that name follows org/name format."""
        if not re.match(ORG_NAME_PATTERN, v) or ".." in v:
            raise ValueError(
                f"Package name must be in 'org/name' format with alphanumeric characters, "
                f"hyphens, underscores, and dots. Cannot start with a dot or contain '..'. Got: {v}"
            )
        return v

    @property
    def org(self) -> str:
        """Extract organization from package name."""
        return self.name.split("/")[0]

    @property
    def short_name(self) -> str:
        """Extract short name (without org) from package name."""
        return self.name.split("/")[1]


class VerifierConfig(BaseModel):
    timeout_sec: float = 600.0
    env: dict[str, str] = Field(default_factory=dict)


class SolutionConfig(BaseModel):
    env: dict[str, str] = Field(default_factory=dict)


class AgentConfig(BaseModel):
    timeout_sec: float | None = None


class EnvironmentConfig(BaseModel):
    build_timeout_sec: float = 600.0  # 10 minutes default
    docker_image: str | None = None
    cpus: int = 1
    memory_mb: int = 2048
    storage_mb: int = 10240
    gpus: int = 0
    gpu_types: list[str] | None = Field(
        default=None,
        description="List of acceptable GPU types (e.g., ['H100', 'A100', 'T4']). None "
        "means any GPU type is acceptable.",
    )
    allow_internet: bool = Field(
        default=True,
        description="Whether to allow internet access in the environment.",
    )
    mcp_servers: list["MCPServerConfig"] = Field(default_factory=list)
    env: dict[str, str] = Field(
        default_factory=dict,
        description="Environment variables required for the task and resolved from the host at runtime. "
        "Supports ${VAR} and ${VAR:-default} template syntax.",
    )
    skills_dir: str | None = Field(
        default=None,
        description="Path to skills directory in the environment. "
        "Contents are copied to the agent's skills config directory.",
    )

    # Deprecated fields - marked as excluded so they don't appear in serialization by default
    memory: str | None = Field(
        default=None,
        deprecated="Use 'memory_mb' instead. This field will be removed in a future version.",
        exclude=True,
    )
    storage: str | None = Field(
        default=None,
        deprecated="Use 'storage_mb' instead. This field will be removed in a future version.",
        exclude=True,
    )

    @staticmethod
    def _parse_size_to_mb(size_str: str) -> int:
        size_str = size_str.strip().upper()

        if size_str.endswith("G"):
            return int(float(size_str[:-1]) * 1024)
        elif size_str.endswith("M"):
            return int(float(size_str[:-1]))
        elif size_str.endswith("K"):
            return int(float(size_str[:-1]) / 1024)
        else:
            raise ValueError(
                f"Invalid size format: {size_str}. Expected format like '1G', "
                "'512M', etc."
            )

    @model_validator(mode="after")
    def handle_deprecated_fields(self) -> "EnvironmentConfig":
        """Map deprecated memory/storage fields to new memory_mb/storage_mb fields."""
        if self.memory is not None:
            warnings.warn(
                "The 'memory' field is deprecated. Use 'memory_mb' instead.",
                DeprecationWarning,
                stacklevel=2,
            )
            self.memory_mb = self._parse_size_to_mb(self.memory)
            self.memory = None

        if self.storage is not None:
            warnings.warn(
                "The 'storage' field is deprecated. Use 'storage_mb' instead.",
                DeprecationWarning,
                stacklevel=2,
            )
            self.storage_mb = self._parse_size_to_mb(self.storage)
            self.storage = None

        return self


class MCPServerConfig(BaseModel):
    """Configuration for an MCP server available to the agent."""

    name: str
    transport: str = "sse"  # "sse" | "streamable-http" | "stdio"
    url: str | None = None  # required for sse/streamable-http
    command: str | None = None  # for stdio
    args: list[str] = Field(default_factory=list)  # for stdio

    @model_validator(mode="after")
    def validate_transport_fields(self) -> "MCPServerConfig":
        if self.transport in ("sse", "streamable-http") and not self.url:
            raise ValueError(f"'url' is required for transport '{self.transport}'")
        if self.transport == "stdio" and not self.command:
            raise ValueError("'command' is required for transport 'stdio'")
        return self


class TaskConfig(BaseModel):
    version: str = "1.0"
    task: PackageInfo | None = Field(
        default=None,
        description="Package information for the task, parsed from the [task] section of task.toml.",
    )
    metadata: dict[str, Any] = Field(default_factory=dict)
    verifier: VerifierConfig = Field(default_factory=VerifierConfig)
    agent: AgentConfig = Field(default_factory=AgentConfig)
    environment: EnvironmentConfig = Field(default_factory=EnvironmentConfig)
    solution: SolutionConfig = Field(default_factory=SolutionConfig)
    source: str | None = None

    @classmethod
    def model_validate_toml(cls, toml_data: str) -> "TaskConfig":
        toml_dict = tomllib.loads(toml_data)
        return cls.model_validate(toml_dict)

    def model_dump_toml(self) -> str:
        return toml.dumps(self.model_dump(mode="json"))
