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

ACTION="${1:-up}"
INSTALL_DIR="${TRUCKWASH_INSTALL_DIR:-/opt/truckwash-edge-agent}"
CONFIG_PATH="$INSTALL_DIR/config.json"
COMPOSE_FILE="$INSTALL_DIR/docker-compose.gateway.yml"
ENV_FILE="$INSTALL_DIR/.env"
RUNTIME_DIR="$INSTALL_DIR/runtime"
ROLLBACK_STATUS_PATH="$RUNTIME_DIR/rollback-status.json"
STAGED_UPDATE_PATH="$RUNTIME_DIR/staged-update.json"
STACK_HEALTHCHECK_TIMEOUT_SECONDS="${STACK_HEALTHCHECK_TIMEOUT_SECONDS:-300}"

log() {
  printf '[gateway-launcher] %s\n' "$1"
}

compose_cmd() {
  if docker compose version >/dev/null 2>&1; then
    docker compose "$@"
    return
  fi

  if command -v docker-compose >/dev/null 2>&1; then
    docker-compose "$@"
    return
  fi

  echo "Docker Compose is required." >&2
  return 1
}

print_compose_diagnostics() {
  local compose_project_name
  compose_project_name="$(config_value composeProjectName 'truckwash-edge-gateway')"

  log "docker ps -a --format '{{.Names}} {{.Status}} {{.Ports}}'"
  docker ps -a --format '{{.Names}} {{.Status}} {{.Ports}}' || true

  log "compose ps -a --no-trunc"
  COMPOSE_PROJECT_NAME="$compose_project_name" compose_cmd -f "$COMPOSE_FILE" ps -a --no-trunc || true

  log "compose logs --no-color --no-log-prefix --tail=120"
  COMPOSE_PROJECT_NAME="$compose_project_name" compose_cmd -f "$COMPOSE_FILE" logs --no-color --no-log-prefix --tail=120 || true
}

config_value() {
  local key="$1"
  local fallback="${2:-}"
  php -r '
    $path = $argv[1];
    $key = $argv[2];
    $fallback = $argv[3];
    if (!is_file($path)) {
        echo $fallback;
        exit(0);
    }
    $decoded = json_decode((string)file_get_contents($path), true);
    if (!is_array($decoded) || !array_key_exists($key, $decoded) || $decoded[$key] === null || $decoded[$key] === "") {
        echo $fallback;
        exit(0);
    }
    echo is_scalar($decoded[$key]) ? (string)$decoded[$key] : json_encode($decoded[$key], JSON_UNESCAPED_SLASHES);
  ' "$CONFIG_PATH" "$key" "$fallback"
}

staged_update_value() {
  local key="$1"
  local fallback="${2:-}"
  php -r '
    $path = $argv[1];
    $key = $argv[2];
    $fallback = $argv[3];
    if (!is_file($path)) {
        echo $fallback;
        exit(0);
    }
    $decoded = json_decode((string)file_get_contents($path), true);
    if (!is_array($decoded) || !array_key_exists($key, $decoded) || $decoded[$key] === null || $decoded[$key] === "") {
        echo $fallback;
        exit(0);
    }
    echo is_scalar($decoded[$key]) ? (string)$decoded[$key] : json_encode($decoded[$key], JSON_UNESCAPED_SLASHES);
  ' "$STAGED_UPDATE_PATH" "$key" "$fallback"
}

staged_update_is_applied() {
  local status
  status="$(staged_update_value status '')"
  [ "$status" = "APPLIED" ]
}

ensure_dirs() {
  mkdir -p "$RUNTIME_DIR" "$RUNTIME_DIR/backups"
}

ensure_stack_env() {
  php -r '
    $path = $argv[1];
    $content = is_file($path) ? (string)file_get_contents($path) : "";
    $hasValue = static function (string $name) use ($content): bool {
        if (!preg_match("/^" . preg_quote($name, "/") . "=(.*)$/m", $content, $matches)) {
            return false;
        }
        return trim((string)$matches[1], " \t\"") !== "";
    };
    $secret = static function (int $bytes): string {
        return rtrim(strtr(base64_encode(random_bytes($bytes)), "+/", "-_"), "=");
    };
    $generated = [];
    $required = [
        "REDIS_PASSWORD" => static fn(): string => $secret(32),
        "MARIADB_PASSWORD" => static fn(): string => $secret(32),
        "MARIADB_ROOT_PASSWORD" => static fn(): string => $secret(32),
        "MINIO_ROOT_USER" => static fn(): string => "twminio" . bin2hex(random_bytes(12)),
        "MINIO_ROOT_PASSWORD" => static fn(): string => $secret(32),
    ];
    foreach ($required as $name => $factory) {
        if (!$hasValue($name)) {
            $generated[] = $name . "=" . $factory();
        }
    }
    if ($generated === []) {
        exit(0);
    }
    $prefix = ($content !== "" && !str_ends_with($content, "\n")) ? "\n" : "";
    file_put_contents($path, $prefix . implode("\n", $generated) . "\n", FILE_APPEND | LOCK_EX);
  ' "$ENV_FILE"
  chmod 0600 "$ENV_FILE"
}


within_update_window() {
  local window
  window="$(staged_update_value update_window '')"
  if [ -z "$window" ]; then
    window="$(config_value updateWindow '02:00-04:00')"
  fi
  php -r '
    $window = $argv[1];
    [$start, $end] = array_pad(explode("-", $window, 2), 2, "");
    $parse = static function (string $value): ?int {
        if (!preg_match("/^(\d{2}):(\d{2})$/", trim($value), $matches)) {
            return null;
        }
        return ((int)$matches[1] * 60) + (int)$matches[2];
    };
    $startMinutes = $parse($start);
    $endMinutes = $parse($end);
    if ($startMinutes === null || $endMinutes === null) {
        exit(1);
    }
    $nowMinutes = ((int)date("G") * 60) + (int)date("i");
    if ($startMinutes <= $endMinutes) {
        exit(($nowMinutes >= $startMinutes && $nowMinutes <= $endMinutes) ? 0 : 1);
    }
    exit(($nowMinutes >= $startMinutes || $nowMinutes <= $endMinutes) ? 0 : 1);
  ' "$window"
}

write_rollback_status() {
  local state="$1"
  local reason="${2:-}"
  local rolled_back_to="${3:-}"
  cat > "$ROLLBACK_STATUS_PATH" <<EOF_JSON
{
  "state": "$state",
  "reason": "$reason",
  "rolled_back_to": "$rolled_back_to",
  "at": "$(date --iso-8601=seconds)"
}
EOF_JSON
}

apply_stack() {
  local edge_base_image
  local worker_base_image
  local auto_updater_base_image
  local redis_base_image
  local mariadb_base_image
  local minio_base_image
  local compose_project_name
  edge_base_image="$(config_value edgeAgentBaseImage 'php:8.2-cli-bookworm')"
  worker_base_image="$(config_value lanWorkerBaseImage 'php:8.2-cli-bookworm')"
  auto_updater_base_image="$(config_value autoUpdaterBaseImage 'php:8.2-cli-bookworm')"
  redis_base_image="$(config_value redisBaseImage 'redis:7-alpine')"
  mariadb_base_image="$(config_value mariadbBaseImage 'mariadb:11')"
  minio_base_image="$(config_value minioBaseImage 'minio/minio:latest')"
  compose_project_name="$(config_value composeProjectName 'truckwash-edge-gateway')"
  ensure_stack_env
  cd "$INSTALL_DIR"
  COMPOSE_PROJECT_NAME="$compose_project_name" \
  EDGE_AGENT_BASE_IMAGE="$edge_base_image" \
  LAN_WORKER_BASE_IMAGE="$worker_base_image" \
  AUTO_UPDATER_BASE_IMAGE="$auto_updater_base_image" \
  REDIS_BASE_IMAGE="$redis_base_image" \
  MARIADB_BASE_IMAGE="$mariadb_base_image" \
  MINIO_BASE_IMAGE="$minio_base_image" \
    compose_cmd -f "$COMPOSE_FILE" up -d --build
}

rollback_stack() {
  local installed_version
  installed_version="$(config_value installedVersion '')"
  for file in \
    agent.php \
    lan-worker.php \
    auto-updater.php \
    docker-compose.gateway.yml \
    Dockerfile.edge-agent \
    Dockerfile.lan-worker \
    Dockerfile.auto-updater \
    gateway-launcher.sh \
    truckwash-edge-gateway-stack.service \
    truckwash-edge-agent.service; do
    if [ -f "$INSTALL_DIR/$file.bak" ]; then
      mv -f "$INSTALL_DIR/$file.bak" "$INSTALL_DIR/$file"
    fi
  done
  if ! apply_stack; then
    write_rollback_status "FAILED" "rollback_apply_failed" "$installed_version"
    return 1
  fi
  write_rollback_status "ROLLED_BACK" "healthcheck_failed" "$installed_version"
  if [ -f "$STAGED_UPDATE_PATH" ]; then
    php -r '
      $path = $argv[1];
      $decoded = is_file($path) ? json_decode((string)file_get_contents($path), true) : [];
      if (!is_array($decoded)) {
          $decoded = [];
      }
      $decoded["status"] = "ROLLED_BACK";
      $decoded["rolled_back_at"] = date(DATE_ATOM);
      file_put_contents($path, json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
    ' "$STAGED_UPDATE_PATH"
  fi
}

container_is_healthy() {
  local container_name="$1"
  local health
  health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_name" 2>/dev/null || echo missing)"
  [ "$health" = "healthy" ] || [ "$health" = "running" ]
}

healthcheck_stack() {
  container_is_healthy truckwash-redis &&
    container_is_healthy truckwash-mariadb &&
    container_is_healthy truckwash-minio &&
    container_is_healthy truckwash-lan-worker &&
    container_is_healthy truckwash-edge-agent &&
    container_is_healthy truckwash-auto-updater
}

wait_for_stack_health() {
  local timeout_seconds="${1:-$STACK_HEALTHCHECK_TIMEOUT_SECONDS}"
  local elapsed=0

  while [ "$elapsed" -lt "$timeout_seconds" ]; do
    if healthcheck_stack; then
      return 0
    fi

    sleep 2
    elapsed=$((elapsed + 2))
  done

  return 1
}

reconcile_stack() {
  local installed_version
  installed_version="$(config_value installedVersion '')"
  ensure_dirs
  log "Reconciling compose stack"
  if ! apply_stack; then
    log "Compose rollout failed during build/startup"
    print_compose_diagnostics
    write_rollback_status "FAILED" "compose_up_failed" "$installed_version"
    return 1
  fi

  if ! wait_for_stack_health "$STACK_HEALTHCHECK_TIMEOUT_SECONDS"; then
    log "Healthcheck failed after compose rollout; reverting to previous artifacts"
    print_compose_diagnostics
    rollback_stack
    return 1
  fi

  write_rollback_status "IDLE" "" ""
  if [ -f "$STAGED_UPDATE_PATH" ] && within_update_window; then
    php -r '
      $configPath = $argv[1];
      $stagedPath = $argv[2];
      $config = is_file($configPath) ? json_decode((string)file_get_contents($configPath), true) : [];
      $staged = is_file($stagedPath) ? json_decode((string)file_get_contents($stagedPath), true) : [];
      if (!is_array($config) || !is_array($staged)) {
          exit(0);
      }
      $targetVersion = trim((string)($staged["target_version"] ?? ""));
      if ($targetVersion !== "") {
          $config["installedVersion"] = $targetVersion;
          $config["targetVersion"] = $targetVersion;
          file_put_contents($configPath, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
          $staged["status"] = "APPLIED";
          $staged["applied_at"] = date(DATE_ATOM);
          file_put_contents($stagedPath, json_encode($staged, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
      }
    ' "$CONFIG_PATH" "$STAGED_UPDATE_PATH"
  fi
}

case "$ACTION" in
  up)
    reconcile_stack
    ;;
  reconcile)
    if [ -f "$STAGED_UPDATE_PATH" ] && staged_update_is_applied; then
      log "Staged update already applied; nothing to reconcile"
      exit 0
    fi
    if [ -f "$STAGED_UPDATE_PATH" ] && ! within_update_window; then
      log "Update is staged but outside the maintenance window; keeping current stack running"
      exit 0
    fi
    reconcile_stack
    ;;
  down)
    cd "$INSTALL_DIR"
    compose_cmd -f "$COMPOSE_FILE" down
    ;;
  *)
    echo "Usage: gateway-launcher.sh [up|reconcile|down]" >&2
    exit 1
    ;;
esac
