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

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"

if [[ -z "${ULTRACLOUD_E2E_IN_NIX:-}" ]]; then
  exec nix develop "$ROOT" -c env ULTRACLOUD_E2E_IN_NIX=1 bash "$0" "$@"
fi

run_chainfire_server_bin() {
  if [[ -n "${ULTRACLOUD_CHAINFIRE_SERVER_BIN:-}" ]]; then
    "$ULTRACLOUD_CHAINFIRE_SERVER_BIN" "$@"
  else
    cargo run --manifest-path "$ROOT/chainfire/Cargo.toml" -p chainfire-server -- "$@"
  fi
}

run_deployer_ctl_bin() {
  if [[ -n "${ULTRACLOUD_DEPLOYER_CTL_BIN:-}" ]]; then
    "$ULTRACLOUD_DEPLOYER_CTL_BIN" "$@"
  else
    cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p deployer-ctl -- "$@"
  fi
}

run_node_agent_bin() {
  if [[ -n "${ULTRACLOUD_NODE_AGENT_BIN:-}" ]]; then
    "$ULTRACLOUD_NODE_AGENT_BIN" "$@"
  else
    cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p node-agent -- "$@"
  fi
}

run_fleet_scheduler_bin() {
  if [[ -n "${ULTRACLOUD_FLEET_SCHEDULER_BIN:-}" ]]; then
    "$ULTRACLOUD_FLEET_SCHEDULER_BIN" "$@"
  else
    cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p fleet-scheduler -- "$@"
  fi
}

tmp_dir="$(mktemp -d)"
cf_pid=""
controller_pids=()

cleanup() {
  set +e

  for pid in "${controller_pids[@]}"; do
    kill "$pid" 2>/dev/null || true
    wait "$pid" 2>/dev/null || true
  done

  if [[ -d "$tmp_dir/pids" ]]; then
    while IFS= read -r -d '' pid_file; do
      [[ -f "$pid_file" ]] || continue
      kill "$(cat "$pid_file")" 2>/dev/null || true
    done < <(find "$tmp_dir/pids" -type f -name '*.pid' -print0 2>/dev/null)
  fi

  if [[ -n "$cf_pid" ]]; then
    kill "$cf_pid" 2>/dev/null || true
    wait "$cf_pid" 2>/dev/null || true
  fi

  if [[ "${ULTRACLOUD_KEEP_TMP:-}" == "1" ]]; then
    echo "Keeping temporary directory: $tmp_dir" >&2
  else
    rm -rf "$tmp_dir"
  fi
}

trap cleanup EXIT

free_port() {
  python3 - <<'PY'
import socket
s = socket.socket()
s.bind(("127.0.0.1", 0))
print(s.getsockname()[1])
s.close()
PY
}

wait_for_port() {
  local host="$1"
  local port="$2"
  local timeout_secs="${3:-60}"
  local deadline=$((SECONDS + timeout_secs))

  while (( SECONDS < deadline )); do
    if python3 - "$host" "$port" <<'PY'
import socket
import sys

host = sys.argv[1]
port = int(sys.argv[2])

with socket.socket() as sock:
    sock.settimeout(0.5)
    try:
        sock.connect((host, port))
    except OSError:
        raise SystemExit(1)
raise SystemExit(0)
PY
    then
      return 0
    fi
    sleep 1
  done

  echo "timed out waiting for ${host}:${port}" >&2
  return 1
}

api_port="$(free_port)"
http_port="$(free_port)"
raft_port="$(free_port)"
gossip_port="$(free_port)"
metrics_port="$(free_port)"

cat >"$tmp_dir/chainfire.toml" <<EOF
[node]
id = 1
name = "chainfire-1"
role = "control_plane"

[storage]
data_dir = "$tmp_dir/chainfire-data"

[network]
api_addr = "127.0.0.1:${api_port}"
http_addr = "127.0.0.1:${http_port}"
raft_addr = "127.0.0.1:${raft_port}"
gossip_addr = "127.0.0.1:${gossip_port}"

[cluster]
id = 1
initial_members = []
bootstrap = true

[raft]
role = "voter"
EOF

mkdir -p "$tmp_dir/pids"

echo "Starting ChainFire on 127.0.0.1:${api_port}"
run_chainfire_server_bin \
  --config "$tmp_dir/chainfire.toml" \
  --metrics-port "$metrics_port" \
  >"$tmp_dir/chainfire.log" 2>&1 &
cf_pid="$!"

wait_for_port "127.0.0.1" "$api_port" 120

cat >"$tmp_dir/cluster.yaml" <<'EOF'
cluster:
  cluster_id: test-cluster
  environment: dev

node_classes:
  - name: worker-linux
    description: Standard worker nodes
    nix_profile: profiles/worker-linux
    roles:
      - worker
    labels:
      tier: general

pools:
  - name: general
    description: Default capacity pool
    node_class: worker-linux
    min_size: 2
    max_size: 10
    labels:
      env: dev

nodes:
  - node_id: node01
    hostname: node01
    ip: 127.0.0.2
    pool: general
    failure_domain: rack-a
    state: pending
  - node_id: node02
    hostname: node02
    ip: 127.0.0.3
    pool: general
    failure_domain: rack-b
    state: pending

services:
  - name: api
    ports:
      http: 18080
    protocol: http
    schedule:
      replicas: 2
      placement:
        roles:
          - worker
        pools:
          - general
        node_classes:
          - worker-linux
        match_labels:
          tier: general
        spread_by_label: failure_domain
        max_instances_per_node: 1
      instance_port: 18080
      process:
        command: python3
        args:
          - -m
          - http.server
          - ${INSTANCE_PORT}
          - --bind
          - ${INSTANCE_IP}
      health_check:
        type: http
        path: /
        interval_secs: 1
        timeout_secs: 2
  - name: worker
    ports:
      http: 18081
    protocol: http
    depends_on:
      - service: api
        condition: healthy
        min_ready: 2
    schedule:
      replicas: 2
      placement:
        roles:
          - worker
        pools:
          - general
        node_classes:
          - worker-linux
        match_labels:
          tier: general
        spread_by_label: failure_domain
        max_instances_per_node: 1
      instance_port: 18081
      process:
        command: python3
        args:
          - -m
          - http.server
          - ${INSTANCE_PORT}
          - --bind
          - ${INSTANCE_IP}
      health_check:
        type: http
        path: /
        interval_secs: 1
        timeout_secs: 2
EOF

cat >"$tmp_dir/cluster-scaled.yaml" <<'EOF'
cluster:
  cluster_id: test-cluster
  environment: dev

node_classes:
  - name: worker-linux
    description: Standard worker nodes
    nix_profile: profiles/worker-linux
    roles:
      - worker
    labels:
      tier: general

pools:
  - name: general
    description: Default capacity pool
    node_class: worker-linux
    min_size: 2
    max_size: 10
    labels:
      env: dev

nodes:
  - node_id: node01
    hostname: node01
    ip: 127.0.0.2
    pool: general
    failure_domain: rack-a
    state: active
  - node_id: node02
    hostname: node02
    ip: 127.0.0.3
    pool: general
    failure_domain: rack-b
    state: active

services:
  - name: api
    ports:
      http: 18080
    protocol: http
    schedule:
      replicas: 1
      placement:
        roles:
          - worker
        pools:
          - general
        node_classes:
          - worker-linux
        match_labels:
          tier: general
        spread_by_label: failure_domain
        max_instances_per_node: 1
      instance_port: 18080
      process:
        command: python3
        args:
          - -m
          - http.server
          - ${INSTANCE_PORT}
          - --bind
          - ${INSTANCE_IP}
      health_check:
        type: http
        path: /
        interval_secs: 1
        timeout_secs: 2
  - name: worker
    ports:
      http: 18081
    protocol: http
    depends_on:
      - service: api
        condition: healthy
        min_ready: 1
    schedule:
      replicas: 1
      placement:
        roles:
          - worker
        pools:
          - general
        node_classes:
          - worker-linux
        match_labels:
          tier: general
        spread_by_label: failure_domain
        max_instances_per_node: 1
      instance_port: 18081
      process:
        command: python3
        args:
          - -m
          - http.server
          - ${INSTANCE_PORT}
          - --bind
          - ${INSTANCE_IP}
      health_check:
        type: http
        path: /
        interval_secs: 1
        timeout_secs: 2
EOF

endpoint="http://127.0.0.1:${api_port}"

run_deployer_ctl() {
  run_deployer_ctl_bin \
    --chainfire-endpoint "$endpoint" \
    --cluster-id test-cluster \
    "$@"
}

run_node_agent_bg() {
  local node_id="$1"
  local pid_dir="$tmp_dir/pids/$node_id"
  mkdir -p "$pid_dir"
  run_node_agent_bin \
    --chainfire-endpoint "$endpoint" \
    --cluster-id test-cluster \
    --node-id "$node_id" \
    --pid-dir "$pid_dir" \
    --interval-secs 1 \
    --apply \
    >"$tmp_dir/node-agent-${node_id}.log" 2>&1 &
  controller_pids+=("$!")
}

run_scheduler_bg() {
  run_fleet_scheduler_bin \
    --chainfire-endpoint "$endpoint" \
    --cluster-id test-cluster \
    --interval-secs 300 \
    >"$tmp_dir/fleet-scheduler.log" 2>&1 &
  controller_pids+=("$!")
}

wait_for_service_state() {
  local service_name="$1"
  local expected_phase="$2"
  local expected_instances="$3"
  local expected_state="${4:--}"
  local timeout_secs="${5:-120}"
  local inspect_path="$tmp_dir/${service_name}-wait.json"
  local deadline=$((SECONDS + timeout_secs))

  while (( SECONDS < deadline )); do
    if run_deployer_ctl service inspect --name "$service_name" --include-instances >"$inspect_path" 2>/dev/null; then
      if python3 - "$inspect_path" "$expected_phase" "$expected_instances" "$expected_state" <<'PY'
import json
import sys

payload = json.load(open(sys.argv[1], "r", encoding="utf-8"))
expected_phase = sys.argv[2]
expected_instances = int(sys.argv[3])
expected_state = sys.argv[4]

status = payload.get("status") or {}
instances = payload.get("instances") or []

if status.get("phase") != expected_phase:
    raise SystemExit(1)
if len(instances) != expected_instances:
    raise SystemExit(1)
if expected_state != "-":
    actual_states = sorted(instance.get("state") for instance in instances)
    if actual_states != [expected_state] * expected_instances:
        raise SystemExit(1)
PY
      then
        return 0
      fi
    fi
    sleep 1
  done

  echo "timed out waiting for ${service_name} phase=${expected_phase} instances=${expected_instances}" >&2
  [[ -f "$inspect_path" ]] && cat "$inspect_path" >&2
  return 1
}

wait_for_endpoint_convergence() {
  local timeout_secs="${1:-60}"
  local deadline=$((SECONDS + timeout_secs))

  while (( SECONDS < deadline )); do
    if python3 - <<'PY'
import socket
import urllib.request

with urllib.request.urlopen("http://127.0.0.2:18080/", timeout=5) as response:
    if response.status != 200:
        raise SystemExit(f"node01 endpoint returned {response.status}")
with urllib.request.urlopen("http://127.0.0.2:18081/", timeout=5) as response:
    if response.status != 200:
        raise SystemExit(f"node01 worker endpoint returned {response.status}")

for port, label in ((18080, "api"), (18081, "worker")):
    sock = socket.socket()
    sock.settimeout(1.5)
    try:
        sock.connect(("127.0.0.3", port))
    except OSError:
        pass
    else:
        raise SystemExit(f"node02 {label} endpoint still accepts connections after scale-down")
    finally:
        sock.close()
PY
    then
      return 0
    fi
    sleep 1
  done

  echo "timed out waiting for endpoint convergence after scale-down" >&2
  return 1
}

echo "Applying cluster declaration"
run_deployer_ctl apply --config "$tmp_dir/cluster.yaml"

echo "Starting watch-driven controllers"
run_scheduler_bg

echo "Waiting for worker to remain blocked until api becomes healthy"
wait_for_service_state worker blocked 0 - 120

echo "Validating dependency block before api is healthy"
run_deployer_ctl dump --prefix "ultracloud/clusters/test-cluster/instances/worker/" >"$tmp_dir/worker-blocked.dump"
python3 - "$tmp_dir/worker-blocked.dump" <<'PY'
import sys

path = sys.argv[1]
lines = [line.strip() for line in open(path, "r", encoding="utf-8") if line.strip()]
if lines:
    raise SystemExit(f"expected no worker instances before api is healthy, found {len(lines)}")
print("worker instances correctly blocked before dependency becomes healthy")
PY

run_deployer_ctl dump --prefix "ultracloud/clusters/test-cluster/service-statuses/worker" >"$tmp_dir/worker-status-blocked.dump"
python3 - "$tmp_dir/worker-status-blocked.dump" <<'PY'
import json
import sys

path = sys.argv[1]
statuses = []
with open(path, "r", encoding="utf-8") as handle:
    for line in handle:
        line = line.strip()
        if not line:
            continue
        marker = " value="
        if marker not in line:
            continue
        statuses.append(json.loads(line.split(marker, 1)[1]))

if len(statuses) != 1:
    raise SystemExit(f"expected exactly one worker service status, found {len(statuses)}")

status = statuses[0]
if status.get("phase") != "blocked":
    raise SystemExit(f"expected worker phase=blocked, found {status.get('phase')}")
blockers = status.get("blockers") or []
if not blockers or "dependency api has 0/2 healthy instance(s)" not in blockers[0]:
    raise SystemExit(f"unexpected blockers: {blockers}")

print("worker service status reports dependency block")
PY

run_deployer_ctl service inspect --name worker >"$tmp_dir/worker-inspect-blocked.json"
python3 - "$tmp_dir/worker-inspect-blocked.json" <<'PY'
import json
import sys

payload = json.load(open(sys.argv[1], "r", encoding="utf-8"))
status = payload.get("status") or {}

if payload.get("spec", {}).get("name") != "worker":
    raise SystemExit("service inspect did not return worker spec")
if status.get("phase") != "blocked":
    raise SystemExit(f"expected worker inspect phase=blocked, found {status.get('phase')}")

print("service inspect reports blocked dependency state")
PY

echo "Starting node agents after dependency block is confirmed"
run_node_agent_bg node01
run_node_agent_bg node02

echo "Reconciling processes and health for api"
wait_for_service_state api healthy 2 healthy 120

run_deployer_ctl service inspect --name api --include-instances >"$tmp_dir/api-inspect-healthy.json"
python3 - "$tmp_dir/api-inspect-healthy.json" <<'PY'
import json
import sys

payload = json.load(open(sys.argv[1], "r", encoding="utf-8"))
status = payload.get("status") or {}
instances = payload.get("instances") or []

if status.get("phase") != "healthy":
    raise SystemExit(f"expected api inspect phase=healthy before scheduler rerun, found {status.get('phase')}")
if len(instances) != 2:
    raise SystemExit(f"expected 2 api instances from service inspect, found {len(instances)}")

print("api service inspect refreshed to healthy from node-agent updates")
PY

echo "Waiting for dependent worker service to converge automatically"
wait_for_service_state worker healthy 2 healthy 120

echo "Validating HTTP endpoints"
python3 - <<'PY'
import urllib.request

for address in (
    "http://127.0.0.2:18080/",
    "http://127.0.0.3:18080/",
    "http://127.0.0.2:18081/",
    "http://127.0.0.3:18081/",
):
    with urllib.request.urlopen(address, timeout=5) as response:
        body = response.read().decode("utf-8")
        if response.status != 200:
            raise SystemExit(f"{address} returned {response.status}")
        if "Directory listing" not in body and "DOCTYPE" not in body:
            raise SystemExit(f"{address} returned unexpected body")
print("HTTP endpoints are healthy")
PY

echo "Inspecting instance state in ChainFire"
run_deployer_ctl dump --prefix "ultracloud/clusters/test-cluster/instances/api/" >"$tmp_dir/instances.dump"
python3 - "$tmp_dir/instances.dump" <<'PY'
import json
import sys

path = sys.argv[1]
instances = []

with open(path, "r", encoding="utf-8") as handle:
    for line in handle:
        line = line.strip()
        if not line:
            continue
        marker = " value="
        if marker not in line:
            continue
        value = line.split(marker, 1)[1]
        instances.append(json.loads(value))

if len(instances) != 2:
    raise SystemExit(f"expected 2 scheduled instances, found {len(instances)}")

node_ids = sorted(instance["node_id"] for instance in instances)
states = sorted(instance.get("state") for instance in instances)

if node_ids != ["node01", "node02"]:
    raise SystemExit(f"unexpected node placement: {node_ids}")
if states != ["healthy", "healthy"]:
    raise SystemExit(f"unexpected health states: {states}")

print("Observed two healthy scheduled instances across node01 and node02")
PY

run_deployer_ctl dump --prefix "ultracloud/clusters/test-cluster/instances/worker/" >"$tmp_dir/worker-instances.dump"
python3 - "$tmp_dir/worker-instances.dump" <<'PY'
import json
import sys

path = sys.argv[1]
instances = []

with open(path, "r", encoding="utf-8") as handle:
    for line in handle:
        line = line.strip()
        if not line:
            continue
        marker = " value="
        if marker not in line:
            continue
        value = line.split(marker, 1)[1]
        instances.append(json.loads(value))

if len(instances) != 2:
    raise SystemExit(f"expected 2 worker instances, found {len(instances)}")

node_ids = sorted(instance["node_id"] for instance in instances)
states = sorted(instance.get("state") for instance in instances)

if node_ids != ["node01", "node02"]:
    raise SystemExit(f"unexpected worker placement: {node_ids}")
if states != ["healthy", "healthy"]:
    raise SystemExit(f"unexpected worker states: {states}")

print("Observed two healthy dependent worker instances across node01 and node02")
PY

run_deployer_ctl service inspect --name worker --include-instances >"$tmp_dir/worker-inspect-healthy.json"
python3 - "$tmp_dir/worker-inspect-healthy.json" <<'PY'
import json
import sys

payload = json.load(open(sys.argv[1], "r", encoding="utf-8"))
status = payload.get("status") or {}
instances = payload.get("instances") or []

if status.get("phase") != "healthy":
    raise SystemExit(f"expected worker inspect phase=healthy, found {status.get('phase')}")
if len(instances) != 2:
    raise SystemExit(f"expected 2 worker instances from service inspect, found {len(instances)}")

print("service inspect reports healthy dependent instances")
PY

echo "Applying scaled declaration"
run_deployer_ctl apply --config "$tmp_dir/cluster-scaled.yaml" --prune

echo "Waiting for scaled services to converge automatically"
wait_for_service_state api healthy 1 healthy 120
wait_for_service_state worker healthy 1 healthy 120

echo "Inspecting scaled instance state in ChainFire"
run_deployer_ctl dump --prefix "ultracloud/clusters/test-cluster/instances/api/" >"$tmp_dir/instances-scaled.dump"
python3 - "$tmp_dir/instances-scaled.dump" <<'PY'
import json
import sys

path = sys.argv[1]
instances = []

with open(path, "r", encoding="utf-8") as handle:
    for line in handle:
        line = line.strip()
        if not line:
            continue
        marker = " value="
        if marker not in line:
            continue
        value = line.split(marker, 1)[1]
        instances.append(json.loads(value))

if len(instances) != 1:
    raise SystemExit(f"expected 1 scheduled instance after scale-down, found {len(instances)}")

instance = instances[0]
if instance["node_id"] != "node01":
    raise SystemExit(f"expected remaining instance on node01, found {instance['node_id']}")
if instance.get("state") != "healthy":
    raise SystemExit(f"expected remaining instance to be healthy, found {instance.get('state')}")

print("Observed one healthy scheduled instance on node01 after scale-down")
PY

run_deployer_ctl dump --prefix "ultracloud/clusters/test-cluster/instances/worker/" >"$tmp_dir/worker-instances-scaled.dump"
python3 - "$tmp_dir/worker-instances-scaled.dump" <<'PY'
import json
import sys

path = sys.argv[1]
instances = []

with open(path, "r", encoding="utf-8") as handle:
    for line in handle:
        line = line.strip()
        if not line:
            continue
        marker = " value="
        if marker not in line:
            continue
        value = line.split(marker, 1)[1]
        instances.append(json.loads(value))

if len(instances) != 1:
    raise SystemExit(f"expected 1 worker instance after scale-down, found {len(instances)}")

instance = instances[0]
if instance["node_id"] != "node01":
    raise SystemExit(f"expected remaining worker instance on node01, found {instance['node_id']}")
if instance.get("state") != "healthy":
    raise SystemExit(f"expected remaining worker instance to be healthy, found {instance.get('state')}")

print("Observed one healthy dependent worker instance on node01 after scale-down")
PY

run_deployer_ctl service inspect --name worker --include-instances >"$tmp_dir/worker-inspect-scaled.json"
python3 - "$tmp_dir/worker-inspect-scaled.json" <<'PY'
import json
import sys

payload = json.load(open(sys.argv[1], "r", encoding="utf-8"))
status = payload.get("status") or {}
instances = payload.get("instances") or []

if status.get("phase") != "healthy":
    raise SystemExit(f"expected scaled worker inspect phase=healthy, found {status.get('phase')}")
if len(instances) != 1:
    raise SystemExit(f"expected 1 scaled worker instance from service inspect, found {len(instances)}")

print("service inspect reports scaled healthy worker state without waiting for scheduler status refresh")
PY

echo "Validating endpoint convergence after scale-down"
wait_for_endpoint_convergence 60
echo "Endpoint convergence validated"

echo "Fleet scheduler E2E verification passed"
