#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(pwd)"
TIER="1"
GPU_BACKEND="nvidia"
PROFILE_OVERLAYS=""
ENV_MODE="false"
SKIP_BROKEN="false"
GPU_COUNT="1"

while [[ $# -gt 0 ]]; do
    case "$1" in
        --script-dir)
            SCRIPT_DIR="${2:-$SCRIPT_DIR}"
            shift 2
            ;;
        --tier)
            TIER="${2:-$TIER}"
            shift 2
            ;;
        --gpu-backend)
            GPU_BACKEND="${2:-$GPU_BACKEND}"
            shift 2
            ;;
        --profile-overlays)
            PROFILE_OVERLAYS="${2:-$PROFILE_OVERLAYS}"
            shift 2
            ;;
        --skip-broken)
            SKIP_BROKEN="true"
            shift
            ;;
        --env)
            ENV_MODE="true"
            shift
            ;;
        --gpu-count)
            GPU_COUNT="${2:-$GPU_COUNT}"
            shift 2
            ;;
        *)
            echo "Unknown argument: $1" >&2
            exit 1
            ;;
    esac
done

ROOT_DIR="$SCRIPT_DIR"
PYTHON_CMD="python3"
if [[ -f "$ROOT_DIR/lib/python-cmd.sh" ]]; then
    . "$ROOT_DIR/lib/python-cmd.sh"
    PYTHON_CMD="$(ds_detect_python_cmd)"
elif command -v python >/dev/null 2>&1; then
    PYTHON_CMD="python"
fi

"$PYTHON_CMD" - "$SCRIPT_DIR" "$TIER" "$GPU_BACKEND" "$PROFILE_OVERLAYS" "$ENV_MODE" "$SKIP_BROKEN" "$GPU_COUNT" <<'PY'
import os
import pathlib
import platform
import sys
import json

script_dir = pathlib.Path(sys.argv[1])
tier = (sys.argv[2] or "1").upper()
gpu_backend = (sys.argv[3] or "nvidia").lower()
profile_overlays = [x.strip() for x in (sys.argv[4] or "").split(",") if x.strip()]
env_mode = (sys.argv[5] or "false").lower() == "true"
skip_broken = (sys.argv[6] or "false").lower() == "true"
dream_mode = os.environ.get("DREAM_MODE", "local").lower()
gpu_count = int(sys.argv[7] or "1")

IS_DARWIN = platform.system() == "Darwin"
APPLE_OVERLAY = "installers/macos/docker-compose.macos.yml" if IS_DARWIN else "docker-compose.apple.yml"

def existing(overlays):
    return all((script_dir / f).exists() for f in overlays)

resolved = []
primary = "docker-compose.yml"

if profile_overlays and existing(profile_overlays):
    resolved = profile_overlays
    primary = profile_overlays[-1]
elif tier in {"AP_ULTRA", "AP_PRO", "AP_BASE"}:
    if existing(["docker-compose.base.yml", APPLE_OVERLAY]):
        resolved = ["docker-compose.base.yml", APPLE_OVERLAY]
        primary = APPLE_OVERLAY
    elif existing(["docker-compose.base.yml"]):
        resolved = ["docker-compose.base.yml"]
        primary = "docker-compose.base.yml"
elif gpu_backend == "cpu":
    if existing(["docker-compose.base.yml", "docker-compose.cpu.yml"]):
        resolved = ["docker-compose.base.yml", "docker-compose.cpu.yml"]
        primary = "docker-compose.cpu.yml"
    elif existing(["docker-compose.base.yml"]):
        resolved = ["docker-compose.base.yml"]
        primary = "docker-compose.base.yml"
elif tier in {"SH_LARGE", "SH_COMPACT"}:
    if existing(["docker-compose.base.yml", "docker-compose.amd.yml"]):
        resolved = ["docker-compose.base.yml", "docker-compose.amd.yml"]
        primary = "docker-compose.amd.yml"
elif gpu_backend == "apple":
    if existing(["docker-compose.base.yml", APPLE_OVERLAY]):
        resolved = ["docker-compose.base.yml", APPLE_OVERLAY]
        primary = APPLE_OVERLAY
    elif existing(["docker-compose.base.yml"]):
        resolved = ["docker-compose.base.yml"]
        primary = "docker-compose.base.yml"
elif gpu_backend == "amd":
    if existing(["docker-compose.base.yml", "docker-compose.amd.yml"]):
        resolved = ["docker-compose.base.yml", "docker-compose.amd.yml"]
        primary = "docker-compose.amd.yml"
elif gpu_backend in ("intel", "sycl") or tier in ("ARC", "ARC_LITE"):
    if existing(["docker-compose.base.yml", "docker-compose.arc.yml"]):
        resolved = ["docker-compose.base.yml", "docker-compose.arc.yml"]
        primary = "docker-compose.arc.yml"
    elif existing(["docker-compose.base.yml", "docker-compose.intel.yml"]):
        resolved = ["docker-compose.base.yml", "docker-compose.intel.yml"]
        primary = "docker-compose.intel.yml"
    elif existing(["docker-compose.base.yml"]):
        resolved = ["docker-compose.base.yml"]
        primary = "docker-compose.base.yml"
else:
    if existing(["docker-compose.base.yml", "docker-compose.nvidia.yml"]):
        resolved = ["docker-compose.base.yml", "docker-compose.nvidia.yml"]
        primary = "docker-compose.nvidia.yml"
    elif (script_dir / "docker-compose.yml").exists():
        resolved = ["docker-compose.yml"]
        primary = "docker-compose.yml"

if not resolved:
    resolved = [primary]

# Multi-GPU overlay if we have more than 1 GPU.
if gpu_count > 1 and (script_dir / "docker-compose.multigpu.yml").exists():
    resolved.append("docker-compose.multigpu.yml")

# PyYAML is a hard requirement — extensions and overlays are YAML and must be
# parsed for the compose security scan. Silent fallback used to hide install
# breakage on systems without PyYAML (Arch/Alpine/Void/some macOS) and let
# user-extension composes through unscanned. Fail loud here instead.
import yaml
import re

_LOOPBACK_VAR_DEFAULT_RE = re.compile(
    r"^\$\{[A-Za-z_][A-Za-z0-9_]*:-127\.0\.0\.1\}$",
)

# Capabilities and security_opt strings that grant container escape primitives.
_DANGEROUS_CAPS = {
    "SYS_ADMIN", "NET_ADMIN", "SYS_PTRACE", "NET_RAW",
    "DAC_OVERRIDE", "SETUID", "SETGID", "SYS_MODULE",
    "SYS_RAWIO", "ALL",
}
_DANGEROUS_SECURITY_OPTS = {
    "seccomp:unconfined", "apparmor:unconfined", "label:disable",
}

# Core service IDs — user extensions must not declare services with these names
# (would shadow the built-in services in the compose merge). Mirrors the
# dashboard-api install endpoint's CORE_SERVICE_IDS / skip_name_collision check
# so a hand-dropped or backup-restored extension under the user-extensions dir
# can't override e.g. dashboard-api or llama-server. Loaded best-effort: if
# config/core-service-ids.json is missing or unparseable, fall back to a
# hardcoded list matching helpers.py CORE_SERVICE_IDS_FALLBACK.
import json as _json_mod
try:
    _CORE_SERVICE_IDS = set(
        _json_mod.loads((script_dir / "config" / "core-service-ids.json").read_text(encoding="utf-8"))
    )
except (OSError, ValueError):
    _CORE_SERVICE_IDS = {
        "ape", "comfyui", "dashboard", "dashboard-api", "dreamforge",
        "embeddings", "langfuse", "litellm", "llama-server", "n8n",
        "open-webui", "openclaw", "perplexica", "privacy-shield", "qdrant",
        "searxng", "token-spy", "tts", "whisper",
    }


def _host_part_is_loopback(host):
    if host == "127.0.0.1":
        return True
    return bool(_LOOPBACK_VAR_DEFAULT_RE.fullmatch(host))


def _split_port_host(port_str):
    """Mirror dashboard-api/_split_port_host: handle ${VAR:-127.0.0.1}: prefix."""
    if port_str.startswith("${"):
        end = port_str.find("}")
        if end == -1 or end + 1 >= len(port_str) or port_str[end + 1] != ":":
            return port_str, ""
        return port_str[: end + 1], port_str[end + 2:]
    if ":" not in port_str:
        return None, port_str
    host, _, rest = port_str.partition(":")
    if host.isdigit():
        return None, port_str
    return host, rest


def _scan_user_compose_content(compose_path):
    """Reject compose fragments containing dangerous directives.

    Mirrors dashboard-api/routers/extensions.py:_scan_compose_content (without
    the FastAPI HTTPException dependency). Returns ``(ok, warnings)``: ``ok``
    is False on any rejection, ``warnings`` is a list of human-readable
    messages. User-extension contexts are always untrusted at the resolver
    layer — no ``trusted=True`` exemption.
    """
    try:
        data = yaml.safe_load(compose_path.read_text(encoding="utf-8"))
    except (yaml.YAMLError, OSError) as e:
        return (False, [f"invalid compose file {compose_path}: {e}"])

    if not isinstance(data, dict):
        return (False, [f"compose file {compose_path} must be a YAML mapping"])

    warnings = []
    services = data.get("services", {})
    if not isinstance(services, dict):
        return (True, warnings)

    ok = True

    def reject(msg):
        nonlocal ok
        ok = False
        warnings.append(msg)

    for svc_name, svc_def in services.items():
        if not isinstance(svc_def, dict):
            continue
        # Name-collision: user-extension service names must not shadow
        # built-in core services in the compose merge. Mirrors the
        # dashboard-api install endpoint's skip_name_collision=False path.
        if svc_name in _CORE_SERVICE_IDS:
            reject(f"service '{svc_name}' collides with a built-in core service name")
        if svc_def.get("privileged") is True:
            reject(f"service '{svc_name}' uses privileged mode")
        if "build" in svc_def:
            reject(f"service '{svc_name}' uses a local build — only pre-built images are allowed for user extensions")
        user = svc_def.get("user")
        if user is not None and str(user).split(":")[0] in ("root", "0"):
            reject(f"service '{svc_name}' runs as root")
        if svc_def.get("network_mode") == "host":
            reject(f"service '{svc_name}' uses host network mode")
        if svc_def.get("pid") == "host":
            reject(f"service '{svc_name}' uses host PID namespace")
        if svc_def.get("ipc") == "host":
            reject(f"service '{svc_name}' uses host IPC namespace")
        if svc_def.get("userns_mode") == "host":
            reject(f"service '{svc_name}' uses host user namespace")
        cap_add = svc_def.get("cap_add", [])
        if isinstance(cap_add, list):
            for cap in cap_add:
                if str(cap).upper() in _DANGEROUS_CAPS:
                    reject(f"service '{svc_name}' adds dangerous capability: {cap}")
        security_opt = svc_def.get("security_opt", [])
        if isinstance(security_opt, list):
            for opt in security_opt:
                opt_str = str(opt).lower().replace("=", ":")
                if opt_str in _DANGEROUS_SECURITY_OPTS:
                    reject(f"service '{svc_name}' uses dangerous security_opt '{opt}'")
        if svc_def.get("devices"):
            reject(f"service '{svc_name}' declares devices")
        deploy = svc_def.get("deploy")
        if isinstance(deploy, dict):
            resources = deploy.get("resources")
            if isinstance(resources, dict):
                reservations = resources.get("reservations")
                if isinstance(reservations, dict) and reservations.get("devices"):
                    reject(f"service '{svc_name}' requests GPU passthrough via deploy.resources.reservations.devices")
        volumes = svc_def.get("volumes", [])
        if isinstance(volumes, list):
            for vol in volumes:
                if isinstance(vol, dict):
                    source = str(vol.get("source", ""))
                    target = str(vol.get("target", ""))
                    if "docker.sock" in source or "docker.sock" in target:
                        reject(f"service '{svc_name}' mounts the Docker socket")
                    if source.startswith("/"):
                        reject(f"service '{svc_name}' bind-mounts absolute host path '{source}'")
                    continue
                vol_str = str(vol)
                if "docker.sock" in vol_str:
                    reject(f"service '{svc_name}' mounts the Docker socket")
                vol_parts = vol_str.split(":")
                if len(vol_parts) >= 2 and vol_parts[0].startswith("/"):
                    reject(f"service '{svc_name}' bind-mounts absolute host path '{vol_parts[0]}'")
        if svc_def.get("extra_hosts"):
            reject(f"service '{svc_name}' declares extra_hosts")
        if svc_def.get("sysctls"):
            reject(f"service '{svc_name}' declares sysctls")
        labels = svc_def.get("labels", [])
        if isinstance(labels, dict):
            label_keys = labels.keys()
        elif isinstance(labels, list):
            label_keys = [lbl.split("=", 1)[0] for lbl in labels if isinstance(lbl, str)]
        else:
            label_keys = []
        for lk in label_keys:
            if str(lk).startswith("com.docker.compose."):
                reject(f"service '{svc_name}' uses reserved Docker Compose label '{lk}'")
        ports = svc_def.get("ports", [])
        if isinstance(ports, list):
            for port in ports:
                if isinstance(port, dict):
                    host_ip = port.get("host_ip", "")
                    if port.get("published") and not _host_part_is_loopback(host_ip):
                        reject(f"service '{svc_name}' dict port binding must use host_ip 127.0.0.1 or '${{VAR:-127.0.0.1}}'")
                else:
                    port_str = str(port)
                    host_part, rest = _split_port_host(port_str)
                    if host_part is None:
                        reject(f"service '{svc_name}' port '{port_str}' must use 127.0.0.1:host:container format")
                        continue
                    if not _host_part_is_loopback(host_part):
                        reject(f"service '{svc_name}' port '{port_str}' must bind 127.0.0.1 (literal or '${{VAR:-127.0.0.1}}')")
                        continue
                    core = rest.split("/", 1)[0]
                    if ":" not in core:
                        reject(f"service '{svc_name}' port '{port_str}' must specify host:host_port:container_port")

    top_volumes = data.get("volumes", {})
    if isinstance(top_volumes, dict):
        for vol_name, vol_def in top_volumes.items():
            if not isinstance(vol_def, dict):
                continue
            driver_opts = vol_def.get("driver_opts", {})
            if not isinstance(driver_opts, dict):
                continue
            vol_type = str(driver_opts.get("type", "")).lower()
            device = str(driver_opts.get("device", ""))
            if vol_type in ("none", "bind") and device.startswith("/"):
                reject(f"named volume '{vol_name}' uses driver_opts to bind-mount host path '{device}'")

    return (ok, warnings)


# Discover enabled extension compose fragments via manifests
ext_dir = script_dir / "extensions" / "services"
if ext_dir.exists():
    for service_dir in sorted(ext_dir.iterdir()):
        if not service_dir.is_dir():
            continue
        # Find manifest
        manifest_path = None
        for name in ("manifest.yaml", "manifest.yml", "manifest.json"):
            candidate = service_dir / name
            if candidate.exists():
                manifest_path = candidate
                break
        if not manifest_path:
            continue
        try:
            with open(manifest_path) as f:
                if manifest_path.suffix == ".json":
                    manifest = json.load(f)
                else:
                    manifest = yaml.safe_load(f)
            if not isinstance(manifest, dict):
                print(f"WARNING: empty/non-dict manifest for {service_dir.name} at {manifest_path}, skipping", file=sys.stderr)
                continue
            if manifest.get("schema_version") != "dream.services.v1":
                continue
            service = manifest.get("service", {})
            # Check GPU backend compatibility
            backends = service.get("gpu_backends", ["amd", "nvidia"])
            # "none" means CPU-only — compatible with any GPU backend
            if gpu_backend not in backends and "all" not in backends and "none" not in backends:
                continue
            # Get compose file from manifest
            compose_rel = service.get("compose_file", "")
            if compose_rel and not compose_rel.endswith(".disabled"):
                # Validate compose_file stays inside its extension's directory.
                # Path.relative_to() does lexical part-prefix matching, so a
                # `..`-traversal ("../../../etc/passwd") still string-matches
                # script_dir without escape detection; an absolute compose_file
                # ("/etc/shadow") replaces service_dir entirely under `/`. Both
                # would otherwise reach docker compose `-f`. Boundary-check on
                # fully resolved paths, but keep the unresolved compose_path
                # for the emit so the existing relative_to(script_dir) contract
                # still holds on systems where script_dir contains symlinks
                # (macOS /var -> /private/var would mismatch otherwise).
                compose_path = service_dir / compose_rel
                try:
                    compose_path.resolve().relative_to(service_dir.resolve())
                except ValueError:
                    print(f"WARNING: {service_dir.name}: compose_file '{compose_rel}' "
                          f"escapes the extension directory; skipping",
                          file=sys.stderr)
                    continue
                if compose_path.exists():
                    resolved.append(str(compose_path.relative_to(script_dir)))
                elif (service_dir / f"{compose_rel}.disabled").exists():
                    continue  # Service disabled — skip all overlays
                else:
                    print(f"WARNING: {service_dir.name}: compose_file '{compose_rel}' not found, skipping overlays", file=sys.stderr)
                    continue  # Base compose missing — skip GPU/mode overlays
            # GPU-specific overlay (filesystem discovery — not in manifest)
            gpu_overlay = service_dir / f"compose.{gpu_backend}.yaml"
            if gpu_overlay.exists():
                resolved.append(str(gpu_overlay.relative_to(script_dir)))

            # Mode-specific overlay — depends_on for local/hybrid mode only.
            # Skip on Apple Silicon: macOS runs llama-server natively on the host
            # (Docker service has replicas: 0), so `depends_on: llama-server:
            # service_healthy` inside compose.local.yaml overlays can never be
            # satisfied and deadlocks the stack. The real LLM-ready gate on macOS
            # is the `llama-server-ready` sidecar defined in the macOS overlay.
            if dream_mode in ("local", "hybrid", "lemonade") and gpu_backend != "apple":
                local_mode_overlay = service_dir / "compose.local.yaml"
                if local_mode_overlay.exists():
                    resolved.append(str(local_mode_overlay.relative_to(script_dir)))

            # Multi-GPU overlay if we have more than 1 GPU
            if gpu_count > 1:
                multi_gpu_overlay = service_dir / "compose.multigpu.yaml"
                if multi_gpu_overlay.exists():
                    resolved.append(str(multi_gpu_overlay.relative_to(script_dir)))

        except Exception as e:
            # Narrow exception handling to specific parse/structure errors
            yaml_error = isinstance(e, yaml.YAMLError)
            json_error = isinstance(e, json.JSONDecodeError)
            structure_error = isinstance(e, (KeyError, TypeError))

            if yaml_error or json_error or structure_error:
                print(f"ERROR: Failed to parse manifest for {service_dir.name}: {e}", file=sys.stderr)
                print(f"  Manifest path: {manifest_path}", file=sys.stderr)
                print(f"  This service will be skipped. Fix the manifest or disable the service.", file=sys.stderr)
                if skip_broken:
                    continue
                else:
                    sys.exit(1)
            else:
                # Unexpected error — re-raise to crash visibly
                raise

# Discover enabled user-installed extensions (from dashboard portal)
user_ext_dir = script_dir / "data" / "user-extensions"
if user_ext_dir.exists():
    try:
        for service_dir in sorted(user_ext_dir.iterdir()):
            if not service_dir.is_dir():
                continue
            # Find manifest
            manifest_path = None
            for name in ("manifest.yaml", "manifest.yml", "manifest.json"):
                candidate = service_dir / name
                if candidate.exists():
                    manifest_path = candidate
                    break
            try:
                manifest = None  # init so the gpu_backends gate below is safe in the manifest-less branch
                if manifest_path is not None:
                    with open(manifest_path) as f:
                        if manifest_path.suffix == ".json":
                            manifest = json.load(f)
                        else:
                            manifest = yaml.safe_load(f)
                    if manifest is not None and not isinstance(manifest, dict):
                        print(f"WARNING: empty/non-dict manifest for {service_dir.name} at {manifest_path}, skipping", file=sys.stderr)
                        continue
                    if isinstance(manifest, dict) and manifest.get("schema_version") != "dream.services.v1":
                        continue
                    service = manifest.get("service", {}) if isinstance(manifest, dict) else {}
                else:
                    service = {}
                # Apply gpu_backends filter — same predicate as the built-in loop above.
                # Gated on isinstance(manifest, dict) so the manifest-less compat
                # carve-out (legacy user extensions that pre-date the manifest convention)
                # falls through unfiltered.
                if isinstance(manifest, dict):
                    backends = service.get("gpu_backends", ["amd", "nvidia"])
                    # "none" means CPU-only — compatible with any GPU backend
                    if gpu_backend not in backends and "all" not in backends and "none" not in backends:
                        continue
                # Get compose file from manifest, default to compose.yaml
                compose_rel = service.get("compose_file", "compose.yaml")
                if compose_rel and not compose_rel.endswith(".disabled"):
                    # Boundary check: a malicious manifest could point compose_file
                    # at "../../etc/passwd" or an absolute path. Resolve both sides
                    # so a `/var → /private/var` symlink on macOS doesn't false-flag.
                    compose_path = service_dir / compose_rel
                    try:
                        compose_path.resolve().relative_to(service_dir.resolve())
                    except ValueError:
                        print(f"WARNING: {service_dir.name}: compose_file '{compose_rel}' "
                              f"escapes the extension directory; skipping",
                              file=sys.stderr)
                        continue
                    if compose_path.exists():
                        # Scan content before appending — user-ext composes are
                        # untrusted. The dashboard-api scans at install time;
                        # the resolver runs every `dream` invocation, so a
                        # tampered-with compose dropped under data/user-extensions
                        # without going through the install API would otherwise
                        # bypass scanning entirely.
                        ok, warnings = _scan_user_compose_content(compose_path)
                        for w in warnings:
                            print(f"WARNING: {service_dir.name}: {w}", file=sys.stderr)
                        if not ok:
                            continue
                        resolved.append(str(compose_path.relative_to(script_dir)))
                    elif (service_dir / f"{compose_rel}.disabled").exists():
                        continue  # Service disabled — skip all overlays
                    else:
                        # No base compose — skip overlays for this user extension
                        continue
                # GPU-specific overlay (filesystem discovery — not in manifest)
                gpu_overlay = service_dir / f"compose.{gpu_backend}.yaml"
                if gpu_overlay.exists():
                    # Fixed filename so traversal isn't possible, but the same
                    # security checks apply to the overlay's content.
                    ok, warnings = _scan_user_compose_content(gpu_overlay)
                    for w in warnings:
                        print(f"WARNING: {service_dir.name}: {w}", file=sys.stderr)
                    if ok:
                        resolved.append(str(gpu_overlay.relative_to(script_dir)))

                # Mode-specific overlay — depends_on for local/hybrid mode only.
                # Skip on Apple Silicon: macOS runs llama-server natively on the host
                # (Docker service has replicas: 0), so `depends_on: llama-server:
                # service_healthy` inside compose.local.yaml overlays can never be
                # satisfied and deadlocks the stack. The real LLM-ready gate on macOS
                # is the `llama-server-ready` sidecar defined in the macOS overlay.
                # Mirrors the same guard in the built-in loop above (PR #1004).
                if dream_mode in ("local", "hybrid", "lemonade") and gpu_backend != "apple":
                    local_mode_overlay = service_dir / "compose.local.yaml"
                    if local_mode_overlay.exists():
                        # Same content scan as compose.yaml/gpu overlay above —
                        # without it, a malicious user extension can put
                        # privileged: true / docker.sock mounts in compose.local.yaml
                        # and reach the host since DREAM_MODE defaults to "local".
                        ok, warnings = _scan_user_compose_content(local_mode_overlay)
                        for w in warnings:
                            print(f"WARNING: {service_dir.name}: {w}", file=sys.stderr)
                        if ok:
                            resolved.append(str(local_mode_overlay.relative_to(script_dir)))

                # Multi-GPU overlay if we have more than 1 GPU
                if gpu_count > 1:
                    multi_gpu_overlay = service_dir / "compose.multigpu.yaml"
                    if multi_gpu_overlay.exists():
                        # Fixed filename, but same content scan applies — see
                        # the gpu/local-mode overlay scans above.
                        ok, warnings = _scan_user_compose_content(multi_gpu_overlay)
                        for w in warnings:
                            print(f"WARNING: {service_dir.name}: {w}", file=sys.stderr)
                        if ok:
                            resolved.append(str(multi_gpu_overlay.relative_to(script_dir)))

            except Exception as e:
                # Narrow exception handling to specific parse/structure errors
                yaml_error = isinstance(e, yaml.YAMLError)
                json_error = isinstance(e, json.JSONDecodeError)
                structure_error = isinstance(e, (KeyError, TypeError))

                if yaml_error or json_error or structure_error:
                    print(f"ERROR: Failed to parse manifest for {service_dir.name}: {e}", file=sys.stderr)
                    print(f"  Manifest path: {manifest_path}", file=sys.stderr)
                    print(f"  This service will be skipped. Fix the manifest or disable the service.", file=sys.stderr)
                    if skip_broken:
                        continue
                    else:
                        sys.exit(1)
                else:
                    # Unexpected error — re-raise to crash visibly
                    raise
    except OSError as e:
        print(f"WARNING: Could not scan user-extensions: {e}", file=sys.stderr)

# Include docker-compose.override.yml if it exists (user customizations).
# Even though the operator placed this file themselves, the resolver runs
# under installer/CI and may handle composes from sources the operator
# trusts less than themselves (cloned repo, restored backup). Apply the
# same content scan as user extensions.
override = script_dir / "docker-compose.override.yml"
if override.exists():
    ok, warnings = _scan_user_compose_content(override)
    for w in warnings:
        print(f"WARNING: docker-compose.override.yml: {w}", file=sys.stderr)
    if ok:
        resolved.append("docker-compose.override.yml")

def to_flags(files):
    return " ".join(f"-f {f}" for f in files)

resolved_flags = to_flags(resolved)

if env_mode:
    def out(key, value):
        safe = str(value).replace("\\", "\\\\").replace('"', '\\"')
        print(f'{key}="{safe}"')
    out("COMPOSE_PRIMARY_FILE", primary)
    out("COMPOSE_FILE_LIST", ",".join(resolved))
    out("COMPOSE_FLAGS", resolved_flags)
else:
    print(resolved_flags)
PY
