"""Pydantic models for VS Code devcontainer.json specification.

This module provides models for parsing and generating devcontainer.json files,
enabling bidirectional conversion with gaol container specifications.

Reference: https://containers.dev/implementors/json_reference/
"""

from __future__ import annotations

import re
from typing import Any

from pydantic import BaseModel, Field, field_validator, model_validator


def to_camel_case(snake_str: str) -> str:
    """Convert snake_case to camelCase."""
    components = snake_str.split("_")
    return components[0] + "".join(x.title() for x in components[1:])


class DevcontainerMount(BaseModel):
    """Parsed mount specification.

    Represents a volume or bind mount in devcontainer format.
    """

    type: str = "bind"  # bind, volume, tmpfs
    source: str
    target: str
    readonly: bool = False

    def to_string(self) -> str:
        """Convert to devcontainer mount string format."""
        parts = [f"type={self.type}", f"source={self.source}", f"target={self.target}"]
        if self.readonly:
            parts.append("readonly")
        return ",".join(parts)

    @classmethod
    def from_string(cls, mount_str: str) -> DevcontainerMount:
        """Parse a mount string into DevcontainerMount.

        Supports formats like:
        - "source=/path,target=/container/path,type=bind"
        - "source=${localEnv:HOME}/projects,target=/workspace,type=bind,readonly"
        """
        parts = {}
        for part in mount_str.split(","):
            if "=" in part:
                key, value = part.split("=", 1)
                parts[key] = value
            elif part == "readonly":
                parts["readonly"] = True

        return cls(
            type=parts.get("type", "bind"),
            source=parts.get("source", ""),
            target=parts.get("target", ""),
            readonly=parts.get("readonly", False),
        )


class DevcontainerBuild(BaseModel):
    """Dockerfile build configuration.

    Used when the devcontainer is built from a Dockerfile rather than
    using a pre-built image.
    """

    dockerfile: str = "Dockerfile"
    context: str = "."
    args: dict[str, str] = Field(default_factory=dict)
    target: str | None = None
    cache_from: list[str] = Field(default_factory=list, alias="cacheFrom")

    model_config = {"populate_by_name": True}

    def to_json(self) -> dict[str, Any]:
        """Convert to JSON-serializable dict with camelCase keys."""
        result: dict[str, Any] = {
            "dockerfile": self.dockerfile,
            "context": self.context,
        }
        if self.args:
            result["args"] = self.args
        if self.target:
            result["target"] = self.target
        if self.cache_from:
            result["cacheFrom"] = self.cache_from
        return result


class DevcontainerVSCodeCustomizations(BaseModel):
    """VS Code specific customizations."""

    extensions: list[str] = Field(default_factory=list)
    settings: dict[str, Any] = Field(default_factory=dict)

    def to_json(self) -> dict[str, Any]:
        """Convert to JSON-serializable dict."""
        result: dict[str, Any] = {}
        if self.extensions:
            result["extensions"] = self.extensions
        if self.settings:
            result["settings"] = self.settings
        return result


class DevcontainerCustomizations(BaseModel):
    """IDE customizations section.

    Holds customizations for various IDEs, primarily VS Code.
    """

    vscode: DevcontainerVSCodeCustomizations = Field(
        default_factory=DevcontainerVSCodeCustomizations
    )

    def to_json(self) -> dict[str, Any]:
        """Convert to JSON-serializable dict."""
        result: dict[str, Any] = {}
        vscode_json = self.vscode.to_json()
        if vscode_json:
            result["vscode"] = vscode_json
        return result


class DevcontainerPortAttributes(BaseModel):
    """Port attributes for a forwarded port."""

    label: str | None = None
    protocol: str = "http"  # http, https
    on_auto_forward: str = Field(default="notify", alias="onAutoForward")
    requires_local_port: bool = Field(default=False, alias="requiresLocalPort")
    elevate_if_needed: bool = Field(default=False, alias="elevateIfNeeded")

    model_config = {"populate_by_name": True}

    def to_json(self) -> dict[str, Any]:
        """Convert to JSON-serializable dict with camelCase keys."""
        result: dict[str, Any] = {}
        if self.label:
            result["label"] = self.label
        if self.protocol != "http":
            result["protocol"] = self.protocol
        if self.on_auto_forward != "notify":
            result["onAutoForward"] = self.on_auto_forward
        if self.requires_local_port:
            result["requiresLocalPort"] = self.requires_local_port
        if self.elevate_if_needed:
            result["elevateIfNeeded"] = self.elevate_if_needed
        return result


class DevcontainerSpec(BaseModel):
    """Parsed devcontainer.json specification.

    This model represents the full devcontainer.json format, supporting
    both image-based and Dockerfile-based containers.

    Reference: https://containers.dev/implementors/json_reference/
    """

    # Basic identification
    name: str | None = None

    # Image or build (mutually exclusive in practice)
    image: str | None = None
    build: DevcontainerBuild | None = None

    # Dev Container Features
    features: dict[str, dict[str, Any]] = Field(default_factory=dict)

    # Port forwarding
    forward_ports: list[int] = Field(default_factory=list, alias="forwardPorts")
    ports_attributes: dict[str, DevcontainerPortAttributes] = Field(
        default_factory=dict, alias="portsAttributes"
    )
    other_ports_attributes: DevcontainerPortAttributes | None = Field(
        default=None, alias="otherPortsAttributes"
    )

    # Mounts
    mounts: list[str | DevcontainerMount] = Field(default_factory=list)

    # Environment variables
    container_env: dict[str, str] = Field(default_factory=dict, alias="containerEnv")
    remote_env: dict[str, str] = Field(default_factory=dict, alias="remoteEnv")

    # User configuration
    remote_user: str | None = Field(default=None, alias="remoteUser")
    container_user: str | None = Field(default=None, alias="containerUser")
    update_remote_user_uid: bool = Field(default=True, alias="updateRemoteUserUID")

    # Lifecycle commands (can be string, list of strings, or dict)
    init_command: str | list[str] | dict[str, str | list[str]] | None = Field(
        default=None, alias="initializeCommand"
    )
    on_create_command: str | list[str] | dict[str, str | list[str]] | None = Field(
        default=None, alias="onCreateCommand"
    )
    update_content_command: str | list[str] | dict[str, str | list[str]] | None = Field(
        default=None, alias="updateContentCommand"
    )
    post_create_command: str | list[str] | dict[str, str | list[str]] | None = Field(
        default=None, alias="postCreateCommand"
    )
    post_start_command: str | list[str] | dict[str, str | list[str]] | None = Field(
        default=None, alias="postStartCommand"
    )
    post_attach_command: str | list[str] | dict[str, str | list[str]] | None = Field(
        default=None, alias="postAttachCommand"
    )

    # Customizations
    customizations: DevcontainerCustomizations = Field(
        default_factory=DevcontainerCustomizations
    )

    # Workspace
    workspace_folder: str | None = Field(default=None, alias="workspaceFolder")
    workspace_mount: str | None = Field(default=None, alias="workspaceMount")

    # Container runtime options
    run_args: list[str] = Field(default_factory=list, alias="runArgs")
    override_command: bool = Field(default=True, alias="overrideCommand")
    shutdown_action: str = Field(default="stopContainer", alias="shutdownAction")
    privileged: bool = False
    cap_add: list[str] = Field(default_factory=list, alias="capAdd")
    security_opt: list[str] = Field(default_factory=list, alias="securityOpt")

    # Host requirements
    host_requirements: dict[str, Any] = Field(
        default_factory=dict, alias="hostRequirements"
    )

    model_config = {"populate_by_name": True}

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str | None) -> str | None:
        """Validate name is not empty if provided."""
        if v is not None and not v.strip():
            raise ValueError("name cannot be empty")
        return v

    @model_validator(mode="after")
    def validate_image_or_build(self) -> DevcontainerSpec:
        """Validate that either image or build is specified (or neither for compose)."""
        # Both can be absent if using docker-compose
        # Both should not be present at the same time
        if self.image and self.build:
            raise ValueError("Cannot specify both 'image' and 'build'")
        return self

    def to_json(self) -> dict[str, Any]:
        """Convert to devcontainer.json format with camelCase keys.

        Returns a dictionary ready for JSON serialization.
        """
        result: dict[str, Any] = {}

        # Basic fields
        if self.name:
            result["name"] = self.name

        # Image or build
        if self.image:
            result["image"] = self.image
        if self.build:
            result["build"] = self.build.to_json()

        # Features
        if self.features:
            result["features"] = self.features

        # Port forwarding
        if self.forward_ports:
            result["forwardPorts"] = self.forward_ports
        if self.ports_attributes:
            result["portsAttributes"] = {
                k: v.to_json() for k, v in self.ports_attributes.items()
            }
        if self.other_ports_attributes:
            result["otherPortsAttributes"] = self.other_ports_attributes.to_json()

        # Mounts
        if self.mounts:
            result["mounts"] = [
                m.to_string() if isinstance(m, DevcontainerMount) else m
                for m in self.mounts
            ]

        # Environment
        if self.container_env:
            result["containerEnv"] = self.container_env
        if self.remote_env:
            result["remoteEnv"] = self.remote_env

        # User configuration
        if self.remote_user:
            result["remoteUser"] = self.remote_user
        if self.container_user:
            result["containerUser"] = self.container_user
        if not self.update_remote_user_uid:
            result["updateRemoteUserUID"] = False

        # Lifecycle commands
        if self.init_command:
            result["initializeCommand"] = self.init_command
        if self.on_create_command:
            result["onCreateCommand"] = self.on_create_command
        if self.update_content_command:
            result["updateContentCommand"] = self.update_content_command
        if self.post_create_command:
            result["postCreateCommand"] = self.post_create_command
        if self.post_start_command:
            result["postStartCommand"] = self.post_start_command
        if self.post_attach_command:
            result["postAttachCommand"] = self.post_attach_command

        # Customizations
        customizations_json = self.customizations.to_json()
        if customizations_json:
            result["customizations"] = customizations_json

        # Workspace
        if self.workspace_folder:
            result["workspaceFolder"] = self.workspace_folder
        if self.workspace_mount:
            result["workspaceMount"] = self.workspace_mount

        # Runtime options
        if self.run_args:
            result["runArgs"] = self.run_args
        if not self.override_command:
            result["overrideCommand"] = False
        if self.shutdown_action != "stopContainer":
            result["shutdownAction"] = self.shutdown_action
        if self.privileged:
            result["privileged"] = True
        if self.cap_add:
            result["capAdd"] = self.cap_add
        if self.security_opt:
            result["securityOpt"] = self.security_opt

        # Host requirements
        if self.host_requirements:
            result["hostRequirements"] = self.host_requirements

        return result

    @classmethod
    def from_json(cls, data: dict[str, Any]) -> DevcontainerSpec:
        """Create DevcontainerSpec from parsed JSON data.

        Handles camelCase to snake_case conversion.
        """
        # Parse build if present
        build = None
        if "build" in data:
            build_data = data["build"]
            if isinstance(build_data, dict):
                build = DevcontainerBuild(
                    dockerfile=build_data.get("dockerfile", "Dockerfile"),
                    context=build_data.get("context", "."),
                    args=build_data.get("args", {}),
                    target=build_data.get("target"),
                    cache_from=build_data.get("cacheFrom", []),
                )

        # Parse customizations if present
        customizations = DevcontainerCustomizations()
        if "customizations" in data:
            custom_data = data["customizations"]
            if isinstance(custom_data, dict) and "vscode" in custom_data:
                vscode_data = custom_data["vscode"]
                customizations = DevcontainerCustomizations(
                    vscode=DevcontainerVSCodeCustomizations(
                        extensions=vscode_data.get("extensions", []),
                        settings=vscode_data.get("settings", {}),
                    )
                )

        # Parse port attributes if present
        ports_attributes = {}
        if "portsAttributes" in data:
            for port, attrs in data["portsAttributes"].items():
                ports_attributes[port] = DevcontainerPortAttributes(
                    label=attrs.get("label"),
                    protocol=attrs.get("protocol", "http"),
                    on_auto_forward=attrs.get("onAutoForward", "notify"),
                    requires_local_port=attrs.get("requiresLocalPort", False),
                    elevate_if_needed=attrs.get("elevateIfNeeded", False),
                )

        # Parse mounts
        mounts: list[str | DevcontainerMount] = []
        for mount in data.get("mounts", []):
            if isinstance(mount, str):
                # Keep as string or parse it
                if "=" in mount:
                    mounts.append(DevcontainerMount.from_string(mount))
                else:
                    mounts.append(mount)
            elif isinstance(mount, dict):
                mounts.append(
                    DevcontainerMount(
                        type=mount.get("type", "bind"),
                        source=mount.get("source", ""),
                        target=mount.get("target", ""),
                        readonly=mount.get("readonly", False),
                    )
                )

        return cls(
            name=data.get("name"),
            image=data.get("image"),
            build=build,
            features=data.get("features", {}),
            forward_ports=data.get("forwardPorts", []),
            ports_attributes=ports_attributes,
            mounts=mounts,
            container_env=data.get("containerEnv", {}),
            remote_env=data.get("remoteEnv", {}),
            remote_user=data.get("remoteUser"),
            container_user=data.get("containerUser"),
            update_remote_user_uid=data.get("updateRemoteUserUID", True),
            init_command=data.get("initializeCommand"),
            on_create_command=data.get("onCreateCommand"),
            update_content_command=data.get("updateContentCommand"),
            post_create_command=data.get("postCreateCommand"),
            post_start_command=data.get("postStartCommand"),
            post_attach_command=data.get("postAttachCommand"),
            customizations=customizations,
            workspace_folder=data.get("workspaceFolder"),
            workspace_mount=data.get("workspaceMount"),
            run_args=data.get("runArgs", []),
            override_command=data.get("overrideCommand", True),
            shutdown_action=data.get("shutdownAction", "stopContainer"),
            privileged=data.get("privileged", False),
            cap_add=data.get("capAdd", []),
            security_opt=data.get("securityOpt", []),
            host_requirements=data.get("hostRequirements", {}),
        )

    def get_all_environment(self) -> dict[str, str]:
        """Get merged environment variables (containerEnv + remoteEnv)."""
        env = dict(self.container_env)
        env.update(self.remote_env)
        return env

    def get_extensions(self) -> list[str]:
        """Get VS Code extensions list."""
        return self.customizations.vscode.extensions

    def get_settings(self) -> dict[str, Any]:
        """Get VS Code settings."""
        return self.customizations.vscode.settings
