#!/usr/bin/env bash
# catalog swap (catalog swap) for the Premium/Essential-V2 and Dedicated-classic lanes.
#
# Upserts a component_images row in the cp9 metadb so the NEXT cluster created for that tenant
# uses the given kernel image tag. Config Center reads component_images with a direct query and no
# cache, and a tenant_id miss falls back to tenant 0, so the change is effective immediately for
# the chosen tenant and leaves other clusters on the global defaults.
#
# See skills/get-started/references/change-kernel-images.md for the field semantics,
# the basename-collapse constraint, and the Starter lane (which this script does NOT cover).
set -Eeuo pipefail

usage() {
  cat >&2 <<'USAGE'
usage: set-kernel-image.sh --lane <premium|dedicated> --component <name> --version <ver> \
                           --tag <image-tag> [options]

required:
  --lane premium|dedicated   premium    -> db config_center_regional, cluster_type SHARED_POOL
                             dedicated  -> db config_center_staging,  cluster_type DEDICATED_TIDB
  --component <name>         component_name, e.g. tidb tikv pd tiflash tiproxy
  --version <ver>            component version row to retag, e.g. v8.5.4
  --tag <image-tag>          image tag to publish into the catalog

options:
  --tenant <id>              tenant_id to scope the change to (default 0 = global default).
                             Prefer a real tenant id; 0 affects every cluster in the environment.
  --cluster-type <type>      override the lane default (SHARED_POOL|LOGICAL_CLUSTER|DEDICATED_TIDB)
  --base-image <repo>        base_image value (default: keep existing row's value, else derive
                             gcr.io/pingcap-public/dbaas/<basename>). Only its last path segment
                             is honoured by cluster-service-ng.
  --set-default-version      also upsert default_versions so this version is selected. Required
                             when --version introduces a NEW version string.
  --mysql-host <host>        default 127.0.0.1   (port-forward svc/premium-metadb in cp9-system)
  --mysql-port <port>        default 3306
  --mysql-user <user>        default root
  --skip-registry-check      skip verifying the image exists (not recommended)
  --ecr-repository <repo>    ECR repository to check (default tidbcloud/<basename of base_image>)
  --ecr-region <region>      default us-west-2
  --aws-profile <profile>    profile for the ECR check
  --apply                    perform the write. Without this the script only previews.

environment:
  CP9_METADB_PASSWORD        MySQL password. Required. Never pass it on the command line.
USAGE
  exit "${1:-64}"
}

lane="" component="" version="" tag="" tenant="0" cluster_type="" base_image=""
set_default_version=false
mysql_host="127.0.0.1" mysql_port="3306" mysql_user="root"
skip_registry_check=false ecr_repository="" ecr_region="us-west-2" aws_profile=""
apply=false

need_value() {
  [[ $# -ge 2 && -n "${2:-}" ]] || { echo "option $1 requires a value" >&2; exit 64; }
}

while (($#)); do
  case "$1" in
    --lane) need_value "$@"; lane="${2:-}"; shift 2 ;;
    --component) need_value "$@"; component="${2:-}"; shift 2 ;;
    --version) need_value "$@"; version="${2:-}"; shift 2 ;;
    --tag) need_value "$@"; tag="${2:-}"; shift 2 ;;
    --tenant) need_value "$@"; tenant="${2:-}"; shift 2 ;;
    --cluster-type) need_value "$@"; cluster_type="${2:-}"; shift 2 ;;
    --base-image) need_value "$@"; base_image="${2:-}"; shift 2 ;;
    --set-default-version) set_default_version=true; shift ;;
    --mysql-host) need_value "$@"; mysql_host="${2:-}"; shift 2 ;;
    --mysql-port) need_value "$@"; mysql_port="${2:-}"; shift 2 ;;
    --mysql-user) need_value "$@"; mysql_user="${2:-}"; shift 2 ;;
    --skip-registry-check) skip_registry_check=true; shift ;;
    --ecr-repository) need_value "$@"; ecr_repository="${2:-}"; shift 2 ;;
    --ecr-region) need_value "$@"; ecr_region="${2:-}"; shift 2 ;;
    --aws-profile) need_value "$@"; aws_profile="${2:-}"; shift 2 ;;
    --apply) apply=true; shift ;;
    -h|--help) usage 0 ;;
    *) echo "unknown argument: $1" >&2; usage ;;
  esac
done

[[ -n "$lane" && -n "$component" && -n "$version" && -n "$tag" ]] || usage

# Every value below reaches MySQL through string interpolation, so validate strictly and
# reject anything outside the shapes these columns legitimately hold. This is the safety
# boundary: do not relax it to "escape harder".
require_shape() {
  local name=$1 value=$2 pattern=$3
  [[ "$value" =~ $pattern ]] || {
    echo "invalid $name: '$value' (allowed: $pattern)" >&2
    exit 64
  }
}
require_shape --component "$component" '^[A-Za-z0-9_]+$'
require_shape --version "$version" '^[A-Za-z0-9._+-]+$'
require_shape --tag "$tag" '^[A-Za-z0-9._-]+$'
require_shape --tenant "$tenant" '^[0-9]+$'
[[ -z "$cluster_type" ]] || require_shape --cluster-type "$cluster_type" '^[A-Z_]+$'
[[ -z "$base_image" ]] || require_shape --base-image "$base_image" '^[A-Za-z0-9._/-]+$'
[[ -z "$ecr_repository" ]] || require_shape --ecr-repository "$ecr_repository" '^[A-Za-z0-9._/-]+$'
require_shape --ecr-region "$ecr_region" '^[a-z0-9-]+$'
require_shape --mysql-port "$mysql_port" '^[0-9]+$' 

case "$lane" in
  premium)
    database="config_center_regional"
    : "${cluster_type:=SHARED_POOL}"
    ;;
  dedicated)
    database="config_center_staging"
    : "${cluster_type:=DEDICATED_TIDB}"
    ;;
  *)
    echo "--lane must be premium or dedicated (Starter has no catalog database; see the reference)" >&2
    exit 64
    ;;
esac

[[ -n "${CP9_METADB_PASSWORD:-}" ]] || {
  echo "CP9_METADB_PASSWORD is not set" >&2
  exit 78
}

command -v mysql >/dev/null || { echo "mysql client is required" >&2; exit 78; }

mysql_run() {
  MYSQL_PWD="$CP9_METADB_PASSWORD" mysql \
    --host "$mysql_host" --port "$mysql_port" --user "$mysql_user" \
    --batch --raw --skip-column-names --database "$database" -e "$1"
}

# Inputs are validated above; this stays as defence in depth. Backslash must be
# escaped before the quote, otherwise the escape itself is corrupted.
sql_quote() {
  local v=$1
  v=${v//\\/\\\\}
  v=${v//"'"/"''"}
  printf "%s" "$v"
}


# ---- read current state -------------------------------------------------------------------------
if ! current_row=$(mysql_run "SELECT CONCAT_WS('\t', base_image, tag) FROM component_images
   WHERE cluster_type='$(sql_quote "$cluster_type")'
     AND component_name='$(sql_quote "$component")'
     AND version='$(sql_quote "$version")'
     AND tenant_id=$tenant;"); then
  echo "could not read the current component_images row; check the port-forward and password" >&2
  exit 1
fi

current_base_image="" current_tag=""
if [[ -n "$current_row" ]]; then
  current_base_image=${current_row%%$'\t'*}
  current_tag=${current_row#*$'\t'}
fi

if [[ -z "$base_image" ]]; then
  if [[ -n "$current_base_image" ]]; then
    base_image="$current_base_image"
  else
    cat >&2 <<'NOBASE'
no existing component_images row for this (cluster_type, component, version, tenant), so
base_image cannot be inherited. Pass --base-image explicitly.

Do not guess: components collapse onto shared repositories by basename, so for example
coprocessor_worker and tikv_worker both belong to the tikv repository. See
skills/get-started/references/change-kernel-images.md.
NOBASE
    exit 64
  fi
fi

basename_component=${base_image##*/}
: "${ecr_repository:=tidbcloud/$basename_component}"
effective_image="385595570414.dkr.ecr.${ecr_region}.amazonaws.com/${ecr_repository}:${tag}"

# ---- preflight: does the image exist? -----------------------------------------------------------
if [[ "$skip_registry_check" == true ]]; then
  echo "warning: skipping registry check; a bad tag will only surface as ImagePullBackOff" >&2
else
  command -v aws >/dev/null || { echo "aws CLI is required (or pass --skip-registry-check)" >&2; exit 78; }
  aws_args=(ecr describe-images --region "$ecr_region"
            --repository-name "$ecr_repository" --image-ids "imageTag=$tag")
  [[ -n "$aws_profile" ]] && aws_args+=(--profile "$aws_profile")
  if ! aws "${aws_args[@]}" >/dev/null 2>&1; then
    echo "image not found in registry: $ecr_repository:$tag (region $ecr_region)" >&2
    echo "push it first, or pass --skip-registry-check if you are pointing at another registry" >&2
    exit 1
  fi
  echo "registry check ok: $ecr_repository:$tag exists"
fi

# ---- preview ------------------------------------------------------------------------------------
cat <<PREVIEW
lane                 : $lane
database             : $database
cluster_type         : $cluster_type
component_name       : $component
version              : $version
tenant_id            : $tenant$([[ "$tenant" == "0" ]] && echo "   <-- global default, affects every cluster" || true)
base_image           : $base_image
tag                  : ${current_tag:-<none>} -> $tag
resolved pod image   : $effective_image
set default_versions : $set_default_version
PREVIEW

image_row_current=false
if [[ -n "$current_tag" && "$current_tag" == "$tag" && "$current_base_image" == "$base_image" ]]; then
  image_row_current=true
  echo "component_images row is already in the desired state"
  if [[ "$set_default_version" != true ]]; then
    echo "nothing to do"
    exit 0
  fi
fi

statements=""
if [[ "$image_row_current" != true ]]; then
  statements="INSERT INTO component_images
  (cluster_type, component_name, version, base_image, tag, GA, tenant_id)
VALUES ('$(sql_quote "$cluster_type")', '$(sql_quote "$component")', '$(sql_quote "$version")',
        '$(sql_quote "$base_image")', '$(sql_quote "$tag")', 0, $tenant)
ON DUPLICATE KEY UPDATE base_image=VALUES(base_image), tag=VALUES(tag);"
fi

# default_versions has no unique key on (cluster_type, component_name, tenant_id) — only
# PRIMARY KEY(id) and a non-unique KEY(cluster_type, component_name). ON DUPLICATE KEY
# UPDATE therefore never fires there and would silently insert a duplicate selector row,
# leaving which row wins undefined. Update in place when a row exists, insert only when it
# does not, and refuse to act if the table already holds duplicates.
default_version_sql=""
if [[ "$set_default_version" == true ]]; then
  dv_where="cluster_type='$(sql_quote "$cluster_type")'
       AND component_name='$(sql_quote "$component")'
       AND tenant_id=$tenant"
  if ! dv_count=$(mysql_run "SELECT COUNT(*) FROM default_versions WHERE $dv_where;"); then
    echo "could not read default_versions" >&2
    exit 1
  fi
  if ((dv_count > 1)); then
    echo "default_versions already has $dv_count rows for this selector; resolve that by hand first" >&2
    echo "  SELECT id, version FROM default_versions WHERE $dv_where;" >&2
    exit 1
  fi
  if ((dv_count == 1)); then
    default_version_sql="UPDATE default_versions SET version='$(sql_quote "$version")'
 WHERE $dv_where;"
  else
    default_version_sql="INSERT INTO default_versions
  (cluster_type, component_name, cloud_provider, region, tenant_id, version, cluster_version)
VALUES ('$(sql_quote "$cluster_type")', '$(sql_quote "$component")', '', '', $tenant,
        '$(sql_quote "$version")', '');"
  fi
  statements+="
$default_version_sql"
fi

echo
echo "--- statements ---"
echo "$statements"

if [[ "$apply" != true ]]; then
  echo
  echo "dry run: nothing was written. Re-run with --apply to perform the upsert."
  exit 0
fi

# ---- apply and read back ------------------------------------------------------------------------
mysql_run "$statements"

read_back=$(mysql_run "SELECT CONCAT_WS('\t', base_image, tag) FROM component_images
   WHERE cluster_type='$(sql_quote "$cluster_type")'
     AND component_name='$(sql_quote "$component")'
     AND version='$(sql_quote "$version")'
     AND tenant_id=$tenant;")

[[ "$read_back" == "$base_image"$'\t'"$tag" ]] || {
  echo "read-back mismatch: expected '$base_image<TAB>$tag', got '$read_back'" >&2
  exit 1
}
echo "read-back ok: $read_back"

if [[ "$set_default_version" == true ]]; then
  dv=$(mysql_run "SELECT GROUP_CONCAT(version) FROM default_versions
     WHERE cluster_type='$(sql_quote "$cluster_type")'
       AND component_name='$(sql_quote "$component")'
       AND tenant_id=$tenant;")
  [[ "$dv" == "$version" ]] || {
    echo "default_versions read-back mismatch: expected exactly '$version', got '$dv'" >&2
    exit 1
  }
  echo "default_versions read-back ok: $dv"
fi

cat <<'DONE'

applied. This affects clusters created from now on; existing clusters keep their current image.
Create a cluster, then assert the running image:

  kubectl --kubeconfig <dataplane-kubeconfig> -n <cluster-ns> get pods \
    -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\n"}{end}'

Record the statements and the observed pod image in the PR or run notes.
DONE
