#!/usr/bin/env bash
# Host port bookkeeping shared by init (which picks ports) and up (which has to
# bind them). Sourced, not executed. Expects RUN_DIR to be set.

# ---- machine-wide reservations ----------------------------------------------
# Ports another workspace on this machine already claimed. A stack that is
# currently down still owns its ports, so "nothing is listening" is not enough
# to hand the same port to a second stack.
PORT_REGISTRY="${RUN_PORTS_FILE:-${HOME}/.run/ports.json}"
RESERVED=""

load_reservations() {
  [ -f "${PORT_REGISTRY}" ] || return 0
  RESERVED="$(python3 - "${PORT_REGISTRY}" "${WORKSPACE_DIR:-${RUN_DIR}}" <<'PY'
import json, os, sys

path, run_dir = sys.argv[1:3]
try:
    with open(path, encoding="utf-8") as fh:
        data = json.load(fh)
except (OSError, ValueError):
    sys.exit(0)

for workspace, entry in (data or {}).items():
    if workspace == run_dir or not os.path.isdir(workspace):
        continue
    name = (entry or {}).get("project") or os.path.basename(workspace)
    for key, port in ((entry or {}).get("ports") or {}).items():
        print(f"{port}:{name}/{key}")
PY
)" || RESERVED=""
}

reserved_by() {
  local entry
  for entry in ${RESERVED}; do
    case "${entry}" in
      "$1":*) echo "${entry#*:}"; return 0 ;;
    esac
  done
  return 1
}

port_in_use() {
  if command -v lsof >/dev/null 2>&1; then
    lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1
  else
    python3 - "$1" <<'PY'
import socket, sys
s = socket.socket()
try:
    s.bind(("127.0.0.1", int(sys.argv[1])))
except OSError:
    sys.exit(0)
finally:
    s.close()
sys.exit(1)
PY
  fi
}

# The container publishing a port, if any. Checked first: on a VM-backed docker
# (colima, Docker Desktop) every published port is held on the host by the same
# forwarder process, so lsof alone only ever says "ssh".
docker_port_holder() {
  docker ps --format '{{.Names}}\t{{.Ports}}' 2>/dev/null |
    awk -F'\t' -v port="$1" '
      $2 ~ (":" port "->") { print $1; exit }
    '
}

# Who is listening on a port, as a "command (pid)" string, so a clash names the
# process instead of leaving it to be hunted down by hand.
port_holder() {
  local container
  container="$(docker_port_holder "$1")"
  if [ -n "${container}" ]; then
    echo "container ${container}"
    return 0
  fi
  command -v lsof >/dev/null 2>&1 || return 1
  lsof -nP -iTCP:"$1" -sTCP:LISTEN -F cp 2>/dev/null |
    awk '/^p/ { pid = substr($0, 2) } /^c/ { print substr($0, 2) " (" pid ")"; exit }'
}

# ---- per-project service cache ----------------------------------------------
# Autocomplete must not invent a global service list. Each workspace's compose
# services are remembered under ~/.run/services.json when you run the stack.
SERVICE_REGISTRY="${RUN_SERVICES_FILE:-${HOME}/.run/services.json}"

save_workspace_services() {
  local services="${1:-}" project
  [ -n "${services}" ] || return 0
  [ -n "${WORKSPACE_DIR:-}" ] || return 0
  project="${COMPOSE_PROJECT_NAME:-${PROJECT_NAME:-$(basename "${WORKSPACE_DIR}")}}"
  mkdir -p "$(dirname "${SERVICE_REGISTRY}")" 2>/dev/null || return 0
  python3 - "${SERVICE_REGISTRY}" "${WORKSPACE_DIR}" "${project}" "${services}" <<'PY' || true
import json, os, sys

path, workspace, project, raw = sys.argv[1:5]
services = [line.strip() for line in raw.splitlines() if line.strip()]
if not services:
    raise SystemExit(0)

data = {}
if os.path.isfile(path):
    try:
        with open(path, encoding="utf-8") as fh:
            data = json.load(fh) or {}
    except (OSError, ValueError):
        data = {}

data[workspace] = {"project": project, "services": services}
data = {k: v for k, v in data.items() if os.path.isdir(k)}

tmp = f"{path}.tmp"
with open(tmp, "w", encoding="utf-8") as fh:
    json.dump(data, fh, indent=2, sort_keys=True)
    fh.write("\n")
os.replace(tmp, path)
PY
}

# Print cached services for this workspace (one per line). Empty if unknown.
load_workspace_services() {
  [ -n "${WORKSPACE_DIR:-}" ] || return 0
  [ -f "${SERVICE_REGISTRY}" ] || return 0
  python3 - "${SERVICE_REGISTRY}" "${WORKSPACE_DIR}" <<'PY'
import json, os, sys

path, workspace = sys.argv[1:3]
try:
    with open(path, encoding="utf-8") as fh:
        data = json.load(fh) or {}
except (OSError, ValueError):
    raise SystemExit(0)

entry = data.get(workspace) or {}
for name in entry.get("services") or []:
    if name:
        print(name)
PY
}

# Resolve live compose services when possible, remember them, then print.
# Falls back to the per-project cache — never a hard-coded service list.
remember_and_list_services() {
  local services=""
  if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
    if [ ! -f "${RUN_DIR}/docker-compose.packages.yml" ]; then
      bash "${PACKAGE_DIR}/scripts/gen-packages.sh" >/dev/null 2>&1 || true
    fi
    bash "${PACKAGE_DIR}/scripts/gen-apps.sh" >/dev/null 2>&1 || true
    bash "${PACKAGE_DIR}/scripts/gen-resources.sh" >/dev/null 2>&1 || true
    services="$(dc config --services 2>/dev/null | sort -u)" || services=""
  fi
  if [ -n "${services}" ]; then
    save_workspace_services "${services}"
    printf '%s\n' "${services}"
    return 0
  fi
  load_workspace_services
}
