#!/usr/bin/env bash

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
TRANSPORT_LIB="${SCRIPT_DIR}/lib/transport.sh"
DEFAULT_ENV_FILE="${SCRIPT_DIR}/enterprise.env"
DEFAULT_REMOTE_DIR="/opt/security-center/enterprise"

# shellcheck disable=SC1090
source "${TRANSPORT_LIB}"

usage() {
  cat <<'EOF'
Remote MySQL backup helper for scripts/enterprise.

This helper:
1. resolves the backup S3 bucket/prefix/region
2. creates or reuses a dedicated backup bucket
3. grants the EC2 instance role scoped write access to that bucket/prefix when possible
4. runs mysqldump against the enterprise MySQL container on the EC2 host
5. uploads the compressed dump to S3 and prints the final object path

Usage:
  ./scripts/enterprise/backup-enterprise-db.sh \
    --env-file ./scripts/enterprise/enterprise.env \
    --instance-id i-0123456789abcdef0

Options:
  --env-file <path>          Enterprise env file. Default: scripts/enterprise/enterprise.env
  --transport <mode>         Optional. ssm (default) or ssh.
  --instance-id <id>         Required for ssm unless provided by the caller.
  --host <user@host>         Required for ssh unless provided by the caller.
  --identity-file <path>     Optional. SSH private key.
  --remote-dir <path>        Remote deployment root. Default: /opt/security-center/enterprise
  --region <name>            AWS region used for local bucket/IAM operations.
  --profile <name>           AWS CLI profile override.
  --backup-s3-bucket <name>  Dedicated S3 bucket used for DB backups.
  --backup-s3-prefix <path>  S3 prefix root used for DB backups.
  --backup-s3-region <name>  S3 region for the DB backup bucket.
  --non-interactive          Fail on missing required values instead of prompting.
  --dry-run                  Print actions without executing them.
  --help                     Show this help.
EOF
}

info() { echo "INFO: $*"; }
warn() { echo "WARN: $*" >&2; }
die() { echo "ERROR: $*" >&2; exit 1; }

trim() {
  local value="${1:-}"
  value="${value#"${value%%[![:space:]]*}"}"
  value="${value%"${value##*[![:space:]]}"}"
  printf '%s' "${value}"
}

require_bin() {
  command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}

resolve_path() {
  local path="$1"
  if [[ -z "${path}" ]]; then
    printf '%s' ""
    return 0
  fi
  if [[ "${path}" = /* ]]; then
    printf '%s' "${path}"
    return 0
  fi
  if [[ -e "${path}" ]]; then
    printf '%s' "${path}"
    return 0
  fi
  if [[ -e "${ROOT_DIR}/${path}" ]]; then
    printf '%s' "${ROOT_DIR}/${path}"
    return 0
  fi
  printf '%s' "${path}"
}

get_env_value() {
  local env_file="$1"
  local key="$2"
  [[ -f "${env_file}" ]] || return 0
  python3 - "${env_file}" "${key}" <<'PY'
from pathlib import Path
import sys

path = Path(sys.argv[1])
key = sys.argv[2]
for raw in path.read_text().splitlines():
    line = raw.strip()
    if not line or line.startswith("#") or "=" not in line:
        continue
    lhs, rhs = line.split("=", 1)
    if lhs.strip() == key:
        print(rhs.strip())
        break
PY
}

set_env_value() {
  local env_file="$1"
  local key="$2"
  local value="$3"
  python3 - "${env_file}" "${key}" "${value}" <<'PY'
from pathlib import Path
import sys

path = Path(sys.argv[1])
key = sys.argv[2]
value = sys.argv[3]
lines = []
found = False
if path.exists():
    lines = path.read_text().splitlines()

new_lines = []
for raw in lines:
    if raw.strip().startswith(f"{key}="):
        new_lines.append(f"{key}={value}")
        found = True
    else:
        new_lines.append(raw)
if not found:
    if new_lines and new_lines[-1].strip():
        new_lines.append("")
    new_lines.append(f"{key}={value}")
path.write_text("\n".join(new_lines) + "\n")
PY
}

prompt_value() {
  local __var="$1"
  local message="$2"
  local default="${3:-}"
  local value=""

  if [[ "${NON_INTERACTIVE}" == "true" ]]; then
    if [[ -z "${default}" ]]; then
      die "missing required value in non-interactive mode: ${message}"
    fi
    printf -v "${__var}" '%s' "${default}"
    return 0
  fi

  if [[ -n "${default}" ]]; then
    printf "%s [%s]: " "${message}" "${default}"
  else
    printf "%s: " "${message}"
  fi
  IFS= read -r value || true
  value="$(trim "${value}")"
  if [[ -z "${value}" ]]; then
    value="${default}"
  fi
  printf -v "${__var}" '%s' "${value}"
}

normalize_s3_bucket_name() {
  local raw="${1:-}"
  local normalized

  normalized="$(printf '%s' "${raw}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9.-]+/-/g; s/^[.-]+//; s/[.-]+$//; s/-+/-/g; s/\.+/./g')"
  normalized="${normalized:0:63}"
  normalized="$(printf '%s' "${normalized}" | sed -E 's/^[.-]+//; s/[.-]+$//')"
  if [[ -z "${normalized}" ]]; then
    normalized="security-center-db-backups"
  fi
  printf '%s' "${normalized}"
}

apply_bucket_tags() {
  local bucket_name="$1"
  local bucket_purpose="$2"
  local tag_file=""
  tag_file="$(mktemp /tmp/security-center-db-backup-bucket-tags.XXXXXX.json)"
  python3 - "${bucket_name}" "${bucket_purpose}" "${STACK_ID}" "${PUBLIC_HOSTNAME}" "${SSM_PATH_PREFIX}" > "${tag_file}" <<'PY'
import json
import sys

bucket_name, purpose, stack_id, hostname, ssm_prefix = sys.argv[1:6]
tags = [
    {"Key": "Name", "Value": bucket_name},
    {"Key": "Project", "Value": "security-center"},
    {"Key": "ManagedBy", "Value": "scripts/enterprise"},
    {"Key": "StackId", "Value": stack_id},
    {"Key": "PublicHostname", "Value": hostname},
    {"Key": "SSMPathPrefix", "Value": ssm_prefix},
    {"Key": "Purpose", "Value": purpose},
]
print(json.dumps({"TagSet": tags}))
PY
  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would apply enterprise tags to S3 bucket ${bucket_name}"
    rm -f "${tag_file}"
    return 0
  fi
  aws s3api put-bucket-tagging \
    --bucket "${bucket_name}" \
    --tagging "file://${tag_file}" >/dev/null
  rm -f "${tag_file}"
}

ensure_backup_bucket() {
  if [[ -z "${BACKUP_S3_BUCKET}" ]]; then
    if [[ "${NON_INTERACTIVE}" == "true" ]]; then
      die "db-backup requires SC_DB_BACKUP_S3_BUCKET or --backup-s3-bucket in non-interactive mode"
    fi
    local default_bucket
    default_bucket="$(normalize_s3_bucket_name "${STACK_ID}-db-backups-${ACCOUNT_ID}-${BACKUP_S3_REGION}")"
    prompt_value BACKUP_S3_BUCKET "Dedicated S3 database backup bucket" "${default_bucket}"
  fi

  if [[ "${BACKUP_S3_BUCKET}" == "${ARTIFACT_BUCKET}" && -n "${ARTIFACT_BUCKET}" ]]; then
    die "SC_DB_BACKUP_S3_BUCKET must not reuse SC_S3_ARTIFACT_BUCKET"
  fi
  if [[ "${BACKUP_S3_BUCKET}" == "${UPLOADS_BUCKET}" && -n "${UPLOADS_BUCKET}" ]]; then
    die "SC_DB_BACKUP_S3_BUCKET must not reuse SC_UPLOADS_BUCKET"
  fi

  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would create or reuse S3 database backup bucket ${BACKUP_S3_BUCKET} in ${BACKUP_S3_REGION}"
    return 0
  fi

  if ! aws s3api head-bucket --bucket "${BACKUP_S3_BUCKET}" >/dev/null 2>&1; then
    if [[ "${BACKUP_S3_REGION}" == "us-east-1" ]]; then
      aws s3api create-bucket --bucket "${BACKUP_S3_BUCKET}" >/dev/null
    else
      aws s3api create-bucket \
        --bucket "${BACKUP_S3_BUCKET}" \
        --create-bucket-configuration "LocationConstraint=${BACKUP_S3_REGION}" >/dev/null
    fi
  fi
  apply_bucket_tags "${BACKUP_S3_BUCKET}" "db-backup"

  # 保留策略：备份对象只保留最近 BACKUP_RETENTION_DAYS 天，防止 bucket 无限增长。
  # 幂等——每次执行把同一条规则覆盖写回；对象 key 固定为 <prefix>/db-backups/... 结构。
  local lifecycle_json
  lifecycle_json="{\"Rules\":[{\"ID\":\"expire-db-backups\",\"Status\":\"Enabled\",\"Filter\":{\"Prefix\":\"${BACKUP_S3_PREFIX%/}/db-backups/\"},\"Expiration\":{\"Days\":${BACKUP_RETENTION_DAYS}}}]}"
  aws s3api put-bucket-lifecycle-configuration \
    --bucket "${BACKUP_S3_BUCKET}" \
    --lifecycle-configuration "${lifecycle_json}" >/dev/null
}

ensure_role_policy_for_db_backups() {
  if [[ -z "${ROLE_NAME}" ]]; then
    warn "SC_EC2_IAM_ROLE_NAME is not set; skipping remote S3 write policy bootstrap. Remote upload will rely on existing host AWS credentials."
    return 0
  fi

  local prefix_match="${BACKUP_S3_PREFIX}"
  prefix_match="${prefix_match%/}"
  if [[ -n "${prefix_match}" ]]; then
    prefix_match="${prefix_match}/"
  fi

  local policy_file=""
  policy_file="$(mktemp /tmp/security-center-db-backup-policy.XXXXXX.json)"
  cat > "${policy_file}" <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": ["arn:aws:s3:::${BACKUP_S3_BUCKET}"],
      "Condition": {
        "StringLike": {
          "s3:prefix": ["${prefix_match}*"]
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:GetObject"],
      "Resource": ["arn:aws:s3:::${BACKUP_S3_BUCKET}/${prefix_match}*"]
    }
  ]
}
EOF
  if [[ "${DRY_RUN}" == "true" ]]; then
    info "would attach inline S3 db-backup policy to role ${ROLE_NAME} scoped to ${BACKUP_S3_BUCKET}/${prefix_match}*"
    rm -f "${policy_file}"
    return 0
  fi
  aws iam put-role-policy \
    --role-name "${ROLE_NAME}" \
    --policy-name "${STACK_ID}-db-backup-access" \
    --policy-document "file://${policy_file}" >/dev/null
  rm -f "${policy_file}"
}

persist_backup_config() {
  if [[ "${DRY_RUN}" == "true" ]]; then
    return 0
  fi
  set_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_BUCKET" "${BACKUP_S3_BUCKET}"
  set_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_PREFIX" "${BACKUP_S3_PREFIX}"
  set_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_REGION" "${BACKUP_S3_REGION}"
}

build_remote_command() {
  python3 - "${REMOTE_RENDER_DIR}" "${BACKUP_S3_BUCKET}" "${S3_OBJECT_KEY}" "${BACKUP_S3_REGION}" "${TIMESTAMP_UTC}" "${HOST_SLUG}" <<'PY'
import json
import shlex
import sys

remote_render_dir, bucket, object_key, region, timestamp_utc, host_slug = sys.argv[1:7]

def q(value: str) -> str:
    return shlex.quote(value)

script = f"""
set -euo pipefail
cd {q(remote_render_dir)}
test -f .env.runtime
test -f docker-compose.enterprise.yml
docker compose --env-file .env.runtime -f docker-compose.enterprise.yml ps mysql >/dev/null
tmp_dir="$(mktemp -d /tmp/security-center-db-backup.XXXXXX)"
cleanup() {{
  rm -rf "$tmp_dir"
}}
trap cleanup EXIT
archive_path="$tmp_dir/{timestamp_utc}-{host_slug}-mysql.sql.gz"
summary_path="$tmp_dir/summary.txt"
docker compose --env-file .env.runtime -f docker-compose.enterprise.yml exec -T mysql sh -lc '
  set -eu
  test -n "${{MYSQL_ROOT_PASSWORD:-}}"
  test -n "${{MYSQL_DATABASE:-}}"
  MYSQL_PWD="${{MYSQL_ROOT_PASSWORD}}" mysqldump \
    --single-transaction \
    --quick \
    --routines \
    --triggers \
    --events \
    -uroot \
    "${{MYSQL_DATABASE}}"
' | gzip -c > "$archive_path"
sha256sum "$archive_path" > "$summary_path"
aws s3 cp "$archive_path" {q(f"s3://{bucket}/{object_key}")} --region {q(region)} >/dev/null
size_bytes="$(wc -c < "$archive_path" | tr -d "[:space:]")"
checksum="$(awk '{{print $1}}' "$summary_path")"
printf "S3_URI=%s\\n" {q(f"s3://{bucket}/{object_key}")}
printf "SIZE_BYTES=%s\\n" "$size_bytes"
printf "SHA256=%s\\n" "$checksum"
"""
print(script.strip())
PY
}

ENV_FILE="${DEFAULT_ENV_FILE}"
TRANSPORT="ssm"
INSTANCE_ID=""
REMOTE_HOST=""
REMOTE_DIR="${DEFAULT_REMOTE_DIR}"
IDENTITY_FILE=""
AWS_REGION_NAME="${AWS_REGION:-${AWS_DEFAULT_REGION:-}}"
AWS_PROFILE_NAME="${AWS_PROFILE:-}"
BACKUP_S3_BUCKET=""
BACKUP_S3_PREFIX=""
BACKUP_S3_REGION=""
NON_INTERACTIVE="false"
DRY_RUN="false"

while (($# > 0)); do
  case "$1" in
    --env-file)
      ENV_FILE="$2"
      shift 2
      ;;
    --transport)
      TRANSPORT="$2"
      shift 2
      ;;
    --instance-id)
      INSTANCE_ID="$2"
      shift 2
      ;;
    --host)
      REMOTE_HOST="$2"
      shift 2
      ;;
    --identity-file)
      IDENTITY_FILE="$(resolve_path "$2")"
      shift 2
      ;;
    --remote-dir)
      REMOTE_DIR="$2"
      shift 2
      ;;
    --region)
      AWS_REGION_NAME="$2"
      shift 2
      ;;
    --profile)
      AWS_PROFILE_NAME="$2"
      shift 2
      ;;
    --backup-s3-bucket)
      BACKUP_S3_BUCKET="$2"
      shift 2
      ;;
    --backup-s3-prefix)
      BACKUP_S3_PREFIX="$2"
      shift 2
      ;;
    --backup-s3-region)
      BACKUP_S3_REGION="$2"
      shift 2
      ;;
    --non-interactive)
      NON_INTERACTIVE="true"
      shift
      ;;
    --dry-run)
      DRY_RUN="true"
      shift
      ;;
    --help|-h)
      usage
      exit 0
      ;;
    *)
      die "unknown argument: $1"
      ;;
  esac
done

ENV_FILE="$(resolve_path "${ENV_FILE}")"
[[ -f "${ENV_FILE}" ]] || die "env file not found: ${ENV_FILE}"
[[ -n "${AWS_REGION_NAME}" ]] || AWS_REGION_NAME="$(trim "$(get_env_value "${ENV_FILE}" "SC_AWS_REGION")")"
[[ -n "${AWS_REGION_NAME}" ]] || die "AWS region is required"

case "${TRANSPORT}" in
  ssm)
    [[ -n "${INSTANCE_ID}" ]] || die "--instance-id is required for --transport ssm"
    ;;
  ssh)
    [[ -n "${REMOTE_HOST}" ]] || die "--host is required for --transport ssh"
    ;;
  *)
    die "unsupported transport: ${TRANSPORT}"
    ;;
esac

require_bin aws
require_bin python3

if [[ -n "${AWS_PROFILE_NAME}" ]]; then
  export AWS_PROFILE="${AWS_PROFILE_NAME}"
  export AWS_SDK_LOAD_CONFIG=1
fi
export AWS_REGION="${AWS_REGION_NAME}"
export AWS_DEFAULT_REGION="${AWS_REGION_NAME}"

if [[ "${DRY_RUN}" == "true" ]]; then
  ACCOUNT_ID="$(aws sts get-caller-identity --query 'Account' --output text 2>/dev/null || true)"
  ACCOUNT_ID="${ACCOUNT_ID:-unknown-account}"
else
  ACCOUNT_ID="$(aws sts get-caller-identity --query 'Account' --output text)"
fi
PUBLIC_HOSTNAME="$(trim "$(get_env_value "${ENV_FILE}" "SC_PUBLIC_HOSTNAME")")"
SSM_PATH_PREFIX="$(trim "$(get_env_value "${ENV_FILE}" "SC_SSM_PATH_PREFIX")")"
STACK_ID="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_NAME_PREFIX")")"
STACK_ID="${STACK_ID:-security-center-enterprise}"
ARTIFACT_BUCKET="$(trim "$(get_env_value "${ENV_FILE}" "SC_S3_ARTIFACT_BUCKET")")"
UPLOADS_BUCKET="$(trim "$(get_env_value "${ENV_FILE}" "SC_UPLOADS_BUCKET")")"
ROLE_NAME="$(trim "$(get_env_value "${ENV_FILE}" "SC_EC2_IAM_ROLE_NAME")")"
BACKUP_S3_BUCKET="${BACKUP_S3_BUCKET:-$(trim "$(get_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_BUCKET")")}"
BACKUP_S3_PREFIX="${BACKUP_S3_PREFIX:-$(trim "$(get_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_PREFIX")")}"
BACKUP_S3_REGION="${BACKUP_S3_REGION:-$(trim "$(get_env_value "${ENV_FILE}" "SC_DB_BACKUP_S3_REGION")")}"
BACKUP_S3_PREFIX="${BACKUP_S3_PREFIX:-security-center-enterprise}"
BACKUP_S3_REGION="${BACKUP_S3_REGION:-${AWS_REGION_NAME}}"
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-$(trim "$(get_env_value "${ENV_FILE}" "SC_DB_BACKUP_RETENTION_DAYS")")}"
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-30}"
REMOTE_RENDER_DIR="${REMOTE_DIR%/}/rendered"
HOST_SLUG="$(printf '%s' "${PUBLIC_HOSTNAME:-${INSTANCE_ID:-${REMOTE_HOST}}}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')"
HOST_SLUG="${HOST_SLUG:-enterprise}"
TIMESTAMP_UTC="$(date -u '+%Y%m%dT%H%M%SZ')"

ensure_backup_bucket
persist_backup_config
ensure_role_policy_for_db_backups

S3_OBJECT_KEY="${BACKUP_S3_PREFIX%/}/db-backups/${HOST_SLUG}/${TIMESTAMP_UTC}-mysql.sql.gz"
S3_OBJECT_KEY="${S3_OBJECT_KEY#/}"

SC_TRANSPORT_MODE="${TRANSPORT}"
SC_TRANSPORT_DRY_RUN="${DRY_RUN}"
SC_TRANSPORT_REMOTE_HOST="${REMOTE_HOST}"
SC_TRANSPORT_INSTANCE_ID="${INSTANCE_ID}"
SC_TRANSPORT_IDENTITY_FILE="${IDENTITY_FILE}"
SC_TRANSPORT_AWS_REGION="${AWS_REGION_NAME}"
SC_TRANSPORT_AWS_PROFILE="${AWS_PROFILE_NAME}"
sc_transport_init

remote_command="$(build_remote_command)"

info "database backup target summary:"
if [[ "${TRANSPORT}" == "ssm" ]]; then
  info "  - target instance: ${INSTANCE_ID}"
else
  info "  - target host: ${REMOTE_HOST}"
fi
info "  - remote compose dir: ${REMOTE_RENDER_DIR}"
info "  - backup bucket: ${BACKUP_S3_BUCKET}"
info "  - backup prefix: ${BACKUP_S3_PREFIX}"
info "  - backup region: ${BACKUP_S3_REGION}"

output="$(sc_transport_run "${remote_command}")"
printf '%s\n' "${output}"

info "enterprise database backup completed"
