#!/usr/bin/env bash

set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PROD_COMPOSE_FILE="${ROOT_DIR}/docker-compose.production.yml"
PORTAINER_COMPOSE_FILE="${ROOT_DIR}/docker-compose.portainer.production.yml"
REGISTRY_COMPOSE_FILE="${ROOT_DIR}/docker-compose.registry.production.yml"
TRAEFIK_COMPOSE_FILE="${ROOT_DIR}/traefik.yaml"
ENV_FILE="${ROOT_DIR}/.env.docker.production"
DYNAMIC_DIR="${ROOT_DIR}/dynamic"
REGISTRY_DYNAMIC_FILE="${DYNAMIC_DIR}/registry-upstream.yml"
PROJECT_NAME="${DOCKER_PROD_PROJECT_NAME:-rentaldrivego-prod}"
PORTAINER_PROJECT_NAME="${DOCKER_PORTAINER_PROJECT_NAME:-rentaldrivego-portainer-prod}"
REGISTRY_PROJECT_NAME="${DOCKER_REGISTRY_PROJECT_NAME:-rentaldrivego-registry-prod}"
TRAEFIK_NETWORK="${DOCKER_PROD_TRAEFIK_NETWORK:-traefik-proxy}"
VOLUME_BACKUP_IMAGE="${DOCKER_VOLUME_BACKUP_IMAGE:-postgres:16-alpine}"
API_UPLOADS_VOLUME="${DOCKER_PROD_API_UPLOADS_VOLUME:-${PROJECT_NAME}_api_uploads}"
REGISTRY_AUTH_DIR="${ROOT_DIR}/docker/registry/auth"
REGISTRY_AUTH_FILE="${REGISTRY_AUTH_DIR}/htpasswd"

ensure_env_file() {
  if [[ ! -f "${ENV_FILE}" ]]; then
    echo "Missing ${ENV_FILE}" >&2
    echo "Create it from .env.docker.production.example before starting production containers." >&2
    exit 1
  fi

  validate_prod_env_file
}

read_env_value() {
  local key="$1"
  local value

  value="$(
    awk -F= -v key="$key" '
      $1 == key {
        value = substr($0, index($0, "=") + 1)
      }
      END {
        if (value != "") {
          print value
        }
      }
    ' "${ENV_FILE}"
  )"

  value="${value%$'\r'}"
  value="${value#\"}"
  value="${value%\"}"
  printf '%s' "${value}"
}

export_env_value_if_unset() {
  local key="$1"
  local value

  if [[ -n "${!key:-}" ]]; then
    return 0
  fi

  value="$(read_env_value "${key}")"
  if [[ -n "${value}" ]]; then
    export "${key}=${value}"
  fi
}

export_compose_env_from_file() {
  local line key value

  while IFS= read -r line || [[ -n "${line}" ]]; do
    line="${line%$'\r'}"

    [[ -z "${line//[[:space:]]/}" || "${line}" =~ ^[[:space:]]*# ]] && continue

    line="${line#"${line%%[![:space:]]*}"}"
    line="${line#export }"

    [[ "${line}" == *"="* ]] || continue

    key="${line%%=*}"
    value="${line#*=}"
    key="${key//[[:space:]]/}"
    value="${value#"${value%%[![:space:]]*}"}"
    value="${value%"${value##*[![:space:]]}"}"

    if [[ "${value}" == \"*\" && "${value}" == *\" ]]; then
      value="${value#\"}"
      value="${value%\"}"
    elif [[ "${value}" == \'*\' && "${value}" == *\' ]]; then
      value="${value#\'}"
      value="${value%\'}"
    fi

    if [[ "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
      export "${key}=${value}"
    fi
  done < "${ENV_FILE}"

  if [[ -z "${NEXT_PUBLIC_CARPLACE_URL:-}" ]]; then
    if [[ -n "${NEXT_PUBLIC_STOREFRONT_URL:-}" ]]; then
      NEXT_PUBLIC_CARPLACE_URL="${NEXT_PUBLIC_STOREFRONT_URL%/}/carplace"
    elif [[ -n "${SITE_ORIGIN:-}" ]]; then
      NEXT_PUBLIC_CARPLACE_URL="${SITE_ORIGIN%/}/carplace"
    fi
    export NEXT_PUBLIC_CARPLACE_URL
  fi
}

load_prod_env_exports() {
  local key

  for key in \
    ACME_EMAIL \
    APP_IMAGE \
    APP_VERSION \
    IMAGE_TAG \
    IMAGE_ARCHIVE_DIR \
    IMAGE_ARCHIVE_REQUIRED \
    REGISTRY_HOST \
    REGISTRY_USER \
    REGISTRY_PASSWORD \
    REGISTRY_TOKEN \
    REGISTRY_INSECURE
  do
    export_env_value_if_unset "${key}"
  done

  if [[ -z "${REGISTRY_PASSWORD:-}" && -n "${REGISTRY_TOKEN:-}" ]]; then
    REGISTRY_PASSWORD="${REGISTRY_TOKEN}"
    export REGISTRY_PASSWORD
  fi
}

registry_host_name() {
  local host="${REGISTRY_HOST:-}"

  host="${host#http://}"
  host="${host#https://}"
  host="${host%%/*}"
  printf '%s' "${host}"
}

ensure_registry_transport_configured() {
  local insecure="${REGISTRY_INSECURE:-false}"
  local host

  if [[ "${insecure}" != "true" && "${insecure}" != "1" ]]; then
    return 0
  fi

  host="$(registry_host_name)"
  if [[ -z "${host}" ]]; then
    return 0
  fi

  REGISTRY_HOST="${host}"
  export REGISTRY_HOST

  if docker info 2>/dev/null | awk '/Insecure Registries:/,/Live Restore Enabled:/' | grep -Fqx "  ${host}"; then
    return 0
  fi

  cat >&2 <<EOF
Docker is not configured to pull ${host} over HTTP.

Docker image names cannot include http://. Configure the VPS Docker daemon once by adding
this entry to /etc/docker/daemon.json, merging it with any existing settings:

{
  "insecure-registries": ["${host}"]
}

Then restart Docker:

sudo systemctl restart docker

Then rerun ./deploy-production.sh.
EOF
  exit 1
}

validate_prod_env_file() {
  local acme_email api_domain public_site_domain cors_origins portainer_domain postgres_password redis_password jwt_secret

  acme_email="$(read_env_value ACME_EMAIL)"
  api_domain="$(read_env_value API_DOMAIN)"
  public_site_domain="$(read_env_value PUBLIC_SITE_DOMAIN)"
  cors_origins="$(read_env_value CORS_ORIGINS)"
  portainer_domain="$(read_env_value PORTAINER_DOMAIN)"
  postgres_password="$(read_env_value POSTGRES_PASSWORD)"
  redis_password="$(read_env_value REDIS_PASSWORD)"
  jwt_secret="$(read_env_value JWT_SECRET)"

  if [[ -z "${acme_email}" || "${acme_email}" != *@* || "${acme_email}" == *example.com* ]]; then
    echo "Invalid ACME_EMAIL in ${ENV_FILE}. Set it to a real contact email for Let's Encrypt, not example.com." >&2
    exit 1
  fi

  if [[ -z "${api_domain}" || "${api_domain}" == *example.com* ]]; then
    echo "Invalid API_DOMAIN in ${ENV_FILE}. Set it to the real production API hostname, not example.com." >&2
    exit 1
  fi

  if [[ -z "${public_site_domain}" || "${public_site_domain}" == *example.com* ]]; then
    echo "Invalid PUBLIC_SITE_DOMAIN in ${ENV_FILE}. Set it to the real production site hostname, not example.com." >&2
    exit 1
  fi

  if [[ -z "${cors_origins}" || "${cors_origins}" == *example.com* ]]; then
    echo "Invalid CORS_ORIGINS in ${ENV_FILE}. Replace example.com origins with the real production domains." >&2
    exit 1
  fi

  for pair in "POSTGRES_PASSWORD:${postgres_password}" "REDIS_PASSWORD:${redis_password}" "JWT_SECRET:${jwt_secret}"; do
    key="${pair%%:*}"
    value="${pair#*:}"
    if [[ -z "${value}" || "${value}" == replace-with-* || "${value}" == *changeme* || "${value}" == *change-me* ]]; then
      echo "Invalid ${key} in ${ENV_FILE}. Set a real production secret." >&2
      exit 1
    fi
  done


  if [[ -n "${portainer_domain}" && "${portainer_domain}" == *example.com* ]]; then
    echo "Invalid PORTAINER_DOMAIN in ${ENV_FILE}. Set a real hostname or disable the portainer router." >&2
    exit 1
  fi
}

ensure_traefik_network() {
  if ! docker network inspect "${TRAEFIK_NETWORK}" >/dev/null 2>&1; then
    echo "Creating Docker network: ${TRAEFIK_NETWORK}"
    docker network create "${TRAEFIK_NETWORK}" >/dev/null
  fi
}

ensure_api_uploads_volume() {
  if ! docker volume inspect "${API_UPLOADS_VOLUME}" >/dev/null 2>&1; then
    echo "Creating Docker volume: ${API_UPLOADS_VOLUME}"
    docker volume create "${API_UPLOADS_VOLUME}" >/dev/null
  fi
}

ensure_dynamic_dir() {
  mkdir -p "${DYNAMIC_DIR}"
}

prod_compose() {
  export_compose_env_from_file
  APP_ENV_FILE="${ENV_FILE}" DOCKER_PROD_API_UPLOADS_VOLUME="${API_UPLOADS_VOLUME}" docker compose -p "${PROJECT_NAME}" --env-file "${ENV_FILE}" -f "${PROD_COMPOSE_FILE}" "$@"
}

portainer_compose() {
  export_compose_env_from_file
  docker compose -p "${PORTAINER_PROJECT_NAME}" --env-file "${ENV_FILE}" -f "${PORTAINER_COMPOSE_FILE}" "$@"
}

registry_compose() {
  export_compose_env_from_file
  docker compose -p "${REGISTRY_PROJECT_NAME}" --env-file "${ENV_FILE}" -f "${REGISTRY_COMPOSE_FILE}" "$@"
}

traefik_compose() {
  export_compose_env_from_file
  docker compose --env-file "${ENV_FILE}" -f "${TRAEFIK_COMPOSE_FILE}" "$@"
}

start_prod_services() {
  ensure_env_file
  ensure_api_uploads_volume

  local services=("$@")
  local needs_migrations=0

  for service in "${services[@]}"; do
    case "${service}" in
      api|carplace|dashboard|admin)
        needs_migrations=1
        break
        ;;
    esac
  done

  if [[ ${needs_migrations} -eq 1 ]]; then
    prod_compose up --build -d postgres redis
    wait_for_healthy postgres 120
    prod_compose run --rm --build migrate
  fi

  prod_compose up --build -d "$@"
}

require_release_image() {
  IMAGE_TAG="${IMAGE_TAG:-${APP_VERSION:-}}"
  export IMAGE_TAG

  if [[ -z "${IMAGE_TAG:-}" ]]; then
    echo "IMAGE_TAG must be set for production deploys." >&2
    exit 1
  fi
}

login_registry_if_configured() {
  ensure_registry_transport_configured

  if [[ -z "${REGISTRY_PASSWORD:-}" && -n "${REGISTRY_TOKEN:-}" ]]; then
    REGISTRY_PASSWORD="${REGISTRY_TOKEN}"
    export REGISTRY_PASSWORD
  fi

  if [[ "${IMAGE_ARCHIVE_REQUIRED:-false}" == "true" || "${IMAGE_ARCHIVE_REQUIRED:-0}" == "1" ]]; then
    echo "IMAGE_ARCHIVE_REQUIRED is enabled; skipping docker login."
    return 0
  fi

  if [[ -n "${IMAGE_ARCHIVE_DIR:-}" && -d "${IMAGE_ARCHIVE_DIR}" ]]; then
    shopt -s nullglob
    local archives=(
      "${IMAGE_ARCHIVE_DIR}"/*"${IMAGE_TAG:-latest}"*.tar
      "${IMAGE_ARCHIVE_DIR}"/*"${IMAGE_TAG:-latest}"*.tar.gz
      "${IMAGE_ARCHIVE_DIR}"/*"${IMAGE_TAG:-latest}"*.tgz
    )
    shopt -u nullglob

    if [[ ${#archives[@]} -gt 0 && -z "${REGISTRY_USER:-}" && -z "${REGISTRY_PASSWORD:-}" ]]; then
      echo "Found image archive in IMAGE_ARCHIVE_DIR; skipping registry login."
      return 0
    fi
  fi

  if [[ -z "${REGISTRY_USER:-}" && -z "${REGISTRY_PASSWORD:-}" ]]; then
    if [[ -n "${REGISTRY_HOST:-}" ]]; then
      echo "REGISTRY_HOST is set but no registry credentials are configured; skipping docker login."
    fi
    return 0
  fi

  if [[ -z "${REGISTRY_HOST:-}" || -z "${REGISTRY_USER:-}" || -z "${REGISTRY_PASSWORD:-}" ]]; then
    echo "REGISTRY_HOST, REGISTRY_USER, and REGISTRY_PASSWORD or REGISTRY_TOKEN are required when registry credentials are configured." >&2
    exit 1
  fi

  echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY_HOST}" -u "${REGISTRY_USER}" --password-stdin
}

pull_prod_release_images() {
  require_release_image
  ensure_registry_transport_configured

  if load_release_image_archives; then
    return 0
  fi

  if [[ "${IMAGE_ARCHIVE_REQUIRED:-false}" == "true" || "${IMAGE_ARCHIVE_REQUIRED:-0}" == "1" ]]; then
    echo "IMAGE_ARCHIVE_REQUIRED is enabled, but no archive for IMAGE_TAG=${IMAGE_TAG} was found in ${IMAGE_ARCHIVE_DIR:-<unset>}." >&2
    echo "Direct docker pull is disabled in this mode. Create a shared archive first, for example:" >&2
    echo "  docker save ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG} -o ${IMAGE_ARCHIVE_DIR:-./image-archives}/carmanagement-${IMAGE_TAG}.tar" >&2
    exit 1
  fi

  prod_compose pull migrate api carplace dashboard admin
}

load_release_image_archives() {
  local archive_dir="${IMAGE_ARCHIVE_DIR:-}"
  local archive
  local loaded=0

  if [[ -z "${archive_dir}" ]]; then
    return 1
  fi

  if [[ ! -d "${archive_dir}" ]]; then
    echo "IMAGE_ARCHIVE_DIR is set but does not exist: ${archive_dir}" >&2
    exit 1
  fi

  shopt -s nullglob
  for archive in \
    "${archive_dir}"/*"${IMAGE_TAG}"*.tar \
    "${archive_dir}"/*"${IMAGE_TAG}"*.tar.gz \
    "${archive_dir}"/*"${IMAGE_TAG}"*.tgz
  do
    echo "Loading Docker image archive: ${archive}"
    docker load -i "${archive}"
    loaded=1
  done
  shopt -u nullglob

  if [[ ${loaded} -eq 0 ]]; then
    echo "No Docker image archive found for IMAGE_TAG=${IMAGE_TAG} in ${archive_dir}."
    return 1
  fi

  echo "Loaded image archive(s) for ${APP_IMAGE:-rentaldrivego/carmanagement}:${IMAGE_TAG}; skipping docker pull."
  return 0
}

run_prod_migrations() {
  prod_compose run --rm migrate
}

wait_for_healthy() {
  local service="$1"
  local timeout="${2:-120}"
  local container="${PROJECT_NAME}-${service}-1"
  local elapsed=0

  echo "Waiting for ${service} to be healthy (timeout: ${timeout}s)..."
  while [[ $elapsed -lt $timeout ]]; do
    local status
    status=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "missing")
    case "$status" in
      healthy)
        echo "${service} is healthy."
        return 0
        ;;
      unhealthy)
        echo "ERROR: ${service} is unhealthy. Check logs with: docker logs ${container}" >&2
        return 1
        ;;
    esac
    sleep 5
    elapsed=$((elapsed + 5))
  done

  echo "ERROR: Timed out waiting for ${service} to become healthy." >&2
  return 1
}

start_portainer() {
  ensure_env_file
  ensure_traefik_network
  portainer_compose up -d
}

render_registry_proxy_config() {
  ensure_dynamic_dir

  if [[ -z "${REGISTRY_DOMAIN:-}" && -z "${REGISTRY_UPSTREAM_URL:-}" ]]; then
    rm -f "${REGISTRY_DYNAMIC_FILE}"
    return 0
  fi

  if [[ -z "${REGISTRY_DOMAIN:-}" || -z "${REGISTRY_UPSTREAM_URL:-}" ]]; then
    echo "REGISTRY_DOMAIN and REGISTRY_UPSTREAM_URL must either both be set or both be unset." >&2
    exit 1
  fi

  cat > "${REGISTRY_DYNAMIC_FILE}" <<EOF
http:
  routers:
    registry:
      rule: Host(\`${REGISTRY_DOMAIN}\`)
      entryPoints:
        - websecure
      tls:
        certResolver: letsencrypt
      service: registry-upstream

  services:
    registry-upstream:
      loadBalancer:
        servers:
          - url: "${REGISTRY_UPSTREAM_URL}"
EOF
}

ensure_registry_auth_file() {
  mkdir -p "${REGISTRY_AUTH_DIR}"

  if [[ -n "${REGISTRY_USER:-}" && -n "${REGISTRY_PASSWORD:-}" ]]; then
    docker run --rm --entrypoint htpasswd httpd:2-alpine -Bbn "${REGISTRY_USER}" "${REGISTRY_PASSWORD}" > "${REGISTRY_AUTH_FILE}"
    chmod 600 "${REGISTRY_AUTH_FILE}"
    return 0
  fi

  if [[ -f "${REGISTRY_AUTH_FILE}" ]]; then
    return 0
  fi

  echo "Set REGISTRY_USER and REGISTRY_PASSWORD in .env.docker.production or create ${REGISTRY_AUTH_FILE} manually." >&2
  exit 1
}

start_registry() {
  ensure_env_file
  ensure_traefik_network
  ensure_registry_auth_file
  registry_compose up -d
}

start_traefik() {
  ensure_env_file
  ensure_traefik_network
  render_registry_proxy_config
  traefik_compose up -d
}

stop_traefik() {
  ensure_env_file
  traefik_compose down >/dev/null 2>&1 || true
}

docker_volume_exists() {
  docker volume inspect "$1" >/dev/null 2>&1
}

backup_named_volume() {
  local volume_name="$1"
  local archive_path="$2"

  docker run --rm \
    -v "${volume_name}:/volume:ro" \
    -v "$(dirname "${archive_path}"):/backup" \
    "${VOLUME_BACKUP_IMAGE}" \
    sh -lc "tar -czf /backup/$(basename "${archive_path}") -C /volume ."
}

volume_running_containers() {
  docker ps --filter "volume=$1" --format '{{.Names}}'
}

restore_named_volume() {
  local volume_name="$1"
  local archive_path="$2"
  local running

  running="$(volume_running_containers "${volume_name}")"
  if [[ -n "${running}" ]]; then
    echo "Volume ${volume_name} is currently used by running containers:" >&2
    echo "${running}" >&2
    echo "Stop those containers before restoring this volume." >&2
    exit 1
  fi

  docker run --rm \
    -v "${volume_name}:/volume" \
    -v "$(dirname "${archive_path}"):/backup" \
    "${VOLUME_BACKUP_IMAGE}" \
    sh -lc "find /volume -mindepth 1 -delete && tar -xzf /backup/$(basename "${archive_path}") -C /volume"
}
