"""Configuration helpers for the Docker worker backend."""

from __future__ import annotations

import os
import re
from dataclasses import dataclass
from pathlib import PurePosixPath
from typing import TYPE_CHECKING

from mindroom.constants import (
    RuntimePaths,
    resolve_config_relative_path,
    resolve_primary_runtime_paths,
    runtime_env_values,
    runtime_paths_with_config_path,
    runtime_paths_with_storage_root,
)
from mindroom.credentials import runtime_credentials_manager_key
from mindroom.runtime_env_policy import KUBERNETES_WORKER_BACKEND_CONFIG_ENV_BY_KEY, SANDBOX_RUNTIME_ENV_BY_KEY
from mindroom.tool_system.worker_routing import worker_root_path
from mindroom.workers.backend import WorkerBackendError
from mindroom.workers.backends._config_helpers import (
    read_env,
    read_float_env,
    read_int_env,
    read_json_mapping_env,
)
from mindroom.workers.backends._dedicated_worker_common import (
    build_backend_config_signature,
    validate_dedicated_worker_extra_env,
)

if TYPE_CHECKING:
    from collections.abc import Mapping
    from pathlib import Path

__all__ = [
    "DEFAULT_WORKER_PORT",
    "DOCKER_RESERVED_EXTRA_ENV_NAMES",
    "DockerWorkerBackendConfig",
    "docker_backend_config_signature",
    "docker_workers_root",
    "normalize_docker_name_prefix",
    "resolve_docker_storage_path",
    "validate_docker_endpoint_host",
    "validate_docker_extra_labels",
    "validate_docker_mount_layout",
]

_DEFAULT_IDLE_TIMEOUT_SECONDS = 1800.0
_DEFAULT_READY_TIMEOUT_SECONDS = 60.0
DEFAULT_WORKER_PORT = 8766
_DEFAULT_WORKER_PORT = DEFAULT_WORKER_PORT
_DEFAULT_STORAGE_MOUNT_PATH = "/app/worker"
_DEFAULT_CONFIG_PATH = "/app/config-host/config.yaml"
_DEFAULT_NAME_PREFIX = "mindroom-worker"
_DEFAULT_PUBLISH_HOST = "127.0.0.1"

_WORKER_BACKEND_ENV = KUBERNETES_WORKER_BACKEND_CONFIG_ENV_BY_KEY["worker_backend"]
_IMAGE_ENV = "MINDROOM_DOCKER_WORKER_IMAGE"
_PORT_ENV = "MINDROOM_DOCKER_WORKER_PORT"
_STORAGE_MOUNT_PATH_ENV = "MINDROOM_DOCKER_WORKER_STORAGE_MOUNT_PATH"
_CONFIG_PATH_ENV = "MINDROOM_DOCKER_WORKER_CONFIG_PATH"
_HOST_CONFIG_PATH_ENV = "MINDROOM_DOCKER_WORKER_HOST_CONFIG_PATH"
_IDLE_TIMEOUT_ENV = "MINDROOM_DOCKER_WORKER_IDLE_TIMEOUT_SECONDS"
_READY_TIMEOUT_ENV = "MINDROOM_DOCKER_WORKER_READY_TIMEOUT_SECONDS"
_NAME_PREFIX_ENV = "MINDROOM_DOCKER_WORKER_NAME_PREFIX"
_PUBLISH_HOST_ENV = "MINDROOM_DOCKER_WORKER_PUBLISH_HOST"
_ENDPOINT_HOST_ENV = "MINDROOM_DOCKER_WORKER_ENDPOINT_HOST"
_USER_ENV = "MINDROOM_DOCKER_WORKER_USER"
_EXTRA_ENV_JSON_ENV = "MINDROOM_DOCKER_WORKER_ENV_JSON"
_EXTRA_LABELS_JSON_ENV = "MINDROOM_DOCKER_WORKER_LABELS_JSON"
DOCKER_RESERVED_EXTRA_ENV_NAMES = frozenset(
    {
        "MINDROOM_RUNTIME_PATHS_JSON",
        SANDBOX_RUNTIME_ENV_BY_KEY["proxy_token"],
    },
)
_DOCKER_RESERVED_EXTRA_ENV_NAMES = DOCKER_RESERVED_EXTRA_ENV_NAMES
_DOCKER_RESERVED_LABEL_NAMES = frozenset(
    {
        "mindroom.ai/component",
        "app.mindroom.ai/managed-by",
        "app.mindroom.ai/name",
        "mindroom.ai/worker-id",
        "mindroom.ai/worker-key",
        "mindroom.ai/launch-config-hash",
        "mindroom.ai/runtime-namespace",
    },
)


def validate_docker_extra_labels(extra_labels: Mapping[str, str]) -> None:
    """Reject extra labels that would override backend-owned Docker metadata."""
    invalid_names = sorted(name for name in extra_labels if name in _DOCKER_RESERVED_LABEL_NAMES)
    if not invalid_names:
        return
    invalid_names_text = ", ".join(invalid_names)
    msg = f"Docker worker extra labels cannot override reserved labels: {invalid_names_text}"
    raise WorkerBackendError(msg)


def validate_docker_mount_layout(*, storage_mount_path: str, config_path: str) -> None:
    """Reject projected-config mount layouts that overlap the writable worker state root."""
    storage_root = PurePosixPath(storage_mount_path)
    config_dir = PurePosixPath(config_path).parent
    if storage_root == config_dir or storage_root in config_dir.parents or config_dir in storage_root.parents:
        msg = (
            "Docker worker config_path must mount outside the worker storage root: "
            f"storage_mount_path={storage_mount_path}, config_path={config_path}"
        )
        raise WorkerBackendError(msg)


def validate_docker_endpoint_host(*, endpoint_host: str) -> None:
    """Reject unusable endpoint hosts for worker health checks and returned handles."""
    if endpoint_host != "0.0.0.0":  # noqa: S104 - explicit wildcard bind host validation
        return
    msg = "Docker worker endpoint_host cannot be 0.0.0.0; set a reachable endpoint host explicitly."
    raise WorkerBackendError(msg)


def _read_host_config_path(runtime_paths: RuntimePaths, env: Mapping[str, str]) -> Path | None:
    configured = read_env(env, _HOST_CONFIG_PATH_ENV)
    if configured:
        resolved = resolve_config_relative_path(configured, runtime_paths)
        if not resolved.exists():
            msg = f"{_HOST_CONFIG_PATH_ENV} points to a missing file: {resolved}"
            raise WorkerBackendError(msg)
        if resolved.is_dir():
            msg = f"{_HOST_CONFIG_PATH_ENV} points to a directory, not a config file: {resolved}"
            raise WorkerBackendError(msg)
        return resolved
    runtime_config_path = runtime_paths.config_path.expanduser().resolve()
    if runtime_config_path.exists():
        return runtime_config_path
    return None


def _default_docker_user_for_os(os_name: str) -> str | None:
    if os_name == "posix":
        return f"{os.getuid()}:{os.getgid()}"
    if os_name == "nt":
        return None
    return None


def _read_docker_user(env: Mapping[str, str] | None = None) -> str | None:
    raw_value = os.getenv(_USER_ENV) if env is None else env.get(_USER_ENV)
    if raw_value is None:
        return _default_docker_user_for_os(os.name)
    normalized = raw_value.strip()
    return normalized or None


def normalize_docker_name_prefix(raw_value: str) -> str:
    """Normalize a configured Docker name prefix to container-safe characters."""
    normalized = re.sub(r"[^a-z0-9-]+", "-", raw_value.strip().lower()).strip("-")
    return normalized or _DEFAULT_NAME_PREFIX


def docker_workers_root(base_storage_path: Path) -> Path:
    """Return the top-level workers directory used by the Docker backend."""
    return worker_root_path(base_storage_path, "__mindroom_root__").parent


def resolve_docker_storage_path(storage_path: Path | None = None, *, runtime_paths: RuntimePaths | None = None) -> Path:
    """Resolve the storage root used by the Docker backend."""
    if storage_path is not None:
        base_storage_path = storage_path
    elif runtime_paths is not None:
        base_storage_path = runtime_paths.storage_root
    else:
        base_storage_path = resolve_primary_runtime_paths(process_env=dict(os.environ)).storage_root
    return base_storage_path.expanduser().resolve()


@dataclass(frozen=True, slots=True)
class _DockerWorkerBackendConfig:
    image: str
    worker_port: int
    storage_mount_path: str
    config_path: str
    host_config_path: Path | None
    idle_timeout_seconds: float
    ready_timeout_seconds: float
    name_prefix: str
    publish_host: str
    endpoint_host: str
    user: str | None
    extra_env: dict[str, str]
    extra_labels: dict[str, str]

    def __post_init__(self) -> None:
        validate_docker_extra_labels(self.extra_labels)
        validate_docker_mount_layout(
            storage_mount_path=self.storage_mount_path,
            config_path=self.config_path,
        )
        validate_docker_endpoint_host(endpoint_host=self.endpoint_host)

    @classmethod
    def from_runtime(cls, runtime_paths: RuntimePaths) -> _DockerWorkerBackendConfig:
        env = runtime_env_values(runtime_paths)
        image = read_env(env, _IMAGE_ENV)
        if not image:
            msg = f"{_IMAGE_ENV} must be set when {_WORKER_BACKEND_ENV}=docker."
            raise WorkerBackendError(msg)

        publish_host = read_env(env, _PUBLISH_HOST_ENV, _DEFAULT_PUBLISH_HOST) or _DEFAULT_PUBLISH_HOST
        endpoint_host = read_env(env, _ENDPOINT_HOST_ENV, publish_host) or publish_host
        extra_env = read_json_mapping_env(env, _EXTRA_ENV_JSON_ENV)
        validate_dedicated_worker_extra_env(
            extra_env,
            backend_name="Docker",
            extra_reserved_names=_DOCKER_RESERVED_EXTRA_ENV_NAMES,
        )
        return cls(
            image=image,
            worker_port=read_int_env(env, _PORT_ENV, _DEFAULT_WORKER_PORT),
            storage_mount_path=read_env(env, _STORAGE_MOUNT_PATH_ENV, _DEFAULT_STORAGE_MOUNT_PATH)
            or _DEFAULT_STORAGE_MOUNT_PATH,
            config_path=read_env(env, _CONFIG_PATH_ENV, _DEFAULT_CONFIG_PATH) or _DEFAULT_CONFIG_PATH,
            host_config_path=_read_host_config_path(runtime_paths, env),
            idle_timeout_seconds=read_float_env(env, _IDLE_TIMEOUT_ENV, _DEFAULT_IDLE_TIMEOUT_SECONDS),
            ready_timeout_seconds=read_float_env(env, _READY_TIMEOUT_ENV, _DEFAULT_READY_TIMEOUT_SECONDS),
            name_prefix=read_env(env, _NAME_PREFIX_ENV, _DEFAULT_NAME_PREFIX) or _DEFAULT_NAME_PREFIX,
            publish_host=publish_host,
            endpoint_host=endpoint_host,
            user=_read_docker_user(env),
            extra_env=extra_env,
            extra_labels=read_json_mapping_env(env, _EXTRA_LABELS_JSON_ENV),
        )

    @classmethod
    def from_env(cls) -> _DockerWorkerBackendConfig:
        return cls.from_runtime(resolve_primary_runtime_paths(process_env=dict(os.environ)))


DockerWorkerBackendConfig = _DockerWorkerBackendConfig


def docker_backend_config_signature(
    runtime_paths: RuntimePaths,
    *,
    auth_token: str | None,
    storage_path: Path | None = None,
    worker_grantable_credentials: frozenset[str] | None = None,
) -> tuple[str, ...]:
    """Return a cache signature for one concrete Docker backend config."""
    config = _DockerWorkerBackendConfig.from_runtime(runtime_paths)
    effective_runtime_paths = runtime_paths
    if config.host_config_path is not None:
        effective_runtime_paths = runtime_paths_with_config_path(effective_runtime_paths, config.host_config_path)
    effective_runtime_paths = runtime_paths_with_storage_root(
        effective_runtime_paths,
        resolve_docker_storage_path(storage_path, runtime_paths=effective_runtime_paths),
    )
    workers_root = docker_workers_root(effective_runtime_paths.storage_root)
    credentials_key = runtime_credentials_manager_key(effective_runtime_paths)
    return build_backend_config_signature(
        prefix_parts=(
            "docker",
            config.image,
            str(config.worker_port),
            config.storage_mount_path,
            config.config_path,
            str(config.host_config_path or ""),
            str(config.idle_timeout_seconds),
            str(config.ready_timeout_seconds),
            config.name_prefix,
            config.publish_host,
            config.endpoint_host,
            config.user or "",
            str(workers_root),
            str(credentials_key.shared_base_path),
            credentials_key.current_worker_key or "",
            str(credentials_key.current_worker_root or ""),
            *sorted(worker_grantable_credentials or frozenset()),
        ),
        runtime_paths=effective_runtime_paths,
        json_values=(
            config.extra_env,
            config.extra_labels,
        ),
        suffix_parts=(auth_token or "",),
    )
