from __future__ import annotations

import argparse
from datetime import datetime, timezone
import json
import os
from pathlib import Path
import signal
import subprocess
import sys
import time
from typing import Any
from urllib.error import URLError
from urllib.request import urlopen


ROOT = Path(__file__).resolve().parents[1]
DEFAULT_RUNTIME_DIR = ROOT / ".local_stack"
DEFAULT_DATA_DIR = ROOT / ".data_local"
DEFAULT_ENV_FILES = [
    ROOT / ".env.production",
    ROOT / ".env.local",
    ROOT / ".env",
]


def _env_int(name: str, default: int, env: dict[str, str] | None = None) -> int:
    source = env or os.environ
    raw = str(source.get(name, "")).strip()
    if not raw:
        return default
    try:
        value = int(raw)
    except ValueError as exc:
        raise RuntimeError(f"{name} must be an integer") from exc
    if value <= 0:
        raise RuntimeError(f"{name} must be positive")
    return value


def utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def load_env_file(path: Path) -> dict[str, str]:
    if not path.exists():
        return {}
    result: dict[str, str] = {}
    for raw_line in path.read_text(encoding="utf-8").splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        key = key.strip()
        value = value.strip()
        if not key:
            continue
        if value and value[0] == value[-1] and value[0] in {'"', "'"}:
            value = value[1:-1]
        result[key] = value
    return result


def load_stack_env(extra_env_files: list[Path] | None = None) -> dict[str, str]:
    env = os.environ.copy()
    for path in DEFAULT_ENV_FILES:
        for key, value in load_env_file(path).items():
            env.setdefault(key, value)
    for path in extra_env_files or []:
        for key, value in load_env_file(path).items():
            env[key] = value
    return env


def build_service_specs(env: dict[str, str] | None = None) -> list[dict[str, Any]]:
    gateway_port = _env_int("LOCAL_STACK_GATEWAY_PORT", 8080, env)
    return [
        {"name": "auth", "port": 8001, "module": "services.auth_service.app:app"},
        {"name": "audit", "port": 8002, "module": "services.audit_service.app:app"},
        {"name": "customer", "port": 8003, "module": "services.customer_service.app:app"},
        {"name": "interaction", "port": 8004, "module": "services.interaction_service.app:app"},
        {"name": "routing", "port": 8005, "module": "services.routing_service.app:app"},
        {"name": "voice", "port": 8006, "module": "services.voice_adapter_service.app:app"},
        {"name": "recording", "port": 8013, "module": "services.recording_service.app:app"},
        {"name": "ivr", "port": 8014, "module": "services.ivr_service.app:app"},
        {"name": "event-bus", "port": 8015, "module": "services.event_bus_service.app:app"},
        {"name": "asterisk-bridge", "port": 8016, "module": "services.asterisk_bridge_service.app:app"},
        {"name": "telegram", "port": 8007, "module": "services.telegram_adapter_service.app:app"},
        {"name": "whatsapp", "port": 8019, "module": "services.whatsapp_adapter_service.app:app"},
        {"name": "ai", "port": 8017, "module": "services.ai_orchestrator_service.app:app"},
        {"name": "streaming-asr-sidecar", "port": 8021, "module": "services.streaming_asr_sidecar_service.app:app"},
        {"name": "ai-voice-runtime", "port": 8018, "module": "services.ai_voice_runtime_service.app:app"},
        {"name": "webchat", "port": 8011, "module": "services.webchat_adapter_service.app:app"},
        {"name": "email", "port": 8012, "module": "services.email_adapter_service.app:app"},
        {"name": "kb", "port": 8008, "module": "services.kb_service.app:app"},
        {"name": "reporting", "port": 8009, "module": "services.reporting_service.app:app"},
        {"name": "supervisor", "port": 8010, "module": "services.supervisor_service.app:app"},
        {"name": "sales", "port": 8020, "module": "services.sales_service.app:app"},
        {"name": "gateway", "port": gateway_port, "module": "gateway.app:app"},
    ]

def build_gateway_env(env: dict[str, str] | None = None) -> dict[str, str]:
    ports = {spec["name"]: int(spec["port"]) for spec in build_service_specs(env)}
    base = "http://127.0.0.1"
    return {
        "AUTH_SERVICE_URL": f"{base}:{ports['auth']}",
        "AUDIT_SERVICE_URL": f"{base}:{ports['audit']}",
        "CUSTOMER_SERVICE_URL": f"{base}:{ports['customer']}",
        "INTERACTION_SERVICE_URL": f"{base}:{ports['interaction']}",
        "ROUTING_SERVICE_URL": f"{base}:{ports['routing']}",
        "VOICE_ADAPTER_SERVICE_URL": f"{base}:{ports['voice']}",
        "RECORDING_SERVICE_URL": f"{base}:{ports['recording']}",
        "IVR_SERVICE_URL": f"{base}:{ports['ivr']}",
        "EVENT_BUS_SERVICE_URL": f"{base}:{ports['event-bus']}",
        "ASTERISK_BRIDGE_SERVICE_URL": f"{base}:{ports['asterisk-bridge']}",
        "TELEGRAM_ADAPTER_SERVICE_URL": f"{base}:{ports['telegram']}",
        "WHATSAPP_ADAPTER_SERVICE_URL": f"{base}:{ports['whatsapp']}",
        "AI_ORCHESTRATOR_SERVICE_URL": f"{base}:{ports['ai']}",
        "AI_VOICE_RUNTIME_SERVICE_URL": f"{base}:{ports['ai-voice-runtime']}",
        "WEBCHAT_ADAPTER_SERVICE_URL": f"{base}:{ports['webchat']}",
        "EMAIL_ADAPTER_SERVICE_URL": f"{base}:{ports['email']}",
        "KB_SERVICE_URL": f"{base}:{ports['kb']}",
        "REPORTING_SERVICE_URL": f"{base}:{ports['reporting']}",
        "SUPERVISOR_SERVICE_URL": f"{base}:{ports['supervisor']}",
        "SALES_SERVICE_URL": f"{base}:{ports['sales']}",
    }


def service_bind_host(spec_name: str, env: dict[str, str]) -> str:
    if spec_name == "gateway":
        return env.get("LOCAL_STACK_GATEWAY_HOST", "127.0.0.1").strip() or "127.0.0.1"
    return env.get("LOCAL_STACK_SERVICE_HOST", "127.0.0.1").strip() or "127.0.0.1"


def service_health_host(spec_name: str, env: dict[str, str]) -> str:
    if spec_name == "gateway":
        configured = env.get("LOCAL_STACK_GATEWAY_HEALTH_HOST", "").strip()
        if configured:
            return configured
    bind_host = service_bind_host(spec_name, env)
    if bind_host in {"0.0.0.0", "::"}:
        return "127.0.0.1"
    return bind_host


def public_base_url(env: dict[str, str] | None = None) -> str:
    current_env = env or {}
    default_url = "http://127.0.0.1:8080"
    return current_env.get("LOCAL_STACK_PUBLIC_BASE_URL", default_url).strip() or default_url


def build_manifest_payload(
    runtime_dir: Path,
    data_dir: Path,
    services: list[dict[str, Any]],
    env: dict[str, str] | None = None,
) -> dict[str, Any]:
    return {
        "generated_at": utc_now_iso(),
        "root": str(ROOT),
        "runtime_dir": str(runtime_dir),
        "data_dir": str(data_dir),
        "base_url": public_base_url(env),
        "services": services,
    }


def manifest_path(runtime_dir: Path) -> Path:
    return runtime_dir / "manifest.json"


def load_manifest(runtime_dir: Path) -> dict[str, Any] | None:
    path = manifest_path(runtime_dir)
    if not path.exists():
        return None
    return json.loads(path.read_text(encoding="utf-8"))


def save_manifest(runtime_dir: Path, payload: dict[str, Any]) -> None:
    runtime_dir.mkdir(parents=True, exist_ok=True)
    manifest_path(runtime_dir).write_text(json.dumps(payload, indent=2), encoding="utf-8")


def remove_manifest(runtime_dir: Path) -> None:
    path = manifest_path(runtime_dir)
    if path.exists():
        path.unlink()


def is_pid_running(pid: int) -> bool:
    if pid <= 0:
        return False
    if os.name == "nt":
        result = subprocess.run(
            ["tasklist", "/FI", f"PID eq {pid}"],
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,
            text=True,
            check=False,
        )
        if result.returncode != 0:
            return False
        return str(pid) in (result.stdout or "")
    try:
        os.kill(pid, 0)
    except (OSError, SystemError):
        return False
    return True


def resolve_listener_pid(port: int) -> int | None:
    if os.name == "nt":
        result = subprocess.run(
            ["netstat", "-ano", "-p", "tcp"],
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,
            text=True,
            check=False,
        )
        if result.returncode != 0:
            return None
        markers = {f"127.0.0.1:{port}", f"0.0.0.0:{port}", f"[::]:{port}"}
        for raw_line in (result.stdout or "").splitlines():
            line = raw_line.strip()
            if "LISTENING" not in line:
                continue
            parts = line.split()
            if len(parts) < 5:
                continue
            local_address = parts[1]
            state = parts[3]
            pid_raw = parts[4]
            if local_address not in markers or state != "LISTENING":
                continue
            try:
                pid = int(pid_raw)
            except ValueError:
                continue
            if pid > 0:
                return pid
        return None
    return None


def wait_for_health(host: str, port: int, retries: int = 80, delay: float = 0.25) -> None:
    url = f"http://{host}:{port}/health"
    for _ in range(retries):
        try:
            with urlopen(url, timeout=1.5) as response:  # noqa: S310 - local health probe
                if response.status == 200:
                    return
        except (OSError, URLError):
            pass
        time.sleep(delay)
    raise RuntimeError(f"Service did not become healthy: {url}")


def create_log_paths(runtime_dir: Path, name: str) -> tuple[Path, Path]:
    log_dir = runtime_dir / "logs"
    log_dir.mkdir(parents=True, exist_ok=True)
    return log_dir / f"{name}.stdout.log", log_dir / f"{name}.stderr.log"


def spawn_service(spec: dict[str, Any], runtime_dir: Path, data_dir: Path, base_env: dict[str, str]) -> dict[str, Any]:
    env = base_env.copy()
    env["CC_DATA_DIR"] = str(data_dir)
    env["EVENT_BUS_ENABLED"] = env.get("EVENT_BUS_ENABLED", "0")
    env["EVENT_BUS_URL"] = env.get("EVENT_BUS_URL", "amqp://guest:guest@127.0.0.1:5672/")
    env["EVENT_BUS_EXCHANGE"] = env.get("EVENT_BUS_EXCHANGE", "mvpcc.domain.events")
    env["EVENT_BUS_DISPATCH_BATCH_SIZE"] = env.get("EVENT_BUS_DISPATCH_BATCH_SIZE", "50")
    env["EVENT_BUS_MAX_ATTEMPTS"] = env.get("EVENT_BUS_MAX_ATTEMPTS", "5")
    env["EVENT_BUS_POLL_INTERVAL_SECONDS"] = env.get("EVENT_BUS_POLL_INTERVAL_SECONDS", "2")
    env["EVENT_BUS_CONSUMER_ENABLED"] = env.get("EVENT_BUS_CONSUMER_ENABLED", "1")
    env["EVENT_BUS_AUDIT_QUEUE"] = env.get("EVENT_BUS_AUDIT_QUEUE", "mvpcc.audit.events")
    env["EVENT_BUS_REPORTING_QUEUE"] = env.get("EVENT_BUS_REPORTING_QUEUE", "mvpcc.reporting.events")
    env["ASTERISK_BRIDGE_ENABLED"] = env.get("ASTERISK_BRIDGE_ENABLED", "0")
    env["ASTERISK_AMI_EVENT_PREFIX"] = env.get("ASTERISK_AMI_EVENT_PREFIX", "MVPCC")
    env["ASTERISK_QUEUE_MAP_JSON"] = env.get("ASTERISK_QUEUE_MAP_JSON", "{}")
    env["ASTERISK_CALLCONTROL_ENABLED"] = env.get("ASTERISK_CALLCONTROL_ENABLED", "0")
    env["ASTERISK_CALLCONTROL_ACTION_TIMEOUT_SECONDS"] = env.get(
        "ASTERISK_CALLCONTROL_ACTION_TIMEOUT_SECONDS",
        "10",
    )
    env["ASTERISK_WEBRTC_ENABLED"] = env.get("ASTERISK_WEBRTC_ENABLED", "0")
    env["ASTERISK_WEBRTC_WS_URL"] = env.get("ASTERISK_WEBRTC_WS_URL", "")
    env["ASTERISK_WEBRTC_ICE_SERVERS_JSON"] = env.get("ASTERISK_WEBRTC_ICE_SERVERS_JSON", "[]")
    env["ASTERISK_OPERATOR_EXTENSION_MAP_JSON"] = env.get("ASTERISK_OPERATOR_EXTENSION_MAP_JSON", "{}")
    env["ASTERISK_BROWSER_SIP_MAP_JSON"] = env.get("ASTERISK_BROWSER_SIP_MAP_JSON", "{}")
    env["ASTERISK_TRANSFER_TARGET_MAP_JSON"] = env.get("ASTERISK_TRANSFER_TARGET_MAP_JSON", "{}")
    env["AI_PROVIDER"] = env.get("AI_PROVIDER", "stub")
    env["AI_API_BASE"] = env.get("AI_API_BASE", "")
    env["AI_API_KEY"] = env.get("AI_API_KEY", "")
    env["AI_MODEL"] = env.get("AI_MODEL", "stub-telegram-assistant")
    env["AI_TIMEOUT_SECONDS"] = env.get("AI_TIMEOUT_SECONDS", "20")
    env["AI_TELEGRAM_ENABLED"] = env.get("AI_TELEGRAM_ENABLED", "0")
    env["AI_TELEGRAM_ALWAYS_REPLY"] = env.get("AI_TELEGRAM_ALWAYS_REPLY", "0")
    env["AI_TELEGRAM_MAX_CONTEXT_MESSAGES"] = env.get("AI_TELEGRAM_MAX_CONTEXT_MESSAGES", "12")
    env["AI_TELEGRAM_MAX_KB_RESULTS"] = env.get("AI_TELEGRAM_MAX_KB_RESULTS", "3")
    env["AI_TELEGRAM_CONFIDENCE_HANDOFF_THRESHOLD"] = env.get(
        "AI_TELEGRAM_CONFIDENCE_HANDOFF_THRESHOLD",
        "0.65",
    )
    env["AI_WHATSAPP_ENABLED"] = env.get("AI_WHATSAPP_ENABLED", "0")
    env["AI_WHATSAPP_ALWAYS_REPLY"] = env.get("AI_WHATSAPP_ALWAYS_REPLY", "0")
    env["AI_WHATSAPP_MAX_CONTEXT_MESSAGES"] = env.get("AI_WHATSAPP_MAX_CONTEXT_MESSAGES", "12")
    env["AI_WHATSAPP_MAX_KB_RESULTS"] = env.get("AI_WHATSAPP_MAX_KB_RESULTS", "3")
    env["AI_WHATSAPP_CONFIDENCE_HANDOFF_THRESHOLD"] = env.get(
        "AI_WHATSAPP_CONFIDENCE_HANDOFF_THRESHOLD",
        "0.65",
    )
    env["AI_VOICE_RUNTIME_SERVICE_URL"] = env.get("AI_VOICE_RUNTIME_SERVICE_URL", "http://127.0.0.1:8018")
    env["AI_VOICE_ENABLED"] = env.get("AI_VOICE_ENABLED", "0")
    env["AI_VOICE_QUEUE_CONFIG_JSON"] = env.get("AI_VOICE_QUEUE_CONFIG_JSON", "{}")
    env["AI_VOICE_ASR_PROVIDER"] = env.get("AI_VOICE_ASR_PROVIDER", "yandex")
    env["AI_VOICE_TTS_PROVIDER"] = env.get("AI_VOICE_TTS_PROVIDER", "yandex")
    env["AI_VOICE_POLICY_MODE"] = env.get("AI_VOICE_POLICY_MODE", "v2_fast_conversational")
    env["AI_VOICE_V2_ENABLED"] = env.get("AI_VOICE_V2_ENABLED", "1")
    env["AI_VOICE_V2_QUEUE_CODES"] = env.get("AI_VOICE_V2_QUEUE_CODES", "voice_lab_ai")
    env["AI_VOICE_V2_ACK_MODE"] = env.get("AI_VOICE_V2_ACK_MODE", "immediate_short")
    env["AI_VOICE_V2_DUPLEX_ENABLED"] = env.get("AI_VOICE_V2_DUPLEX_ENABLED", "1")
    env["AI_VOICE_V2_STREAMING_ASR_BACKEND"] = env.get("AI_VOICE_V2_STREAMING_ASR_BACKEND", "yandex_speechkit")
    env["AI_VOICE_V2_STREAMING_ASR_BASE_URL"] = env.get("AI_VOICE_V2_STREAMING_ASR_BASE_URL", "http://127.0.0.1:8021")
    env["AI_VOICE_V2_STREAMING_ASR_TIMEOUT_SECONDS"] = env.get("AI_VOICE_V2_STREAMING_ASR_TIMEOUT_SECONDS", "4")
    env["AI_VOICE_V2_STREAMING_ASR_PARTIAL_POLL_ENABLED"] = env.get("AI_VOICE_V2_STREAMING_ASR_PARTIAL_POLL_ENABLED", "1")
    env["AI_VOICE_V2_STREAMING_ASR_MODEL"] = env.get("AI_VOICE_V2_STREAMING_ASR_MODEL", "base")
    env["AI_VOICE_V2_STREAMING_ASR_COMPUTE_TYPE"] = env.get("AI_VOICE_V2_STREAMING_ASR_COMPUTE_TYPE", "int8")
    env["AI_VOICE_V2_STREAMING_ASR_DEVICE"] = env.get("AI_VOICE_V2_STREAMING_ASR_DEVICE", "cpu")
    env["AI_VOICE_V2_STREAMING_ASR_CACHE_DIR"] = env.get("AI_VOICE_V2_STREAMING_ASR_CACHE_DIR", str(ROOT / ".models" / "faster_whisper"))
    env["AI_VOICE_V2_STREAMING_ASR_SUPPORTED_LANGUAGES"] = env.get("AI_VOICE_V2_STREAMING_ASR_SUPPORTED_LANGUAGES", "ru")
    env["AI_VOICE_V2_PREBAKED_ACK_ENABLED"] = env.get("AI_VOICE_V2_PREBAKED_ACK_ENABLED", "1")
    env["AI_VOICE_V2_PREBAKED_ACK_DIR"] = env.get("AI_VOICE_V2_PREBAKED_ACK_DIR", str(DATA_DIR / "voice_v2_ack_bank"))
    env["AI_VOICE_V2_STREAMING_TTS"] = env.get("AI_VOICE_V2_STREAMING_TTS", "1")
    env["AI_VOICE_V2_PARTIAL_ASR"] = env.get("AI_VOICE_V2_PARTIAL_ASR", "1")
    env["AI_VOICE_V2_EMOTIVE_ACK_ENABLED"] = env.get("AI_VOICE_V2_EMOTIVE_ACK_ENABLED", "1")
    env["AI_VOICE_V2_EMOTIVE_ACK_RU_ONLY"] = env.get("AI_VOICE_V2_EMOTIVE_ACK_RU_ONLY", "1")
    env["AI_VOICE_MAX_CONTEXT_SEGMENTS"] = env.get("AI_VOICE_MAX_CONTEXT_SEGMENTS", "12")
    env["AI_VOICE_HANDOFF_TIMEOUT_SECONDS"] = env.get("AI_VOICE_HANDOFF_TIMEOUT_SECONDS", "8")
    env["AI_VOICE_VAD_TRAILING_SILENCE_MS"] = env.get("AI_VOICE_VAD_TRAILING_SILENCE_MS", "400")
    env["AI_VOICE_RUNTIME_TRUSTED_SERVICE_SUBJECTS"] = env.get(
        "AI_VOICE_RUNTIME_TRUSTED_SERVICE_SUBJECTS",
        "svc:ai-voice-runtime",
    )
    env.update(build_gateway_env(env))
    env["ASTERISK_CALLCONTROL_CLAIM_CONTEXT"] = env.get("ASTERISK_CALLCONTROL_CLAIM_CONTEXT", "mvpcc-claim")
    env["ASTERISK_CALLCONTROL_TRANSFER_CONTEXT"] = env.get(
        "ASTERISK_CALLCONTROL_TRANSFER_CONTEXT",
        "mvpcc-transfer",
    )
    if spec["name"] == "recording":
        env["CC_RECORDINGS_DIR"] = str((data_dir / "recordings").resolve())
        env["RECORDING_MAX_BYTES"] = env.get("RECORDING_MAX_BYTES", "26214400")
    if spec["name"] == "streaming-asr-sidecar":
        Path(env["AI_VOICE_V2_STREAMING_ASR_CACHE_DIR"]).mkdir(parents=True, exist_ok=True)

    stdout_path, stderr_path = create_log_paths(runtime_dir, str(spec["name"]))
    creationflags = 0
    if os.name == "nt":
        creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)

    cmd = [
        sys.executable,
        "-m",
        "uvicorn",
        str(spec["module"]),
        "--host",
        service_bind_host(str(spec["name"]), env),
        "--port",
        str(spec["port"]),
    ]

    with stdout_path.open("ab") as stdout_file, stderr_path.open("ab") as stderr_file:
        proc = subprocess.Popen(
            cmd,
            cwd=str(ROOT),
            env=env,
            stdin=subprocess.DEVNULL,
            stdout=stdout_file,
            stderr=stderr_file,
            creationflags=creationflags,
        )

    time.sleep(0.15)
    if proc.poll() is not None:
        raise RuntimeError(f"{spec['name']} failed to start; check {stderr_path}")

    return {
        "name": spec["name"],
        "module": spec["module"],
        "port": spec["port"],
        "pid": proc.pid,
        "stdout_log": str(stdout_path),
        "stderr_log": str(stderr_path),
    }


def terminate_pid(pid: int) -> None:
    if not is_pid_running(pid):
        return
    try:
        os.kill(pid, signal.SIGTERM)
    except OSError:
        return

    for _ in range(20):
        if not is_pid_running(pid):
            return
        time.sleep(0.2)

    if os.name == "nt":
        subprocess.run(
            ["taskkill", "/PID", str(pid), "/T", "/F"],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            check=False,
        )
        return

    try:
        os.kill(pid, signal.SIGKILL)
    except OSError:
        pass


def running_services(manifest: dict[str, Any] | None) -> list[dict[str, Any]]:
    if not manifest or not isinstance(manifest, dict):
        return []
    active: list[dict[str, Any]] = []
    for svc in manifest.get("services", []):
        if not isinstance(svc, dict):
            continue
        try:
            pid = int(svc.get("pid", 0))
        except Exception:  # noqa: BLE001
            continue
        if is_pid_running(pid):
            active.append(svc)
    return active


def start_stack(runtime_dir: Path, data_dir: Path, force_restart: bool, env: dict[str, str]) -> int:
    service_specs = build_service_specs(env)
    current = load_manifest(runtime_dir)
    active = running_services(current)
    if active and not force_restart:
        print("Local stack is already running.")
        print(f"Base URL: {current.get('base_url', public_base_url(env))}")
        print(f"Logs: {runtime_dir / 'logs'}")
        print("Use `python scripts\\local_stack.py stop` first or rerun with --force-restart.")
        return 0

    if active and force_restart:
        stop_stack(runtime_dir)

    if current and not active:
        remove_manifest(runtime_dir)

    runtime_dir.mkdir(parents=True, exist_ok=True)
    data_dir.mkdir(parents=True, exist_ok=True)

    launched: list[dict[str, Any]] = []
    try:
        for spec in service_specs:
            meta = spawn_service(spec, runtime_dir, data_dir, env)
            wait_for_health(service_health_host(str(spec["name"]), env), int(meta["port"]))
            if not is_pid_running(int(meta["pid"])):
                listener_pid = resolve_listener_pid(int(meta["port"]))
                if listener_pid and is_pid_running(listener_pid):
                    meta["pid"] = listener_pid
                else:
                    raise RuntimeError(f"{meta['name']} exited before becoming ready; check {meta['stderr_log']}")
            launched.append(meta)
            print(f"[ok] {meta['name']} on {meta['port']} (pid {meta['pid']})")
    except Exception as exc:
        print(f"[fail] {exc}")
        print(f"Check logs: {runtime_dir / 'logs'}")
        print("If ports are already busy, stop old local processes and rerun.")
        for meta in reversed(launched):
            terminate_pid(int(meta["pid"]))
        return 1

    save_manifest(runtime_dir, build_manifest_payload(runtime_dir, data_dir, launched, env))
    print("")
    print("Local stack is ready.")
    print(f"UI: {public_base_url(env)}/")
    print(f"Logs: {runtime_dir / 'logs'}")
    print("Stop: python scripts\\local_stack.py stop")
    return 0


def stop_stack(runtime_dir: Path) -> int:
    current = load_manifest(runtime_dir)
    if not current:
        print("Local stack is not running (no manifest found).")
        return 0

    services = current.get("services", []) if isinstance(current, dict) else []
    for meta in reversed(services):
        if not isinstance(meta, dict):
            continue
        name = str(meta.get("name", "service"))
        try:
            pid = int(meta.get("pid", 0))
        except Exception:  # noqa: BLE001
            print(f"[skip] {name} (pid invalid)")
            continue
        if is_pid_running(pid):
            terminate_pid(pid)
            print(f"[stopped] {name} (pid {pid})")
        else:
            print(f"[skip] {name} (pid {pid}) already stopped")

    remove_manifest(runtime_dir)
    print("Local stack is stopped.")
    return 0


def status_stack(runtime_dir: Path, env: dict[str, str]) -> int:
    current = load_manifest(runtime_dir)
    if not current:
        print("Local stack is not running.")
        return 0

    print(f"Base URL: {current.get('base_url', public_base_url(env))}")
    print(f"Data dir: {current.get('data_dir', str(DEFAULT_DATA_DIR))}")
    print(f"Logs: {runtime_dir / 'logs'}")
    print("")
    services = current.get("services", []) if isinstance(current, dict) else []
    for meta in services:
        if not isinstance(meta, dict):
            continue
        try:
            pid = int(meta.get("pid", 0))
        except Exception:  # noqa: BLE001
            pid = 0
        label = "running" if is_pid_running(pid) else "stopped"
        print(f"- {meta.get('name')}: {label} on {meta.get('port')} (pid {pid})")
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description="Manage the local MVP stack without opening extra consoles")
    parser.add_argument("command", choices=["start", "stop", "status"], help="Action to perform")
    parser.add_argument(
        "--runtime-dir",
        default=str(DEFAULT_RUNTIME_DIR),
        help="Directory for manifest and logs",
    )
    parser.add_argument(
        "--data-dir",
        default=None,
        help="Directory for local runtime data and recordings",
    )
    parser.add_argument(
        "--env-file",
        action="append",
        default=[],
        help="Additional env file to apply after the default stack env files",
    )
    parser.add_argument(
        "--force-restart",
        action="store_true",
        help="Stop the existing managed stack before starting a new one",
    )
    args = parser.parse_args()

    extra_env_files: list[Path] = []
    for raw_path in args.env_file:
        path = Path(raw_path)
        if not path.is_absolute():
            path = (ROOT / path).resolve()
        extra_env_files.append(path)

    env = load_stack_env(extra_env_files)

    runtime_dir = Path(args.runtime_dir)
    if not runtime_dir.is_absolute():
        runtime_dir = (ROOT / runtime_dir).resolve()

    raw_data_dir = args.data_dir or env.get("CC_DATA_DIR") or str(DEFAULT_DATA_DIR)
    data_dir = Path(raw_data_dir)
    if not data_dir.is_absolute():
        data_dir = (ROOT / data_dir).resolve()

    if args.command == "start":
        return start_stack(runtime_dir, data_dir, args.force_restart, env)
    if args.command == "stop":
        return stop_stack(runtime_dir)
    return status_stack(runtime_dir, env)


if __name__ == "__main__":
    raise SystemExit(main())
