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

SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=/dev/null
source "$SCRIPT_DIR/libenv.sh"

runtime_bin_dir() {
  local base_dir=${LIMRISTEM_MAIL_BASE_DIR:-/opt/limristem-mail}
  if [[ -d "$base_dir/bin" ]]; then
    printf '%s\n' "$base_dir/bin"
  else
    printf '%s\n' "$base_dir/scripts"
  fi
}

ENV_FILE=$(limristem_mail_resolve_main_env_file)
BACKUP_ENV_FILE=$(limristem_mail_resolve_backup_env_file)
RCLONE_CONFIG_FILE=$(limristem_mail_resolve_rclone_config_file)
STATE_DIR=$(limristem_mail_resolve_managed_config_dir)
SCHEDULES_FILE=$STATE_DIR/backup-schedules.json
STORAGES_FILE=$STATE_DIR/backup-storages.json
KEYS_DIR=$STATE_DIR/backup-storage-keys
PENDING_KEYS_DIR=$STATE_DIR/backup-storage-private
PENDING_PRIVATE_KEY_TTL_SECONDS=$((30 * 60))

declare -A BACKUP_ENV_MAP=(
  [backup-enabled]=LIMRISTEM_MAIL_ENABLE_BACKUP_TIMER
  [backup-type]=LIMRISTEM_MAIL_BACKUP_TYPE
  [backup-full-weekday]=LIMRISTEM_MAIL_BACKUP_FULL_WEEKDAY
  [backup-db-mode]=LIMRISTEM_MAIL_BACKUP_DB_MODE
  [backup-db-root-password]=LIMRISTEM_MAIL_BACKUP_DB_ROOT_PASSWORD
  [backup-local-dir]=LIMRISTEM_MAIL_BACKUP_LOCAL_DIR
  [backup-retention-days]=LIMRISTEM_MAIL_BACKUP_RETENTION_DAYS
  [backup-remote-targets]=LIMRISTEM_MAIL_BACKUP_REMOTE_TARGETS
  [backup-oncalendar]=LIMRISTEM_MAIL_BACKUP_ONCALENDAR
  [backup-compression]=LIMRISTEM_MAIL_BACKUP_COMPRESSION
  [backup-include-redis]=LIMRISTEM_MAIL_BACKUP_INCLUDE_REDIS
  [backup-storage-type]=LIMRISTEM_MAIL_BACKUP_STORAGE_TYPE
  [backup-storage-remote-name]=LIMRISTEM_MAIL_BACKUP_STORAGE_REMOTE_NAME
  [backup-storage-path]=LIMRISTEM_MAIL_BACKUP_STORAGE_PATH
  [backup-storage-host]=LIMRISTEM_MAIL_BACKUP_STORAGE_HOST
  [backup-storage-port]=LIMRISTEM_MAIL_BACKUP_STORAGE_PORT
  [backup-storage-user]=LIMRISTEM_MAIL_BACKUP_STORAGE_USER
  [backup-storage-password]=LIMRISTEM_MAIL_BACKUP_STORAGE_PASSWORD
  [backup-storage-bucket]=LIMRISTEM_MAIL_BACKUP_STORAGE_BUCKET
  [backup-storage-region]=LIMRISTEM_MAIL_BACKUP_STORAGE_REGION
  [backup-storage-endpoint]=LIMRISTEM_MAIL_BACKUP_STORAGE_ENDPOINT
  [backup-storage-access-key-id]=LIMRISTEM_MAIL_BACKUP_STORAGE_ACCESS_KEY_ID
  [backup-storage-secret-access-key]=LIMRISTEM_MAIL_BACKUP_STORAGE_SECRET_ACCESS_KEY
)

declare -A BACKUP_DEFAULT_MAP=(
  [backup-enabled]=yes
  [backup-type]=auto
  [backup-full-weekday]=Sun
  [backup-db-mode]=logical
  [backup-db-root-password]=""
  [backup-local-dir]=/var/backups/limristem-mail
  [backup-retention-days]=14
  [backup-remote-targets]=""
  [backup-oncalendar]="*-*-* 03:15:00"
  [backup-compression]=gzip
  [backup-include-redis]=yes
  [backup-storage-type]=local
  [backup-storage-remote-name]=limristem-mail-backup
  [backup-storage-path]=""
  [backup-storage-host]=""
  [backup-storage-port]=""
  [backup-storage-user]=""
  [backup-storage-password]=""
  [backup-storage-bucket]=""
  [backup-storage-region]=""
  [backup-storage-endpoint]=""
  [backup-storage-access-key-id]=""
  [backup-storage-secret-access-key]=""
)

usage() {
  cat <<'USAGE'
Usage:
  manage-backups.sh show [--json]
  manage-backups.sh list [--json]
  manage-backups.sh list-schedules [--json]
  manage-backups.sh save-schedule <json> [--json]
  manage-backups.sh delete-schedule <id>
  manage-backups.sh list-storages [--json]
  manage-backups.sh save-storage <json> [--json]
  manage-backups.sh delete-storage <id>
  manage-backups.sh test-storage <id> [--json]
  manage-backups.sh consume-private-key <token>
  manage-backups.sh set <key> <value>
  manage-backups.sh set-many <key> <value> [<key> <value> ...]
  manage-backups.sh sync-schedules
  manage-backups.sh run [schedule-id]
USAGE
}

require_root() {
  if [[ $EUID -ne 0 ]]; then
    echo "Run as root." >&2
    exit 1
  fi
}

validate_resource_id() {
  [[ "$1" =~ ^[a-z0-9][a-z0-9_.-]{0,63}$ ]] || {
    echo "Invalid resource ID" >&2
    exit 1
  }
}

load_env() {
  limristem_mail_load_env_file "$ENV_FILE"
  limristem_mail_load_env_file "$BACKUP_ENV_FILE"
}

env_file_for_key() {
  local key=$1
  case "$key" in
    backup-enabled) printf '%s\n' "$ENV_FILE" ;;
    *) printf '%s\n' "$BACKUP_ENV_FILE" ;;
  esac
}

set_env_value() {
  local file=$1
  local key=$2
  local value=$3
  limristem_mail_upsert_env_value "$file" "$key" "$value"
}

storage_value() {
  local key=$1
  local env_key=${BACKUP_ENV_MAP[$key]}
  printf '%s' "${!env_key:-${BACKUP_DEFAULT_MAP[$key]}}"
}

obscure_rclone_password() {
  local secret=${1-}
  if [[ -z "$secret" ]]; then
    printf '\n'
    return 0
  fi
  if command -v rclone >/dev/null 2>&1; then
    rclone obscure "$secret"
  else
    printf '%s\n' "$secret"
  fi
}

write_rclone_profile() {
  local config_file=$1
  local storage_json=$2
  python3 - "$config_file" "$storage_json" <<'PY'
import configparser
import io
import json
import pathlib
import sys

config_path = pathlib.Path(sys.argv[1])
storage = json.loads(sys.argv[2])
config_path.parent.mkdir(parents=True, exist_ok=True)
remote_name = storage.get("remote_name") or storage.get("name") or "limristem-mail-backup"
storage_type = storage.get("type", "local")

config = configparser.ConfigParser()
if storage_type == "s3":
    config[remote_name] = {
        "type": "s3",
        "provider": "AWS",
        "env_auth": "false",
        "access_key_id": storage.get("access_key_id", ""),
        "secret_access_key": storage.get("secret_access_key", ""),
    }
    if storage.get("region"):
        config[remote_name]["region"] = storage["region"]
    if storage.get("endpoint"):
        config[remote_name]["endpoint"] = storage["endpoint"]
elif storage_type in {"sftp", "ftp", "ftps"}:
    config[remote_name] = {
        "type": "ftp" if storage_type in {"ftp", "ftps"} else "sftp",
        "host": storage.get("host", ""),
        "user": storage.get("user", ""),
    }
    if storage.get("port"):
        config[remote_name]["port"] = storage["port"]
    if storage.get("password") or storage.get("password_obscured"):
        config[remote_name]["pass"] = storage.get("password_obscured", storage["password"])
    if storage_type == "ftps":
        config[remote_name]["tls"] = "true"
else:
    config_path.write_text("", encoding="utf-8")
    raise SystemExit(0)

buf = io.StringIO()
config.write(buf, space_around_delimiters=True)
config_path.write_text(buf.getvalue().rstrip("\n") + "\n", encoding="utf-8")
PY
}

storage_remote_target() {
  python3 - "$1" <<'PY'
import json
import sys

storage = json.loads(sys.argv[1])
storage_type = storage.get("type", "local")
remote_name = storage.get("remote_name") or storage.get("name") or "limristem-mail-backup"
path = (storage.get("path") or "").lstrip("/")
if storage_type == "s3":
    target = f"{remote_name}:{storage.get('bucket', '')}".rstrip(":")
    if path:
        target = f"{target}/{path}"
elif storage_type in {"sftp", "ftp", "ftps"}:
    remote_path = storage.get("path") or "/"
    target = f"{remote_name}:{remote_path}"
else:
    target = ""
print(target)
PY
}

ensure_state_files() {
  limristem_mail_prepare_managed_dir "$STATE_DIR"
  mkdir -p "$KEYS_DIR" "$PENDING_KEYS_DIR"
  chmod 700 "$KEYS_DIR" "$PENDING_KEYS_DIR" 2>/dev/null || true
  [[ -f "$STORAGES_FILE" ]] || printf '[]\n' > "$STORAGES_FILE"
  [[ -f "$SCHEDULES_FILE" ]] || printf '[]\n' > "$SCHEDULES_FILE"
  python3 - "$PENDING_KEYS_DIR" "$PENDING_PRIVATE_KEY_TTL_SECONDS" <<'PY'
import pathlib
import sys
import time

pending_dir = pathlib.Path(sys.argv[1])
ttl_seconds = int(sys.argv[2])
now = time.time()
for item in pending_dir.glob("*.pem"):
    try:
        if now - item.stat().st_mtime > ttl_seconds:
            item.unlink(missing_ok=True)
    except FileNotFoundError:
        continue
PY
  python3 - "$ENV_FILE" "$BACKUP_ENV_FILE" "$STORAGES_FILE" "$SCHEDULES_FILE" <<'PY'
import json
import pathlib
import re
import sys


def load_env(path: pathlib.Path) -> dict[str, str]:
    data: dict[str, str] = {}
    if not path.exists():
        return data
    for line in path.read_text(encoding="utf-8").splitlines():
        if not line or line.lstrip().startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        data[key] = value
    return data


def load_json(path: pathlib.Path):
    try:
        return json.loads(path.read_text(encoding="utf-8") or "[]")
    except Exception:
        return []


def slugify(value: str, default: str) -> str:
    slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
    return slug or default

main_env = load_env(pathlib.Path(sys.argv[1]))
backup_env = load_env(pathlib.Path(sys.argv[2]))
merged = {**main_env, **backup_env}
storages_path = pathlib.Path(sys.argv[3])
schedules_path = pathlib.Path(sys.argv[4])
storages = load_json(storages_path)
schedules = load_json(schedules_path)

if not storages:
    storage_type = merged.get("LIMRISTEM_MAIL_BACKUP_STORAGE_TYPE", "local")
    remote_name = merged.get("LIMRISTEM_MAIL_BACKUP_STORAGE_REMOTE_NAME", "limristem-mail-backup")
    legacy_storage = {
        "id": "legacy-default",
        "name": "Default storage",
        "type": storage_type,
        "remote_name": remote_name,
        "path": merged.get("LIMRISTEM_MAIL_BACKUP_STORAGE_PATH", ""),
        "host": merged.get("LIMRISTEM_MAIL_BACKUP_STORAGE_HOST", ""),
        "port": merged.get("LIMRISTEM_MAIL_BACKUP_STORAGE_PORT", ""),
        "user": merged.get("LIMRISTEM_MAIL_BACKUP_STORAGE_USER", ""),
        "password": merged.get("LIMRISTEM_MAIL_BACKUP_STORAGE_PASSWORD", ""),
        "bucket": merged.get("LIMRISTEM_MAIL_BACKUP_STORAGE_BUCKET", ""),
        "region": merged.get("LIMRISTEM_MAIL_BACKUP_STORAGE_REGION", ""),
        "endpoint": merged.get("LIMRISTEM_MAIL_BACKUP_STORAGE_ENDPOINT", ""),
        "access_key_id": merged.get("LIMRISTEM_MAIL_BACKUP_STORAGE_ACCESS_KEY_ID", ""),
        "secret_access_key": merged.get("LIMRISTEM_MAIL_BACKUP_STORAGE_SECRET_ACCESS_KEY", ""),
        "encrypt": "no",
        "public_key_path": "",
    }
    has_remote = storage_type != "local" or any(legacy_storage[key] for key in ("host", "bucket", "access_key_id", "user", "path"))
    storages = [legacy_storage] if has_remote else []
    storages_path.write_text(json.dumps(storages, indent=2) + "\n", encoding="utf-8")

if not schedules:
    schedules = [
        {
            "id": slugify("default-backup", "default-backup"),
            "name": "Default backup schedule",
            "enabled": "yes",
            "oncalendar": merged.get("LIMRISTEM_MAIL_BACKUP_ONCALENDAR", "*-*-* 03:15:00") or "*-*-* 03:15:00",
            "backup_type": merged.get("LIMRISTEM_MAIL_BACKUP_TYPE", "auto") or "auto",
            "full_weekday": merged.get("LIMRISTEM_MAIL_BACKUP_FULL_WEEKDAY", "Sun") or "Sun",
            "compression": merged.get("LIMRISTEM_MAIL_BACKUP_COMPRESSION", "gzip") or "gzip",
            "include_database": "no" if merged.get("LIMRISTEM_MAIL_BACKUP_DB_MODE", "logical") == "none" else "yes",
            "include_redis": merged.get("LIMRISTEM_MAIL_BACKUP_INCLUDE_REDIS", "yes") or "yes",
            "storage_id": storages[0]["id"] if storages else "",
        }
    ]
    schedules_path.write_text(json.dumps(schedules, indent=2) + "\n", encoding="utf-8")
PY
}

mask_storage_records() {
  python3 - "$1" <<'PY'
import json
import sys

records = json.loads(sys.argv[1])
for record in records:
    for field, flag in (("password", "has_password"), ("secret_access_key", "has_secret_access_key")):
        if record.get(field):
            record[flag] = True
            record[field] = ""
        else:
            record[flag] = False
print(json.dumps(records))
PY
}

list_storages_json() {
  ensure_state_files
  local json_output
  json_output=$(cat "$STORAGES_FILE")
  mask_storage_records "$json_output"
}

normalize_storage_json() {
  python3 - "$1" "$STORAGES_FILE" <<'PY'
import json
import pathlib
import re
import sys

payload = json.loads(sys.argv[1] or "{}")
storage_path = pathlib.Path(sys.argv[2])
try:
    existing = json.loads(storage_path.read_text(encoding="utf-8") or "[]")
except Exception:
    existing = []
existing_by_id = {item.get("id"): item for item in existing if item.get("id")}
source = existing_by_id.get((payload.get("id") or "").strip(), {})
name = str(payload.get("name", "")).strip() or str(source.get("name", "")).strip() or "Backup storage"
storage_type = str(payload.get("type", source.get("type", "local"))).strip().lower() or "local"
if storage_type not in {"local", "s3", "sftp", "ftp", "ftps"}:
    raise SystemExit("Invalid storage type")
storage_id = str(payload.get("id", "")).strip()
if not storage_id:
    storage_id = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") or "backup-storage"
if not re.fullmatch(r"[a-z0-9][a-z0-9_.-]{0,63}", storage_id):
    raise SystemExit("Invalid storage ID")
remote_name = str(payload.get("remote_name", "")).strip() or str(source.get("remote_name", "")).strip() or storage_id
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,63}", remote_name):
    raise SystemExit("Invalid storage remote name")
record = {
    "id": storage_id,
    "name": name,
    "type": storage_type,
    "remote_name": remote_name,
    "path": str(payload.get("path", source.get("path", ""))).strip(),
    "host": str(payload.get("host", source.get("host", ""))).strip(),
    "port": str(payload.get("port", source.get("port", ""))).strip(),
    "user": str(payload.get("user", source.get("user", ""))).strip(),
    "password": str(payload.get("password", source.get("password", ""))),
    "bucket": str(payload.get("bucket", source.get("bucket", ""))).strip(),
    "region": str(payload.get("region", source.get("region", ""))).strip(),
    "endpoint": str(payload.get("endpoint", source.get("endpoint", ""))).strip(),
    "access_key_id": str(payload.get("access_key_id", source.get("access_key_id", ""))).strip(),
    "secret_access_key": str(payload.get("secret_access_key", source.get("secret_access_key", ""))),
    "encrypt": "yes" if str(payload.get("encrypt", source.get("encrypt", "no"))).strip().lower() == "yes" else "no",
    "public_key_path": str(source.get("public_key_path", "")),
}
for field in (
    "name",
    "path",
    "host",
    "user",
    "password",
    "bucket",
    "region",
    "endpoint",
    "access_key_id",
    "secret_access_key",
):
    if any(character in record[field] for character in ("\x00", "\r", "\n")):
        raise SystemExit(f"Invalid storage {field}")
if record["port"]:
    port = int(record["port"])
    if port < 1 or port > 65535:
        raise SystemExit("Invalid storage port")
required = {
    "s3": ("bucket", "access_key_id", "secret_access_key"),
    "sftp": ("host", "user", "password"),
    "ftp": ("host", "user", "password"),
    "ftps": ("host", "user", "password"),
}.get(storage_type, ())
for field in required:
    if not str(record.get(field, "")).strip():
        raise SystemExit(f"Missing value for storage {field}")
print(json.dumps(record))
PY
}

write_storage_record() {
  python3 - "$STORAGES_FILE" "$1" <<'PY'
import json
import pathlib
import sys

path = pathlib.Path(sys.argv[1])
record = json.loads(sys.argv[2])
try:
    records = json.loads(path.read_text(encoding="utf-8") or "[]")
except Exception:
    records = []
updated = []
replaced = False
for item in records:
    if item.get("id") == record.get("id"):
        updated.append(record)
        replaced = True
    else:
        updated.append(item)
if not replaced:
    updated.append(record)
updated.sort(key=lambda item: (item.get("name") or item.get("id") or "").lower())
path.write_text(json.dumps(updated, indent=2) + "\n", encoding="utf-8")
PY
}

generate_storage_keypair() {
  local storage_id=$1
  local public_key=$KEYS_DIR/${storage_id}.pub.pem
  local random_token
  if command -v openssl >/dev/null 2>&1; then
    random_token=$(openssl rand -hex 12)
  else
    random_token=$(python3 - <<'PY'
import secrets
print(secrets.token_hex(12))
PY
)
  fi
  local token=${storage_id}-${random_token}.pem
  local private_key=$PENDING_KEYS_DIR/$token
  openssl genpkey -algorithm RSA -out "$private_key" -pkeyopt rsa_keygen_bits:2048 >/dev/null 2>&1
  openssl rsa -pubout -in "$private_key" -out "$public_key" >/dev/null 2>&1
  chmod 600 "$private_key" "$public_key" 2>/dev/null || true
  printf '%s\t%s\t%s\n' "$token" "$private_key" "$public_key"
}

save_storage() {
  ensure_state_files
  local raw_json=$1
  local as_json=${2:-no}
  local normalized storage_id encrypt public_key_path token private_key
  normalized=$(normalize_storage_json "$raw_json")
  storage_id=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["id"])' "$normalized")
  encrypt=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1]).get("encrypt", "no"))' "$normalized")
  public_key_path=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1]).get("public_key_path", ""))' "$normalized")
  if [[ -n "$public_key_path" && "$public_key_path" != "$KEYS_DIR/${storage_id}.pub.pem" ]]; then
    echo "Invalid storage public key path" >&2
    exit 1
  fi
  token=
  if [[ "$encrypt" == "yes" && -z "$public_key_path" ]]; then
    read -r token private_key public_key_path < <(generate_storage_keypair "$storage_id")
    normalized=$(python3 - "$normalized" "$public_key_path" <<'PY'
import json
import sys
record = json.loads(sys.argv[1])
record["public_key_path"] = sys.argv[2]
print(json.dumps(record))
PY
)
  elif [[ "$encrypt" != "yes" ]]; then
    rm -f "$KEYS_DIR/${storage_id}.pub.pem"
    normalized=$(python3 - "$normalized" <<'PY'
import json
import sys
record = json.loads(sys.argv[1])
record["public_key_path"] = ""
print(json.dumps(record))
PY
)
  fi
  write_storage_record "$normalized"
  sync_legacy_storage_state
  load_env
  if [[ "$as_json" == "yes" || "$as_json" == "--json" ]]; then
    python3 - "$normalized" "$token" <<'PY'
import json
import sys
record = json.loads(sys.argv[1])
for field in ("password", "secret_access_key"):
    record[field] = ""
    record[f"has_{field}"] = bool(json.loads(sys.argv[1]).get(field))
print(json.dumps({"storage": record, "private_key_token": sys.argv[2]}))
PY
  fi
}

delete_storage() {
  ensure_state_files
  local storage_id=$1
  validate_resource_id "$storage_id"
  python3 - "$STORAGES_FILE" "$SCHEDULES_FILE" "$storage_id" <<'PY'
import json
import pathlib
import sys

storage_path = pathlib.Path(sys.argv[1])
schedule_path = pathlib.Path(sys.argv[2])
storage_id = sys.argv[3]
storages = [item for item in json.loads(storage_path.read_text(encoding="utf-8") or "[]") if item.get("id") != storage_id]
schedules = json.loads(schedule_path.read_text(encoding="utf-8") or "[]")
for schedule in schedules:
    if schedule.get("storage_id") == storage_id:
        schedule["storage_id"] = ""
storage_path.write_text(json.dumps(storages, indent=2) + "\n", encoding="utf-8")
schedule_path.write_text(json.dumps(schedules, indent=2) + "\n", encoding="utf-8")
PY
  rm -f "$KEYS_DIR/${storage_id}.pub.pem"
  sync_legacy_storage_state
  load_env
}

consume_private_key() {
  ensure_state_files
  local token=$1
  if [[ ! "$token" =~ ^[a-z0-9][a-z0-9_.-]{0,63}-[a-f0-9]{24}\.pem$ ]]; then
    echo "Invalid private key token" >&2
    exit 1
  fi
  local key_file=$PENDING_KEYS_DIR/$token
  if [[ ! -f "$key_file" ]]; then
    echo "Private key not found" >&2
    exit 1
  fi
  # Atomic read-and-delete: move to a temp name first so no other process
  # can race us to read the same file, then read and delete.
  local tmp_key
  tmp_key=$(mktemp "$PENDING_KEYS_DIR/.consume-XXXXXX.pem")
  mv "$key_file" "$tmp_key"
  cat "$tmp_key"
  rm -f "$tmp_key"
}

test_storage_connection() {
  ensure_state_files
  local storage_id=$1
  local as_json=${2:-no}
  local storage_json
  storage_json=$(python3 -c 'import json, pathlib, sys; records = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8") or "[]"); matches = [item for item in records if item.get("id") == sys.argv[2]]; print(json.dumps(matches[0]) if matches else (_ for _ in ()).throw(SystemExit("Storage not found")))' "$STORAGES_FILE" "$storage_id")
  local storage_type
  storage_type=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1]).get("type", "local"))' "$storage_json")
  local ok=yes detail="Connection successful."
  if [[ "$storage_type" == "local" ]]; then
    local target_path
    target_path=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1]).get("path") or "/var/backups/limristem-mail")' "$storage_json")
    mkdir -p "$target_path"
    if ! limristem_mail_path_is_writable "$target_path/.limristem-mail-backup-test"; then
      ok=no
      detail="Local storage path is not writable."
    fi
  else
    if ! command -v rclone >/dev/null 2>&1; then
      ok=no
      detail="rclone is not installed."
    else
      local tmp_config obscured_password target
      tmp_config=$(mktemp)
      if [[ "$storage_type" =~ ^(sftp|ftp|ftps)$ ]]; then
        local raw_password
        raw_password=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1]).get("password", ""))' "$storage_json")
        if [[ -n "$raw_password" ]]; then
          obscured_password=$(obscure_rclone_password "$raw_password")
          storage_json=$(python3 -c 'import json,sys; record = json.loads(sys.argv[1]); record["password_obscured"] = sys.argv[2]; print(json.dumps(record))' "$storage_json" "$obscured_password")
        fi
      fi
      write_rclone_profile "$tmp_config" "$storage_json"
      chmod 600 "$tmp_config" 2>/dev/null || true
      target=$(storage_remote_target "$storage_json")
      if ! rclone lsf --config "$tmp_config" "$target" --max-depth 1 >/dev/null 2>&1; then
        ok=no
        detail="Remote storage test failed."
      fi
      rm -f "$tmp_config"
    fi
  fi
  if [[ "$as_json" == "yes" || "$as_json" == "--json" ]]; then
    python3 -c 'import json,sys; print(json.dumps({"storage_id": sys.argv[1], "ok": sys.argv[2] == "yes", "detail": sys.argv[3]}))' "$storage_id" "$ok" "$detail"
  elif [[ "$ok" != "yes" ]]; then
    echo "$detail" >&2
    exit 1
  else
    echo "$detail"
  fi
}

list_schedules_json() {
  ensure_state_files
  cat "$SCHEDULES_FILE"
}

normalize_schedule_json() {
  python3 - "$1" "$SCHEDULES_FILE" "$STORAGES_FILE" <<'PY'
import json
import pathlib
import re
import sys

payload = json.loads(sys.argv[1] or "{}")
schedule_path = pathlib.Path(sys.argv[2])
storage_path = pathlib.Path(sys.argv[3])
try:
    schedules = json.loads(schedule_path.read_text(encoding="utf-8") or "[]")
except Exception:
    schedules = []
try:
    storages = json.loads(storage_path.read_text(encoding="utf-8") or "[]")
except Exception:
    storages = []
storage_ids = {item.get("id") for item in storages if item.get("id")}
existing = {item.get("id"): item for item in schedules if item.get("id")}
source = existing.get(str(payload.get("id", "")).strip(), {})
name = str(payload.get("name", "")).strip() or str(source.get("name", "")).strip() or "Backup schedule"
schedule_id = str(payload.get("id", "")).strip()
if not schedule_id:
    schedule_id = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") or "backup-schedule"
if not re.fullmatch(r"[a-z0-9][a-z0-9_.-]{0,63}", schedule_id):
    raise SystemExit("Invalid schedule ID")
if any(character in name for character in ("\x00", "\r", "\n")):
    raise SystemExit("Invalid schedule name")
enabled = "yes" if str(payload.get("enabled", source.get("enabled", "yes"))).strip().lower() == "yes" else "no"
raw_oncalendar = str(payload.get("oncalendar", source.get("oncalendar", "*-*-* 03:15:00"))).replace("\r", "")
lines = [line.strip() for line in raw_oncalendar.splitlines() if line.strip()]
if not lines:
    raise SystemExit("Missing value for schedule")
backup_type = str(payload.get("backup_type", source.get("backup_type", "auto"))).strip().lower()
if backup_type not in {"auto", "full", "incremental"}:
    raise SystemExit("Invalid schedule backup type")
if backup_type == "auto":
    full_weekday = str(payload.get("full_weekday", source.get("full_weekday", "Sun"))).strip().title()
    if full_weekday not in {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}:
        raise SystemExit("Invalid schedule weekday")
else:
    full_weekday = "Sun"
compression = str(payload.get("compression", source.get("compression", "gzip"))).strip().lower()
if compression not in {"gzip", "none"}:
    raise SystemExit("Invalid schedule compression")
include_database = "yes" if str(payload.get("include_database", source.get("include_database", "yes"))).strip().lower() == "yes" else "no"
include_redis = "yes" if str(payload.get("include_redis", source.get("include_redis", "yes"))).strip().lower() == "yes" else "no"
storage_id = str(payload.get("storage_id", source.get("storage_id", ""))).strip()
if storage_id and storage_id not in storage_ids:
    raise SystemExit("Unknown storage selected")
record = {
    "id": schedule_id,
    "name": name,
    "enabled": enabled,
    "oncalendar": "\n".join(lines),
    "backup_type": backup_type,
    "full_weekday": full_weekday,
    "compression": compression,
    "include_database": include_database,
    "include_redis": include_redis,
    "storage_id": storage_id,
}
print(json.dumps(record))
PY
}

write_schedule_record() {
  python3 - "$SCHEDULES_FILE" "$1" <<'PY'
import json
import pathlib
import sys

path = pathlib.Path(sys.argv[1])
record = json.loads(sys.argv[2])
try:
    records = json.loads(path.read_text(encoding="utf-8") or "[]")
except Exception:
    records = []
updated = []
replaced = False
for item in records:
    if item.get("id") == record.get("id"):
        updated.append(record)
        replaced = True
    else:
        updated.append(item)
if not replaced:
    updated.append(record)
updated.sort(key=lambda item: (item.get("name") or item.get("id") or "").lower())
path.write_text(json.dumps(updated, indent=2) + "\n", encoding="utf-8")
PY
}

sync_legacy_storage_state() {
  ensure_state_files
  python3 - "$BACKUP_ENV_FILE" "$STORAGES_FILE" <<'PY'
import json
import pathlib
import sys

backup_env_path = pathlib.Path(sys.argv[1])
storages_path = pathlib.Path(sys.argv[2])
try:
    storages = json.loads(storages_path.read_text(encoding="utf-8") or "[]")
except Exception:
    storages = []
record = next((item for item in storages if item.get("id") == "legacy-default"), None)
if not record:
    raise SystemExit(0)
mapping = {
    "backup-storage-type": record.get("type", "local"),
    "backup-storage-remote-name": record.get("remote_name", ""),
    "backup-storage-path": record.get("path", ""),
    "backup-storage-host": record.get("host", ""),
    "backup-storage-port": record.get("port", ""),
    "backup-storage-user": record.get("user", ""),
    "backup-storage-password": record.get("password", ""),
    "backup-storage-bucket": record.get("bucket", ""),
    "backup-storage-region": record.get("region", ""),
    "backup-storage-endpoint": record.get("endpoint", ""),
    "backup-storage-access-key-id": record.get("access_key_id", ""),
    "backup-storage-secret-access-key": record.get("secret_access_key", ""),
}
existing = {}
if backup_env_path.exists():
    for line in backup_env_path.read_text(encoding="utf-8").splitlines():
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        existing[key] = value
for key, value in mapping.items():
    existing_key = {
        "backup-storage-type": "LIMRISTEM_MAIL_BACKUP_STORAGE_TYPE",
        "backup-storage-remote-name": "LIMRISTEM_MAIL_BACKUP_STORAGE_REMOTE_NAME",
        "backup-storage-path": "LIMRISTEM_MAIL_BACKUP_STORAGE_PATH",
        "backup-storage-host": "LIMRISTEM_MAIL_BACKUP_STORAGE_HOST",
        "backup-storage-port": "LIMRISTEM_MAIL_BACKUP_STORAGE_PORT",
        "backup-storage-user": "LIMRISTEM_MAIL_BACKUP_STORAGE_USER",
        "backup-storage-password": "LIMRISTEM_MAIL_BACKUP_STORAGE_PASSWORD",
        "backup-storage-bucket": "LIMRISTEM_MAIL_BACKUP_STORAGE_BUCKET",
        "backup-storage-region": "LIMRISTEM_MAIL_BACKUP_STORAGE_REGION",
        "backup-storage-endpoint": "LIMRISTEM_MAIL_BACKUP_STORAGE_ENDPOINT",
        "backup-storage-access-key-id": "LIMRISTEM_MAIL_BACKUP_STORAGE_ACCESS_KEY_ID",
        "backup-storage-secret-access-key": "LIMRISTEM_MAIL_BACKUP_STORAGE_SECRET_ACCESS_KEY",
    }[key]
    existing[existing_key] = value
lines = [f"{key}={value}" for key, value in sorted(existing.items())]
backup_env_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
PY
}

sync_legacy_schedule_state() {
  ensure_state_files
  python3 - "$BACKUP_ENV_FILE" "$SCHEDULES_FILE" <<'PY'
import json
import pathlib
import sys

backup_env_path = pathlib.Path(sys.argv[1])
schedules_path = pathlib.Path(sys.argv[2])
try:
    schedules = json.loads(schedules_path.read_text(encoding="utf-8") or "[]")
except Exception:
    schedules = []
schedule = next((item for item in schedules if item.get("id") == "default-backup"), None)
if not schedule and schedules:
    schedule = schedules[0]
if not schedule:
    raise SystemExit(0)
existing = {}
if backup_env_path.exists():
    for line in backup_env_path.read_text(encoding="utf-8").splitlines():
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        existing[key] = value
existing["LIMRISTEM_MAIL_BACKUP_ONCALENDAR"] = schedule.get("oncalendar", "*-*-* 03:15:00")
existing["LIMRISTEM_MAIL_BACKUP_TYPE"] = schedule.get("backup_type", "auto")
existing["LIMRISTEM_MAIL_BACKUP_FULL_WEEKDAY"] = schedule.get("full_weekday", "Sun")
existing["LIMRISTEM_MAIL_BACKUP_COMPRESSION"] = schedule.get("compression", "gzip")
existing["LIMRISTEM_MAIL_BACKUP_INCLUDE_REDIS"] = schedule.get("include_redis", "yes")
existing["LIMRISTEM_MAIL_BACKUP_DB_MODE"] = "logical" if schedule.get("include_database", "yes") == "yes" else "none"
backup_env_path.write_text("\n".join(f"{key}={value}" for key, value in sorted(existing.items())) + "\n", encoding="utf-8")
PY
}

save_schedule() {
  ensure_state_files
  local raw_json=$1
  local as_json=${2:-no}
  local normalized
  normalized=$(normalize_schedule_json "$raw_json")
  write_schedule_record "$normalized"
  sync_legacy_schedule_state
  load_env
  sync_schedule_units
  if [[ "$as_json" == "yes" || "$as_json" == "--json" ]]; then
    printf '%s\n' "$normalized"
  fi
}

delete_schedule() {
  ensure_state_files
  local schedule_id=$1
  validate_resource_id "$schedule_id"
  python3 - "$SCHEDULES_FILE" "$schedule_id" <<'PY'
import json
import pathlib
import sys
path = pathlib.Path(sys.argv[1])
items = [item for item in json.loads(path.read_text(encoding="utf-8") or "[]") if item.get("id") != sys.argv[2]]
path.write_text(json.dumps(items, indent=2) + "\n", encoding="utf-8")
PY
  sync_legacy_schedule_state
  load_env
  sync_schedule_units
}

schedule_unit_name() {
  python3 -c 'import re,sys; print("limristem-mail-backup-schedule-" + re.sub(r"[^A-Za-z0-9_.-]+", "-", sys.argv[1]).strip("-") )' "$1"
}

sync_schedule_units() {
  ensure_state_files
  local base_dir=${LIMRISTEM_MAIL_BASE_DIR:-/opt/limristem-mail}
  local global_enabled=${LIMRISTEM_MAIL_ENABLE_BACKUP_TIMER:-yes}
  local systemd_dir=/etc/systemd/system
  if ! limristem_mail_path_is_writable "$systemd_dir/.limristem-mail-backup-schedule-probe"; then
    echo "Skipping backup schedule unit sync because ${systemd_dir} is not writable in the current environment." >&2
    return 0
  fi
  local metadata
  metadata=$(python3 - "$SCHEDULES_FILE" <<'PY'
import json
import pathlib
import sys
for item in json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8") or "[]"):
    print("\t".join([
        item.get("id", ""),
        item.get("name", ""),
        item.get("enabled", "yes"),
        item.get("oncalendar", "*-*-* 03:15:00").replace("\n", "\\n"),
    ]))
PY
)
  local unit_file timer_file unit_name schedule_id schedule_name enabled oncalendar
  while IFS= read -r existing_timer; do
    [[ -n "$existing_timer" ]] || continue
    systemctl disable --now "$(basename "$existing_timer")" >/dev/null 2>&1 || true
    rm -f "$existing_timer" "${existing_timer%.timer}.service"
  done < <(find "$systemd_dir" -maxdepth 1 -type f -name 'limristem-mail-backup-schedule-*.timer' 2>/dev/null)
  systemctl disable --now limristem-mail-backup.timer >/dev/null 2>&1 || true
  while IFS=$'\t' read -r schedule_id schedule_name enabled oncalendar; do
    [[ -n "$schedule_id" ]] || continue
    unit_name=$(schedule_unit_name "$schedule_id")
    unit_file=$systemd_dir/${unit_name}.service
    timer_file=$systemd_dir/${unit_name}.timer
    cat > "$unit_file" <<SERVICE
[Unit]
Description=Limristem eMail backup job (${schedule_name})
After=network-online.target mariadb.service redis-server.service
Wants=network-online.target

[Service]
Type=oneshot
EnvironmentFile=${ENV_FILE}
EnvironmentFile=${BACKUP_ENV_FILE}
ExecStart=$(runtime_bin_dir)/backup.sh --schedule-id ${schedule_id}
User=root
Group=root
SERVICE
    {
      printf '[Unit]\n'
      printf 'Description=Run Limristem eMail backup schedule (%s)\n\n' "$schedule_name"
      printf '[Timer]\n'
      python3 - "$oncalendar" <<'PY'
import sys
for line in sys.argv[1].replace("\\n", "\n").splitlines():
    line = line.strip()
    if line:
        print(f"OnCalendar={line}")
PY
      printf 'Persistent=true\n\n'
      printf '[Install]\nWantedBy=timers.target\n'
    } > "$timer_file"
  done <<< "$metadata"
  systemctl daemon-reload >/dev/null 2>&1 || true
  while IFS=$'\t' read -r schedule_id schedule_name enabled oncalendar; do
    [[ -n "$schedule_id" ]] || continue
    unit_name=$(schedule_unit_name "$schedule_id")
    if [[ "$global_enabled" == "yes" && "$enabled" == "yes" ]]; then
      systemctl enable --now "${unit_name}.timer" >/dev/null 2>&1 || true
    else
      systemctl disable --now "${unit_name}.timer" >/dev/null 2>&1 || true
    fi
  done <<< "$metadata"
}

sync_storage_profile() {
  ensure_state_files
  local storage_type remote_name remote_path host port user password bucket region endpoint access_key secret_key remote_target obscured_password
  storage_type=$(storage_value backup-storage-type)
  remote_name=$(storage_value backup-storage-remote-name)
  remote_path=$(storage_value backup-storage-path)
  host=$(storage_value backup-storage-host)
  port=$(storage_value backup-storage-port)
  user=$(storage_value backup-storage-user)
  password=$(storage_value backup-storage-password)
  bucket=$(storage_value backup-storage-bucket)
  region=$(storage_value backup-storage-region)
  endpoint=$(storage_value backup-storage-endpoint)
  access_key=$(storage_value backup-storage-access-key-id)
  secret_key=$(storage_value backup-storage-secret-access-key)

  mkdir -p "$(dirname "$RCLONE_CONFIG_FILE")"

  case "$storage_type" in
    local|'')
      set_env_value "$BACKUP_ENV_FILE" "${BACKUP_ENV_MAP[backup-remote-targets]}" ""
      rm -f "$RCLONE_CONFIG_FILE"
      return 0
      ;;
    s3)
      remote_target="${remote_name}:${bucket}"
      if [[ -n "$remote_path" ]]; then
        remote_target="${remote_target}/$(printf '%s' "$remote_path" | sed 's#^/*##')"
      fi
      {
        printf '[%s]\n' "$remote_name"
        printf 'type = s3\n'
        printf 'provider = AWS\n'
        printf 'env_auth = false\n'
        printf 'access_key_id = %s\n' "$access_key"
        printf 'secret_access_key = %s\n' "$secret_key"
        [[ -n "$region" ]] && printf 'region = %s\n' "$region"
        [[ -n "$endpoint" ]] && printf 'endpoint = %s\n' "$endpoint"
      } > "$RCLONE_CONFIG_FILE"
      ;;
    sftp)
      remote_target="${remote_name}:$( [[ -n "$remote_path" ]] && printf '%s' "$remote_path" || printf '/' )"
      obscured_password=$(obscure_rclone_password "$password")
      {
        printf '[%s]\n' "$remote_name"
        printf 'type = sftp\n'
        printf 'host = %s\n' "$host"
        [[ -n "$port" ]] && printf 'port = %s\n' "$port"
        printf 'user = %s\n' "$user"
        [[ -n "$obscured_password" ]] && printf 'pass = %s\n' "$obscured_password"
      } > "$RCLONE_CONFIG_FILE"
      ;;
    ftp|ftps)
      remote_target="${remote_name}:$( [[ -n "$remote_path" ]] && printf '%s' "$remote_path" || printf '/' )"
      obscured_password=$(obscure_rclone_password "$password")
      {
        printf '[%s]\n' "$remote_name"
        printf 'type = ftp\n'
        printf 'host = %s\n' "$host"
        [[ -n "$port" ]] && printf 'port = %s\n' "$port"
        printf 'user = %s\n' "$user"
        [[ -n "$obscured_password" ]] && printf 'pass = %s\n' "$obscured_password"
        if [[ "$storage_type" == "ftps" ]]; then
          printf 'tls = true\n'
        fi
      } > "$RCLONE_CONFIG_FILE"
      ;;
    *)
      echo "Unknown backup storage type: $storage_type" >&2
      exit 1
      ;;
  esac

  chmod 600 "$RCLONE_CONFIG_FILE"
  set_env_value "$BACKUP_ENV_FILE" "${BACKUP_ENV_MAP[backup-remote-targets]}" "$remote_target"
}

show_backups() {
  local as_json=${1:-no}
  local key env_key value
  if [[ "$as_json" == "yes" ]]; then
    printf '{'
    local first=yes
    for key in "${!BACKUP_ENV_MAP[@]}"; do
      env_key=${BACKUP_ENV_MAP[$key]}
      value=${!env_key:-${BACKUP_DEFAULT_MAP[$key]}}
      if [[ "$first" == "yes" ]]; then
        first=no
      else
        printf ','
      fi
      python3 - "$key" "$value" <<'PY'
import json
import sys
print(json.dumps(sys.argv[1]) + ":" + json.dumps(sys.argv[2]), end="")
PY
    done
    printf '}\n'
    return 0
  fi
  for key in "${!BACKUP_ENV_MAP[@]}"; do
    env_key=${BACKUP_ENV_MAP[$key]}
    value=${!env_key:-${BACKUP_DEFAULT_MAP[$key]}}
    printf '%s=%s\n' "$key" "$value"
  done | sort
}

list_runs() {
  local as_json=${1:-no}
  local backup_dir=${LIMRISTEM_MAIL_BACKUP_LOCAL_DIR:-/var/backups/limristem-mail}
  if [[ ! -d "$backup_dir" ]]; then
    [[ "$as_json" == "yes" ]] && printf '[]\n' || true
    return 0
  fi

  if [[ "$as_json" == "yes" ]]; then
    python3 - "$backup_dir" <<'PY'
import json
import sys
from pathlib import Path

backup_dir = Path(sys.argv[1])
runs = []
for path in sorted((p for p in backup_dir.iterdir() if p.is_dir()), key=lambda p: p.name, reverse=True):
    stat = path.stat()
    runs.append(
        {
            "name": path.name,
            "path": str(path),
            "mtime": int(stat.st_mtime),
            "metadata": str(path / "metadata.env") if (path / "metadata.env").exists() else None,
        }
    )
print(json.dumps(runs))
PY
    return 0
  fi

  find "$backup_dir" -mindepth 1 -maxdepth 1 -type d -printf '%TY-%Tm-%Td %TH:%TM %p\n' | sort -r
}

set_backup_value() {
  local key=$1
  local value=$2
  local env_key=${BACKUP_ENV_MAP[$key]:-}
  local file
  if [[ -z "$env_key" ]]; then
    echo "Unknown backup key: $key" >&2
    exit 1
  fi
  file=$(env_file_for_key "$key")
  set_env_value "$file" "$env_key" "$value"
  load_env
  sync_storage_profile
  sync_legacy_storage_state
  sync_legacy_schedule_state
  sync_schedule_units
}

set_backup_values_batch() {
  if (( $# == 0 || $# % 2 != 0 )); then
    echo "set-many requires <key> <value> pairs" >&2
    exit 1
  fi
  local key value env_key file
  while (( $# > 0 )); do
    key=$1
    value=$2
    env_key=${BACKUP_ENV_MAP[$key]:-}
    if [[ -z "$env_key" ]]; then
      echo "Unknown backup key: $key" >&2
      exit 1
    fi
    file=$(env_file_for_key "$key")
    set_env_value "$file" "$env_key" "$value"
    shift 2
  done
  load_env
  sync_storage_profile
  sync_legacy_storage_state
  sync_legacy_schedule_state
  sync_schedule_units
}

run_backup_now() {
  local base_dir=${LIMRISTEM_MAIL_BASE_DIR:-/opt/limristem-mail}
  local runtime_bin
  runtime_bin=$(runtime_bin_dir)
  if [[ -n ${1:-} ]]; then
    bash "$runtime_bin/backup.sh" --schedule-id "$1"
  else
    bash "$runtime_bin/backup.sh"
  fi
}

require_root
load_env
ensure_state_files

command=${1:-}
case "$command" in
  show)
    if [[ ${2:-} == "--json" ]]; then
      show_backups yes
    else
      show_backups no
    fi
    ;;
  list)
    if [[ ${2:-} == "--json" ]]; then
      list_runs yes
    else
      list_runs no
    fi
    ;;
  list-schedules)
    list_schedules_json
    ;;
  save-schedule)
    save_schedule "${2:?json required}" "${3:-no}"
    ;;
  delete-schedule)
    delete_schedule "${2:?id required}"
    ;;
  list-storages)
    list_storages_json
    ;;
  save-storage)
    save_storage "${2:?json required}" "${3:-no}"
    ;;
  delete-storage)
    delete_storage "${2:?id required}"
    ;;
  test-storage)
    test_storage_connection "${2:?id required}" "${3:-no}"
    ;;
  consume-private-key)
    consume_private_key "${2:?token required}"
    ;;
  set)
    set_backup_value "${2:?key required}" "${3-}"
    ;;
  set-many)
    shift
    set_backup_values_batch "$@"
    ;;
  sync-schedules)
    sync_schedule_units
    ;;
  run)
    run_backup_now "${2:-}"
    ;;
  *)
    usage >&2
    exit 1
    ;;
esac
