from __future__ import annotations

import os
from difflib import get_close_matches
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse

import yaml

from .agents_modules import AGENT_RUNTIME_BINARIES, SUPPORTED_AGENT_TYPES, get_agent_class
from .i18n import tr
from .vm_bundles import normalize_install_bundles

CONFIG_ENV_VAR = "CONFIG_PATH"
DEFAULT_CONFIG_PATH = Path.home() / ".config" / "agsekit" / "config.yaml"
DEFAULT_AGSEKIT_DIR = DEFAULT_CONFIG_PATH.parent
DEFAULT_SSH_KEYS_DIR = DEFAULT_AGSEKIT_DIR / "ssh"
DEFAULT_SYSTEMD_ENV_DIR = DEFAULT_AGSEKIT_DIR
DEFAULT_PORTFORWARD_CONFIG_CHECK_INTERVAL_SEC = 10
ALLOWED_AGENT_TYPES = {agent_type: agent_type for agent_type in SUPPORTED_AGENT_TYPES}


def agent_runtime_binary(agent_type: str) -> str:
    try:
        return get_agent_class(agent_type).runtime_binary
    except KeyError:
        return agent_type


@dataclass
class MountConfig:
    source: Path
    target: Path
    backup: Path
    interval_minutes: int = 5
    max_backups: int = 100
    backup_clean_method: str = "thin"
    vm_name: str = ""
    allowed_agents: Optional[List[str]] = None


@dataclass
class VmConfig:
    name: str
    cpu: int
    ram: str
    disk: str
    cloud_init: Dict[str, Any]
    port_forwarding: List["PortForwardingRule"]
    proxychains: Optional[str] = None
    allowed_agents: Optional[List[str]] = None
    install: List[str] = field(default_factory=list)


@dataclass
class AgentConfig:
    name: str
    type: str
    env: Dict[str, str]
    default_args: List[str] = field(default_factory=list)
    vm_name: Optional[str] = None
    vm_names: Optional[List[str]] = None
    proxychains: Optional[str] = None
    proxychains_defined: bool = False


@dataclass(frozen=True)
class GlobalConfig:
    ssh_keys_folder: Path = DEFAULT_SSH_KEYS_DIR
    systemd_env_folder: Path = DEFAULT_SYSTEMD_ENV_DIR
    portforward_config_check_interval_sec: int = DEFAULT_PORTFORWARD_CONFIG_CHECK_INTERVAL_SEC


class ConfigError(RuntimeError):
    """Raised when configuration cannot be loaded."""

    def __init__(
        self,
        message: str,
        *,
        path: Optional[Path] = None,
        location: Optional[str] = None,
        block_kind: Optional[str] = None,
        block_name: Optional[str] = None,
    ):
        super().__init__(message)
        self.message = message
        self.path = path
        self.location = location
        self.block_kind = block_kind
        self.block_name = block_name

    def with_context(
        self,
        *,
        path: Optional[Path] = None,
        location: Optional[str] = None,
        block_kind: Optional[str] = None,
        block_name: Optional[str] = None,
    ) -> "ConfigError":
        return ConfigError(
            self.message,
            path=path if path is not None else self.path,
            location=location if location is not None else self.location,
            block_kind=block_kind if block_kind is not None else self.block_kind,
            block_name=block_name if block_name is not None else self.block_name,
        )

    def __str__(self) -> str:
        details = self.message
        has_context = any([self.path, self.location, self.block_kind, self.block_name])
        if not has_context:
            return details

        context_parts: List[str] = []
        if self.location:
            context_parts.append(tr("config.error_context_location", location=self.location))
        if self.block_kind and self.block_name:
            context_parts.append(
                tr(
                    "config.error_context_block_named",
                    block_kind=self.block_kind,
                    block_name=self.block_name,
                )
            )
        elif self.block_kind:
            context_parts.append(tr("config.error_context_block", block_kind=self.block_kind))

        context_text = ", ".join(context_parts)
        if self.path and context_text:
            return tr(
                "config.error_with_path_and_context",
                path=self.path,
                context=context_text,
                details=details,
            )
        if self.path:
            return tr("config.error_with_path", path=self.path, details=details)
        return tr("config.error_with_context", context=context_text, details=details)


class LoadedConfig(dict):
    def __init__(self, payload: Dict[str, Any], *, path: Path):
        super().__init__(payload)
        self.path = path


def _config_path(config: Dict[str, Any]) -> Optional[Path]:
    candidate = getattr(config, "path", None)
    return candidate if isinstance(candidate, Path) else None


def _raise_with_context(
    exc: ConfigError,
    *,
    config: Dict[str, Any],
    location: Optional[str] = None,
    block_kind: Optional[str] = None,
    block_name: Optional[str] = None,
) -> None:
    raise exc.with_context(
        path=_config_path(config),
        location=location,
        block_kind=block_kind,
        block_name=block_name,
    )


def resolve_config_path(explicit_path: Optional[Path] = None) -> Path:
    env_path = os.environ.get(CONFIG_ENV_VAR)
    base_path = explicit_path or (Path(env_path) if env_path else DEFAULT_CONFIG_PATH)
    return base_path.expanduser()


def load_config(path: Optional[Path] = None) -> Dict[str, Any]:
    config_path = resolve_config_path(path)
    if not config_path.exists():
        raise ConfigError(tr("config.file_not_found", path=config_path), path=config_path)

    try:
        with config_path.open("r", encoding="utf-8") as handle:
            data = yaml.safe_load(handle) or {}
    except yaml.YAMLError as exc:
        raise ConfigError(
            tr("config.parse_error", error=str(exc)),
            path=config_path,
            location="root",
            block_kind=tr("config.block_config"),
        )

    if not isinstance(data, dict):
        raise ConfigError(
            tr("config.root_not_mapping"),
            path=config_path,
            location="root",
            block_kind=tr("config.block_config"),
        )

    return LoadedConfig(data, path=config_path)


def default_global_config() -> GlobalConfig:
    return GlobalConfig()


def load_global_config(config: Dict[str, Any]) -> GlobalConfig:
    raw_global = config.get("global")
    if raw_global is None:
        return default_global_config()

    if not isinstance(raw_global, dict):
        _raise_with_context(
            ConfigError(tr("config.global_not_mapping")),
            config=config,
            location="global",
            block_kind=tr("config.block_global"),
        )

    ssh_keys_folder_raw = raw_global.get("ssh_keys_folder")
    systemd_env_folder_raw = raw_global.get("systemd_env_folder")
    portforward_interval_raw = raw_global.get("portforward_config_check_interval_sec")

    ssh_keys_folder = (
        DEFAULT_SSH_KEYS_DIR
        if ssh_keys_folder_raw is None
        else _ensure_path(ssh_keys_folder_raw, "global.ssh_keys_folder")
    )
    systemd_env_folder = (
        DEFAULT_SYSTEMD_ENV_DIR
        if systemd_env_folder_raw is None
        else _ensure_path(systemd_env_folder_raw, "global.systemd_env_folder")
    )
    portforward_config_check_interval_sec = (
        DEFAULT_PORTFORWARD_CONFIG_CHECK_INTERVAL_SEC
        if portforward_interval_raw is None
        else _require_positive_int(
            portforward_interval_raw,
            "global.portforward_config_check_interval_sec",
        )
    )

    return GlobalConfig(
        ssh_keys_folder=ssh_keys_folder,
        systemd_env_folder=systemd_env_folder,
        portforward_config_check_interval_sec=portforward_config_check_interval_sec,
    )


def load_global_config_from_path(path: Optional[Path] = None, *, allow_missing: bool = False) -> GlobalConfig:
    resolved_path = resolve_config_path(path)
    if allow_missing and not resolved_path.exists():
        return default_global_config()
    config = load_config(resolved_path)
    return load_global_config(config)


def _require_positive_int(value: Any, field_name: str) -> int:
    try:
        result = int(value)
    except (TypeError, ValueError):
        raise ConfigError(tr("config.field_not_int", field_name=field_name))
    if result <= 0:
        raise ConfigError(tr("config.field_not_positive", field_name=field_name))
    return result


def _validate_size_field(value: Any, field_name: str) -> str:
    if isinstance(value, (str, int, float)) and str(value).strip():
        return str(value)
    raise ConfigError(tr("config.field_not_string_or_number", field_name=field_name))


def _normalize_address(value: Any, field_name: str) -> str:
    if not isinstance(value, (str, int, float)):
        raise ConfigError(tr("config.field_not_host_port", field_name=field_name))

    text = str(value).strip()
    if ":" not in text:
        raise ConfigError(tr("config.field_missing_host_port", field_name=field_name))

    host, port_text = text.rsplit(":", 1)
    if not host:
        raise ConfigError(tr("config.field_missing_host", field_name=field_name))
    try:
        port = int(port_text)
    except ValueError:
        raise ConfigError(tr("config.field_port_not_numeric", field_name=field_name))
    if port <= 0 or port > 65535:
        raise ConfigError(tr("config.field_port_invalid", field_name=field_name))

    return f"{host}:{port}"


@dataclass
class PortForwardingRule:
    type: str
    host_addr: Optional[str]
    vm_addr: str


def _normalize_port_forwarding(raw_entry: Any, vm_name: str) -> List[PortForwardingRule]:
    if raw_entry is None:
        return []
    if not isinstance(raw_entry, list):
        raise ConfigError(tr("config.port_forwarding_not_list", vm_name=vm_name))

    rules: List[PortForwardingRule] = []
    for index, rule in enumerate(raw_entry):
        if not isinstance(rule, dict):
            raise ConfigError(tr("config.port_forwarding_not_mapping", vm_name=vm_name, index=index))

        raw_type = rule.get("type")
        if raw_type not in {"local", "remote", "socks5"}:
            raise ConfigError(
                tr("config.port_forwarding_invalid_type", vm_name=vm_name, index=index)
            )

        vm_addr_raw = rule.get("vm-addr")
        if vm_addr_raw is None:
            raise ConfigError(tr("config.port_forwarding_missing_vm_addr", vm_name=vm_name, index=index))

        host_addr: Optional[str]
        if raw_type in {"local", "remote"}:
            host_addr_raw = rule.get("host-addr")
            if host_addr_raw is None:
                raise ConfigError(tr("config.port_forwarding_missing_host_addr", vm_name=vm_name, index=index))
            host_addr = _normalize_address(host_addr_raw, f"vms.{vm_name}.port-forwarding[{index}].host-addr")
        else:
            host_addr = None

        vm_addr = _normalize_address(vm_addr_raw, f"vms.{vm_name}.port-forwarding[{index}].vm-addr")

        rules.append(
            PortForwardingRule(
                type=str(raw_type),
                host_addr=host_addr,
                vm_addr=vm_addr,
            )
        )

    return rules


def _normalize_proxychains(value: Any, field_name: str) -> Optional[str]:
    if value is None:
        return None
    if not isinstance(value, str):
        raise ConfigError(tr("config.proxychains_not_string", field_name=field_name))
    cleaned = value.strip()
    if not cleaned:
        return None

    parsed = urlparse(cleaned)
    if not parsed.scheme or not parsed.hostname or not parsed.port:
        raise ConfigError(
            tr("config.proxychains_invalid_url", field_name=field_name)
        )
    if parsed.username or parsed.password or parsed.path not in {"", "/"} or parsed.params or parsed.query or parsed.fragment:
        raise ConfigError(tr("config.proxychains_forbidden_parts", field_name=field_name))

    scheme = parsed.scheme.lower()
    allowed_schemes = {"http", "https", "socks4", "socks5"}
    if scheme not in allowed_schemes:
        raise ConfigError(
            tr(
                "config.proxychains_invalid_scheme",
                field_name=field_name,
                schemes=", ".join(sorted(allowed_schemes)),
            )
        )

    return f"{scheme}://{parsed.hostname}:{parsed.port}"


def load_vms_config(config: Dict[str, Any]) -> Dict[str, VmConfig]:
    raw_vms = config.get("vms")
    if not isinstance(raw_vms, dict) or not raw_vms:
        raise ConfigError(tr("config.vms_missing")).with_context(
            path=_config_path(config),
            location="vms",
            block_kind=tr("config.block_config"),
        )

    raw_agents = config.get("agents")
    known_agents = {str(name) for name in raw_agents.keys()} if isinstance(raw_agents, dict) else set()

    vms: Dict[str, VmConfig] = {}
    for vm_name, raw_entry in raw_vms.items():
        try:
            if not isinstance(raw_entry, dict):
                raise ConfigError(tr("config.vm_not_mapping", vm_name=vm_name))

            missing = [field for field in ("cpu", "ram", "disk") if field not in raw_entry]
            if missing:
                raise ConfigError(tr("config.vm_missing_fields", vm_name=vm_name, missing=", ".join(missing)))

            install_bundles = normalize_install_bundles(raw_entry.get("install"), vm_name)

            allowed_agents = _normalize_vm_allowed_agents(
                raw_entry.get("allowed_agents"),
                vm_name=str(vm_name),
                known_agents=known_agents,
            )

            vms[vm_name] = VmConfig(
                name=str(vm_name),
                cpu=_require_positive_int(raw_entry.get("cpu"), f"vms.{vm_name}.cpu"),
                ram=_validate_size_field(raw_entry.get("ram"), f"vms.{vm_name}.ram"),
                disk=_validate_size_field(raw_entry.get("disk"), f"vms.{vm_name}.disk"),
                cloud_init=raw_entry.get("cloud-init") or {},
                port_forwarding=_normalize_port_forwarding(raw_entry.get("port-forwarding"), vm_name),
                proxychains=_normalize_proxychains(raw_entry.get("proxychains"), f"vms.{vm_name}.proxychains"),
                allowed_agents=allowed_agents,
                install=install_bundles,
            )
        except ValueError as exc:
            _raise_with_context(
                ConfigError(str(exc)),
                config=config,
                location=f"vms.{vm_name}",
                block_kind=tr("config.block_vm"),
                block_name=str(vm_name),
            )
        except ConfigError as exc:
            _raise_with_context(
                exc,
                config=config,
                location=f"vms.{vm_name}",
                block_kind=tr("config.block_vm"),
                block_name=str(vm_name),
            )

    return vms


def _ensure_path(value: Any, field_name: str) -> Path:
    if isinstance(value, Path):
        path = value
    elif isinstance(value, str):
        path = Path(value)
    else:
        raise ConfigError(tr("config.field_not_path", field_name=field_name))
    return path.expanduser().resolve()


def _default_target(source: Path) -> Path:
    return Path("/home/ubuntu") / source.name


def _default_backup(source: Path) -> Path:
    return source.parent / f"backups-{source.name}"


def default_mount_target(source: Path) -> Path:
    return _default_target(source)


def default_mount_backup(source: Path) -> Path:
    return _default_backup(source)


def _default_vm_name(config: Dict[str, Any]) -> Optional[str]:
    vms_section = config.get("vms")
    if isinstance(vms_section, dict) and vms_section:
        return next(iter(vms_section.keys()))
    return None


def _normalize_agent_type(value: Any) -> str:
    if not isinstance(value, str) or not value.strip():
        raise ConfigError(tr("config.agent_type_missing"))

    cleaned = value.strip().lower()
    normalized = ALLOWED_AGENT_TYPES.get(cleaned)
    if normalized is None:
        allowed = ", ".join(sorted(ALLOWED_AGENT_TYPES.keys()))
        nearest = get_close_matches(cleaned, sorted(ALLOWED_AGENT_TYPES.keys()), n=1, cutoff=0.7)
        if nearest:
            raise ConfigError(
                tr(
                    "config.agent_type_unknown_with_hint",
                    value=value,
                    allowed=allowed,
                    hint=nearest[0],
                )
            )
        raise ConfigError(tr("config.agent_type_unknown", value=value, allowed=allowed))
    return normalized


def _normalize_env_vars(value: Any) -> Dict[str, str]:
    if value is None:
        return {}
    if not isinstance(value, dict):
        raise ConfigError(tr("config.agent_env_not_mapping"))

    normalized: Dict[str, str] = {}
    for raw_key, raw_value in value.items():
        if not isinstance(raw_key, str) or not raw_key.strip():
            raise ConfigError(tr("config.env_name_empty"))
        normalized[str(raw_key)] = "" if raw_value is None else str(raw_value)
    return normalized


def _normalize_default_args(value: Any) -> List[str]:
    if value is None:
        return []
    if not isinstance(value, list):
        raise ConfigError(tr("config.agent_default_args_not_list"))

    normalized: List[str] = []
    for index, entry in enumerate(value):
        if not isinstance(entry, str) or not entry.strip():
            raise ConfigError(tr("config.agent_default_args_empty", index=index))
        normalized.append(entry)
    return normalized


def _normalize_agent_vms(
    raw_vm: Any,
    raw_vms: Any,
    *,
    agent_name: str,
    known_vms: set[str],
) -> Optional[List[str]]:
    raw_items: List[Any] = []

    if raw_vm is not None:
        if isinstance(raw_vm, str):
            raw_vm_value = raw_vm.strip()
        else:
            raw_vm_value = str(raw_vm).strip()
        if raw_vm_value:
            raw_items.append(raw_vm_value)

    if raw_vms is not None:
        vms_items: List[Any]
        if isinstance(raw_vms, list):
            vms_items = raw_vms
        elif isinstance(raw_vms, str):
            stripped = raw_vms.strip()
            if not stripped:
                vms_items = []
            else:
                vms_items = raw_vms.split(",")
        else:
            raise ConfigError(tr("config.agent_vms_not_list", agent_name=agent_name))

        if vms_items:
            if all(isinstance(item, str) and not item.strip() for item in vms_items):
                vms_items = []
            else:
                for item_index, item in enumerate(vms_items):
                    if not isinstance(item, str) or not item.strip():
                        raise ConfigError(
                            tr(
                                "config.agent_vms_item_invalid",
                                agent_name=agent_name,
                                item_index=item_index,
                            )
                        )
        raw_items.extend(vms_items)

    if not raw_items:
        return None

    normalized: List[str] = []
    seen: set[str] = set()
    for item in raw_items:
        vm_name = item.strip()
        if vm_name in seen:
            continue
        if known_vms and vm_name not in known_vms:
            raise ConfigError(tr("config.agent_vms_unknown_vm", agent_name=agent_name, vm_name=vm_name))
        seen.add(vm_name)
        normalized.append(vm_name)
    return normalized


def load_agents_config(config: Dict[str, Any]) -> Dict[str, AgentConfig]:
    raw_agents = config.get("agents") or {}
    if not isinstance(raw_agents, dict):
        raise ConfigError(tr("config.agents_not_mapping")).with_context(
            path=_config_path(config),
            location="agents",
            block_kind=tr("config.block_config"),
        )

    raw_vms = config.get("vms")
    known_vms = {str(name) for name in raw_vms.keys()} if isinstance(raw_vms, dict) else set()
    agents: Dict[str, AgentConfig] = {}
    for agent_name, raw_entry in raw_agents.items():
        try:
            if not isinstance(raw_entry, dict):
                raise ConfigError(tr("config.agent_not_mapping", agent_name=agent_name))

            if "type" not in raw_entry:
                raw_keys = [key for key in raw_entry.keys() if isinstance(key, str)]
                typo_key = get_close_matches("type", raw_keys, n=1, cutoff=0.75)
                if typo_key:
                    raise ConfigError(
                        tr("config.agent_type_field_typo", wrong_field=typo_key[0], expected_field="type")
                    )
                raise ConfigError(tr("config.agent_type_required"))

            raw_agent_type = raw_entry.get("type")
            agent_type = _normalize_agent_type(raw_agent_type)
            env_vars = _normalize_env_vars(raw_entry.get("env"))
            default_args = _normalize_default_args(raw_entry.get("default-args"))
            vm_names = _normalize_agent_vms(
                raw_entry.get("vm"),
                raw_entry.get("vms"),
                agent_name=str(agent_name),
                known_vms=known_vms,
            )
            vm_name = vm_names[0] if vm_names else None
            proxychains_defined = "proxychains" in raw_entry
            proxychains = (
                _normalize_proxychains(raw_entry.get("proxychains"), f"agents.{agent_name}.proxychains")
                if proxychains_defined
                else None
            )

            agents[agent_name] = AgentConfig(
                name=str(agent_name),
                type=agent_type,
                env=env_vars,
                default_args=default_args,
                vm_name=vm_name,
                vm_names=vm_names,
                proxychains=proxychains,
                proxychains_defined=proxychains_defined,
            )
        except ConfigError as exc:
            _raise_with_context(
                exc,
                config=config,
                location=f"agents.{agent_name}",
                block_kind=tr("config.block_agent"),
                block_name=str(agent_name),
            )

    return agents


def _normalize_interval(raw_value: Any) -> int:
    if raw_value is None:
        return 5
    try:
        interval = int(raw_value)
    except (TypeError, ValueError):
        raise ConfigError(tr("config.mount_interval_not_int"))
    if interval <= 0:
        raise ConfigError(tr("config.mount_interval_not_positive"))
    return interval


def _normalize_max_backups(raw_value: Any, index: int) -> int:
    if raw_value is None:
        return 100
    try:
        value = int(raw_value)
    except (TypeError, ValueError):
        raise ConfigError(tr("config.mount_max_backups_not_int", index=index))
    if value <= 0:
        raise ConfigError(tr("config.mount_max_backups_not_positive", index=index))
    return value


def _normalize_backup_clean_method(raw_value: Any, index: int) -> str:
    if raw_value is None:
        return "thin"
    if not isinstance(raw_value, str):
        raise ConfigError(tr("config.mount_backup_clean_method_not_string", index=index))
    cleaned = raw_value.strip().lower()
    if cleaned in {"tail", "thin"}:
        return cleaned
    raise ConfigError(tr("config.mount_backup_clean_method_unknown", index=index, value=raw_value))


def _normalize_allowed_agents(
    raw_value: Any,
    *,
    index: int,
    known_agents: set[str],
) -> Optional[List[str]]:
    if raw_value is None:
        return None

    raw_items: List[Any]
    if isinstance(raw_value, list):
        raw_items = raw_value
    elif isinstance(raw_value, str):
        raw_items = raw_value.split(",")
    else:
        raise ConfigError(tr("config.mount_allowed_agents_not_list", index=index))

    normalized: List[str] = []
    for item_index, item in enumerate(raw_items):
        if not isinstance(item, str) or not item.strip():
            raise ConfigError(
                tr(
                    "config.mount_allowed_agents_item_invalid",
                    index=index,
                    item_index=item_index,
                )
            )

        agent_name = item.strip()
        if agent_name not in known_agents:
            raise ConfigError(
                tr(
                    "config.mount_allowed_agents_unknown_agent",
                    index=index,
                    agent_name=agent_name,
                )
            )
        normalized.append(agent_name)
    return normalized


def _normalize_vm_allowed_agents(
    raw_value: Any,
    *,
    vm_name: str,
    known_agents: set[str],
) -> Optional[List[str]]:
    if raw_value is None:
        return None

    raw_items: List[Any]
    if isinstance(raw_value, list):
        raw_items = raw_value
    elif isinstance(raw_value, str):
        raw_items = raw_value.split(",")
    else:
        raise ConfigError(tr("config.vm_allowed_agents_not_list", vm_name=vm_name))

    normalized: List[str] = []
    for item_index, item in enumerate(raw_items):
        if not isinstance(item, str) or not item.strip():
            raise ConfigError(
                tr(
                    "config.vm_allowed_agents_item_invalid",
                    vm_name=vm_name,
                    item_index=item_index,
                )
            )

        agent_name = item.strip()
        if agent_name not in known_agents:
            raise ConfigError(
                tr(
                    "config.vm_allowed_agents_unknown_agent",
                    vm_name=vm_name,
                    agent_name=agent_name,
                )
            )
        normalized.append(agent_name)
    return normalized


def load_mounts_config(config: Dict[str, Any]) -> List[MountConfig]:
    raw_mounts = config.get("mounts") or []
    if not isinstance(raw_mounts, list):
        raise ConfigError(tr("config.mounts_not_list")).with_context(
            path=_config_path(config),
            location="mounts",
            block_kind=tr("config.block_config"),
        )

    default_vm = _default_vm_name(config)
    if raw_mounts and default_vm is None:
        raise ConfigError(tr("config.mounts_missing_vms")).with_context(
            path=_config_path(config),
            location="mounts",
            block_kind=tr("config.block_config"),
        )

    raw_agents = config.get("agents")
    known_agents = {str(name) for name in raw_agents.keys()} if isinstance(raw_agents, dict) else set()

    mounts: List[MountConfig] = []
    for index, entry in enumerate(raw_mounts):
        try:
            if not isinstance(entry, dict):
                raise ConfigError(tr("config.mount_entry_not_mapping", index=index + 1))

            if "source" not in entry:
                raise ConfigError(tr("config.mount_entry_missing_source", index=index + 1))

            source = _ensure_path(entry.get("source"), f"mounts[{index}].source")
            target_raw = entry.get("target")
            backup_raw = entry.get("backup")
            vm_name = entry.get("vm") or default_vm
            if not vm_name:
                raise ConfigError(tr("config.mount_entry_missing_vm", index=index + 1))

            target = _ensure_path(target_raw, f"mounts[{index}].target") if target_raw else _default_target(source)
            backup = _ensure_path(backup_raw, f"mounts[{index}].backup") if backup_raw else _default_backup(source)
            interval_minutes = _normalize_interval(entry.get("interval"))
            max_backups = _normalize_max_backups(entry.get("max_backups"), index + 1)
            backup_clean_method = _normalize_backup_clean_method(entry.get("backup_clean_method"), index + 1)
            allowed_agents = _normalize_allowed_agents(
                entry.get("allowed_agents"),
                index=index + 1,
                known_agents=known_agents,
            )

            mounts.append(
                MountConfig(
                    source=source,
                    target=target,
                    backup=backup,
                    interval_minutes=interval_minutes,
                    max_backups=max_backups,
                    backup_clean_method=backup_clean_method,
                    vm_name=str(vm_name),
                    allowed_agents=allowed_agents,
                )
            )
        except ConfigError as exc:
            _raise_with_context(
                exc,
                config=config,
                location=f"mounts[{index}]",
                block_kind=tr("config.block_mount"),
                block_name=str(index + 1),
            )

    return mounts
