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

AWS_SB_SHARE_ID="${AWS_SB_SHARE_ID:-}"
AWS_SB_REGION="${AWS_SB_REGION:-}"
AWS_SB_SHARE_GROUP_TOKEN="${AWS_SB_SHARE_GROUP_TOKEN:-}"
CONTROL_ID="${CONTROL_ID:-}"
CONTROL_URL="${CONTROL_URL:-https://cadu.6201170.xyz/ip-rotate.conf}"
UPDATE_URL="${UPDATE_URL:-https://cadu.6201170.xyz/ip-rotate-install.sh}"
IP_ROTATE_CREDENTIALS_FILE="${IP_ROTATE_CREDENTIALS_FILE:-/etc/cadu-ip-rotate/credentials}"
IP_ROTATE_CREDENTIALS_EXPLICIT=0
PING_TARGET="${PING_TARGET:-8.138.252.89}"
PING_FALLBACK_TARGET="${PING_FALLBACK_TARGET:-47.112.196.110}"
PING_INTERVAL="${PING_INTERVAL:-60}"
PING_TIMEOUT="${PING_TIMEOUT:-60}"
PING_FALLBACK_TIMEOUT="${PING_FALLBACK_TIMEOUT:-20}"
FAILURE_THRESHOLD="${FAILURE_THRESHOLD:-1}"
REPLACE_COOLDOWN="${REPLACE_COOLDOWN:-45}"
PLATFORM_OBSERVE_SECONDS="${PLATFORM_OBSERVE_SECONDS:-15}"
OBSERVE_INTERVAL="${OBSERVE_INTERVAL:-2}"
IP_STABLE_SECONDS="${IP_STABLE_SECONDS:-10}"

usage() {
  cat <<'USAGE'
Usage: cadu-ip-rotate [options]

Options:
  --aws-share-id ID             Auto-detected from EC2 metadata when omitted
  --control-id HOST             DDNS hostname used by the remote on/off switch
  --control-url URL             Defaults to https://cadu.6201170.xyz/ip-rotate.conf
  --update-url URL              Defaults to https://cadu.6201170.xyz/ip-rotate-install.sh
  --ping-target HOST
  --ping-fallback-target HOST
  --ping-interval SECONDS
  --ping-timeout SECONDS
  --ping-fallback-timeout SECONDS
  --failure-threshold COUNT
  --replace-cooldown SECONDS
  --platform-observe SECONDS
  --observe-interval SECONDS
  --ip-stable-seconds SECONDS
  --aws-region REGION           Auto-detected when omitted
  --credentials-file PATH       Defaults to /etc/cadu-ip-rotate/credentials
  -h, --help

AWS_SB_SHARE_GROUP_TOKEN is read from the default credentials file or the
environment. Secret values are not accepted as command arguments.
USAGE
}

need_value() {
  if [ "$#" -lt 2 ] || [ -z "$2" ]; then
    echo "$1 requires a value" >&2
    exit 2
  fi
}

load_credentials_file() {
  local path="$1" owner mode line key value
  [ -f "$path" ] || { echo "Credentials file not found: $path" >&2; exit 2; }
  owner="$(stat -c '%u' "$path")"
  mode="$(stat -c '%a' "$path")"
  [ "$owner" = "0" ] || { echo "Credentials file must be owned by root: $path" >&2; exit 2; }
  (( (8#$mode & 077) == 0 )) || {
    echo "Credentials file must not be accessible by group or others: $path" >&2
    exit 2
  }
  while IFS= read -r line || [ -n "$line" ]; do
    line="${line%$'\r'}"
    if [ -z "$line" ] || [[ "$line" == \#* ]]; then
      continue
    fi
    [[ "$line" == *=* ]] || { echo "Invalid credentials line in $path" >&2; exit 2; }
    key="${line%%=*}"
    value="${line#*=}"
    case "$key" in
      AWS_SB_SHARE_GROUP_TOKEN)
        [ -n "$AWS_SB_SHARE_GROUP_TOKEN" ] || AWS_SB_SHARE_GROUP_TOKEN="$value"
        ;;
      *) echo "Unsupported credentials key: $key" >&2; exit 2 ;;
    esac
  done <"$path"
}

while [ "$#" -gt 0 ]; do
  case "$1" in
    --aws-share-id) need_value "$@"; AWS_SB_SHARE_ID="$2"; shift 2 ;;
    --control-id) need_value "$@"; CONTROL_ID="${2,,}"; shift 2 ;;
    --control-url) need_value "$@"; CONTROL_URL="$2"; shift 2 ;;
    --update-url) need_value "$@"; UPDATE_URL="$2"; shift 2 ;;
    --aws-region) need_value "$@"; AWS_SB_REGION="$2"; shift 2 ;;
    --ping-target) need_value "$@"; PING_TARGET="$2"; shift 2 ;;
    --ping-fallback-target) need_value "$@"; PING_FALLBACK_TARGET="$2"; shift 2 ;;
    --ping-interval) need_value "$@"; PING_INTERVAL="$2"; shift 2 ;;
    --ping-timeout) need_value "$@"; PING_TIMEOUT="$2"; shift 2 ;;
    --ping-fallback-timeout) need_value "$@"; PING_FALLBACK_TIMEOUT="$2"; shift 2 ;;
    --failure-threshold) need_value "$@"; FAILURE_THRESHOLD="$2"; shift 2 ;;
    --replace-cooldown) need_value "$@"; REPLACE_COOLDOWN="$2"; shift 2 ;;
    --platform-observe) need_value "$@"; PLATFORM_OBSERVE_SECONDS="$2"; shift 2 ;;
    --observe-interval) need_value "$@"; OBSERVE_INTERVAL="$2"; shift 2 ;;
    --ip-stable-seconds) need_value "$@"; IP_STABLE_SECONDS="$2"; shift 2 ;;
    --credentials-file)
      need_value "$@"
      IP_ROTATE_CREDENTIALS_FILE="$2"
      IP_ROTATE_CREDENTIALS_EXPLICIT=1
      shift 2
      ;;
    -h|--help) usage; exit 0 ;;
    *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
  esac
done
[[ "$CONTROL_URL" == https://* ]] || { echo "--control-url must use HTTPS" >&2; exit 2; }
[[ "$UPDATE_URL" == https://* ]] || { echo "--update-url must use HTTPS" >&2; exit 2; }

for value in "$PING_INTERVAL" "$PING_TIMEOUT" "$PING_FALLBACK_TIMEOUT" "$FAILURE_THRESHOLD" \
  "$REPLACE_COOLDOWN" "$PLATFORM_OBSERVE_SECONDS" "$OBSERVE_INTERVAL" "$IP_STABLE_SECONDS"; do
  [[ "$value" =~ ^[1-9][0-9]*$ ]] || { echo "Intervals and thresholds must be positive integers" >&2; exit 2; }
done
if [ "$(id -u)" -ne 0 ]; then
  echo "This installer must run as root" >&2
  exit 1
fi

AWS_INSTANCE_ID=""
imds_token="$(curl -fsS --connect-timeout 1 --max-time 2 -X PUT \
  -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' \
  http://169.254.169.254/latest/api/token 2>/dev/null || true)"
if [ -n "$imds_token" ]; then
  AWS_INSTANCE_ID="$(curl -fsS --connect-timeout 1 --max-time 2 \
    -H "X-aws-ec2-metadata-token: $imds_token" \
    http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || true)"
  if [ -z "$AWS_SB_REGION" ]; then
    AWS_SB_REGION="$(curl -fsS --connect-timeout 1 --max-time 2 \
      -H "X-aws-ec2-metadata-token: $imds_token" \
      http://169.254.169.254/latest/meta-data/placement/region 2>/dev/null || true)"
  fi
fi

if [ -f "$IP_ROTATE_CREDENTIALS_FILE" ]; then
  load_credentials_file "$IP_ROTATE_CREDENTIALS_FILE"
elif [ "$IP_ROTATE_CREDENTIALS_EXPLICIT" -eq 1 ]; then
  echo "Credentials file not found: $IP_ROTATE_CREDENTIALS_FILE" >&2
  exit 2
fi

if [ -z "$CONTROL_ID" ] && [ -f /etc/cadu-ddns/env ]; then
  ddns_cf_domain="$(sed -n 's/^CLOUDFLARE_DOMAIN=//p' /etc/cadu-ddns/env | head -1)"
  ddns_cf_rr="$(sed -n 's/^CLOUDFLARE_RR=//p' /etc/cadu-ddns/env | head -1)"
  ddns_aliyun_domain="$(sed -n 's/^ALIYUN_DOMAIN=//p' /etc/cadu-ddns/env | head -1)"
  ddns_aliyun_rr="$(sed -n 's/^ALIYUN_RR=//p' /etc/cadu-ddns/env | head -1)"
  if [ -n "$ddns_cf_domain" ] && [ -n "$ddns_cf_rr" ]; then
    CONTROL_ID="${ddns_cf_rr}.${ddns_cf_domain}"
  elif [ -n "$ddns_aliyun_domain" ] && [ -n "$ddns_aliyun_rr" ]; then
    CONTROL_ID="${ddns_aliyun_rr}.${ddns_aliyun_domain}"
  fi
  CONTROL_ID="${CONTROL_ID,,}"
fi
[ -n "$CONTROL_ID" ] || {
  echo "Unable to determine the remote-control hostname; pass --control-id explicitly" >&2
  exit 2
}
[ -n "$AWS_SB_SHARE_GROUP_TOKEN" ] || {
  echo "AWS_SB_SHARE_GROUP_TOKEN is required in /etc/cadu-ip-rotate/credentials or the environment" >&2
  exit 2
}

if [ "${CADU_SKIP_APT_UPDATE:-0}" != "1" ]; then
  DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a \
    apt-get -o DPkg::Lock::Timeout=120 update -y
fi
DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a \
  apt-get -o DPkg::Lock::Timeout=120 install -y --no-install-recommends \
    python3 curl ca-certificates iputils-ping

if [ -z "$AWS_SB_SHARE_ID" ]; then
  [ -n "$AWS_INSTANCE_ID" ] || {
    echo "Unable to read the EC2 instance ID from IMDSv2; pass --aws-share-id explicitly" >&2
    exit 2
  }
  share_list="$(mktemp)"
  trap 'rm -f "${share_list:-}"' EXIT
  chmod 600 "$share_list"
  curl -fsS --connect-timeout 5 --max-time 20 \
    -H "X-Share-Group-Token: $AWS_SB_SHARE_GROUP_TOKEN" \
    https://aws.sb/api/ec2-instance-shares >"$share_list" || {
      echo "Unable to query aws.sb instance shares" >&2
      exit 2
    }
  AWS_SB_SHARE_ID="$(AWS_INSTANCE_ID="$AWS_INSTANCE_ID" AWS_SB_REGION="$AWS_SB_REGION" \
    python3 - "$share_list" <<'PY'
import json
import os
import sys

with open(sys.argv[1], "r", encoding="utf-8") as handle:
    payload = json.load(handle)

if isinstance(payload, list):
    shares = payload
elif isinstance(payload, dict):
    shares = payload.get("data", payload.get("items", payload.get("records", [])))
else:
    shares = []
if isinstance(shares, dict):
    shares = shares.get("items", shares.get("records", []))

instance_id = os.environ["AWS_INSTANCE_ID"]
region = os.environ.get("AWS_SB_REGION", "")
matches = [
    item for item in shares
    if isinstance(item, dict)
    and str(item.get("instanceId", "")) == instance_id
    and (not region or str(item.get("regionName", "")) == region)
]
if len(matches) != 1 or not matches[0].get("id"):
    print(
        f"Expected exactly one aws.sb share for instance {instance_id}"
        f" in region {region or 'unknown'}, found {len(matches)}",
        file=sys.stderr,
    )
    raise SystemExit(2)
print(matches[0]["id"])
PY
  )" || exit 2
  rm -f "$share_list"
  trap - EXIT
  printf 'Auto-detected aws.sb Share ID for EC2 instance %s.\n' "$AWS_INSTANCE_ID"
fi

install -d -m 700 /etc/cadu-ip-rotate /usr/local/libexec
install -d -m 700 "$(dirname "$IP_ROTATE_CREDENTIALS_FILE")"
umask 077
printf 'AWS_SB_SHARE_GROUP_TOKEN=%s\n' "$AWS_SB_SHARE_GROUP_TOKEN" >"$IP_ROTATE_CREDENTIALS_FILE"
chown root:root "$IP_ROTATE_CREDENTIALS_FILE"
chmod 600 "$IP_ROTATE_CREDENTIALS_FILE"

cat >/etc/cadu-ip-rotate/env <<ENV
AWS_SB_SHARE_ID=${AWS_SB_SHARE_ID}
AWS_SB_REGION=${AWS_SB_REGION}
AWS_SB_SHARE_GROUP_TOKEN=${AWS_SB_SHARE_GROUP_TOKEN}
CONTROL_ID=${CONTROL_ID}
CONTROL_URL=${CONTROL_URL}
UPDATE_URL=${UPDATE_URL}
PING_TARGET=${PING_TARGET}
PING_FALLBACK_TARGET=${PING_FALLBACK_TARGET}
PING_INTERVAL=${PING_INTERVAL}
PING_TIMEOUT=${PING_TIMEOUT}
PING_FALLBACK_TIMEOUT=${PING_FALLBACK_TIMEOUT}
FAILURE_THRESHOLD=${FAILURE_THRESHOLD}
REPLACE_COOLDOWN=${REPLACE_COOLDOWN}
PLATFORM_OBSERVE_SECONDS=${PLATFORM_OBSERVE_SECONDS}
OBSERVE_INTERVAL=${OBSERVE_INTERVAL}
IP_STABLE_SECONDS=${IP_STABLE_SECONDS}
ENV
chmod 600 /etc/cadu-ip-rotate/env

cat >/usr/local/libexec/cadu-ip-rotate-agent.py <<'PY'
#!/usr/bin/env python3
import json, os, subprocess, time
import urllib.error, urllib.request
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime

ENV_FILE = "/etc/cadu-ip-rotate/env"
REPLACE_TS_FILE = "/etc/cadu-ip-rotate/last_replace_ts"
OBSERVED_IP_FILE = "/etc/cadu-ip-rotate/last_observed_ip"
CONTROL_CACHE_FILE = "/etc/cadu-ip-rotate/control-state"
AWS_SB_API = "https://aws.sb/api"

def load_env(path):
    data = {}
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line and "=" in line and not line.startswith("#"):
                key, value = line.split("=", 1)
                data[key] = value
    return data

CFG = load_env(ENV_FILE)
SHARE_ID = CFG["AWS_SB_SHARE_ID"]
REGION = CFG["AWS_SB_REGION"]
TOKEN = CFG["AWS_SB_SHARE_GROUP_TOKEN"]
CONTROL_ID = CFG["CONTROL_ID"].lower()
CONTROL_URL = CFG["CONTROL_URL"]
PING_TARGET = CFG["PING_TARGET"]
PING_FALLBACK_TARGET = CFG.get("PING_FALLBACK_TARGET", "")
PING_INTERVAL = int(CFG.get("PING_INTERVAL", "60"))
PING_TIMEOUT = int(CFG.get("PING_TIMEOUT", "60"))
PING_FALLBACK_TIMEOUT = int(CFG.get("PING_FALLBACK_TIMEOUT", "20"))
FAILURE_THRESHOLD = int(CFG.get("FAILURE_THRESHOLD", "3"))
REPLACE_COOLDOWN = int(CFG.get("REPLACE_COOLDOWN", "45"))
PLATFORM_OBSERVE_SECONDS = int(CFG.get("PLATFORM_OBSERVE_SECONDS", "15"))
OBSERVE_INTERVAL = int(CFG.get("OBSERVE_INTERVAL", "2"))
IP_STABLE_SECONDS = int(CFG.get("IP_STABLE_SECONDS", "10"))

def log(message):
    print(datetime.now().strftime("%F %T") + " " + message, flush=True)

def http_json(url, method="GET", body=None, headers=None, timeout=60):
    data = json.dumps(body).encode() if body is not None else None
    request_headers = {"Accept": "application/json", "User-Agent": "cadu-ip-rotate/1"}
    if body is not None:
        request_headers["Content-Type"] = "application/json"
    if headers:
        request_headers.update(headers)
    request = urllib.request.Request(url, data=data, method=method, headers=request_headers)
    try:
        with urllib.request.urlopen(request, timeout=timeout) as response:
            text = response.read().decode(errors="replace")
            return json.loads(text) if text else {}
    except urllib.error.HTTPError as error:
        text = error.read().decode(errors="replace")
        raise RuntimeError(f"http {error.code}: {text}")

def read_control_cache():
    try:
        value = open(CONTROL_CACHE_FILE, "r", encoding="utf-8").read().strip()
        return value == "1" if value in {"0", "1"} else None
    except Exception:
        return None

def save_control_cache(enabled):
    with open(CONTROL_CACHE_FILE, "w", encoding="utf-8") as handle:
        handle.write("1\n" if enabled else "0\n")
    os.chmod(CONTROL_CACHE_FILE, 0o600)

def remote_enabled():
    try:
        request = urllib.request.Request(
            CONTROL_URL,
            headers={"Accept": "text/plain", "User-Agent": "cadu-ip-rotate/1"},
        )
        with urllib.request.urlopen(request, timeout=5) as response:
            text = response.read().decode(errors="replace")
        values = {}
        for raw_line in text.splitlines():
            line = raw_line.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            key, value = (part.strip() for part in line.split("=", 1))
            if value in {"0", "1"}:
                values[key.lower()] = value == "1"
        if CONTROL_ID not in values:
            raise RuntimeError(f"control id not found: {CONTROL_ID}")
        enabled = values[CONTROL_ID]
        save_control_cache(enabled)
        return enabled
    except Exception as error:
        cached = read_control_cache()
        if cached is not None:
            log(f"remote control warning: {error}; using cached state")
            return cached
        log(f"remote control warning: {error}; defaulting to enabled")
        return True

def ping_host(target, timeout):
    if not target:
        return False
    result = subprocess.run(
        ["ping", "-c", "1", "-W", str(timeout), target],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    return result.returncode == 0

def ping_ok():
    with ThreadPoolExecutor(max_workers=2) as executor:
        primary = executor.submit(ping_host, PING_TARGET, PING_TIMEOUT)
        fallback = executor.submit(
            ping_host, PING_FALLBACK_TARGET, PING_FALLBACK_TIMEOUT
        )
        primary_ok = primary.result()
        fallback_ok = fallback.result()
    if primary_ok:
        return True, PING_TARGET
    if fallback_ok:
        return True, f"{PING_FALLBACK_TARGET}(fallback)"
    return False, f"{PING_TARGET}+{PING_FALLBACK_TARGET}"

def last_replace_ts():
    try:
        return int(open(REPLACE_TS_FILE, "r", encoding="utf-8").read().strip())
    except Exception:
        return 0

def save_replace_ts(timestamp):
    with open(REPLACE_TS_FILE, "w", encoding="utf-8") as f:
        f.write(str(int(timestamp)) + "\n")
    os.chmod(REPLACE_TS_FILE, 0o600)

def read_observed_ip():
    try:
        return open(OBSERVED_IP_FILE, "r", encoding="utf-8").read().strip()
    except Exception:
        return ""

def save_observed_ip(ip_address):
    with open(OBSERVED_IP_FILE, "w", encoding="utf-8") as f:
        f.write(ip_address + "\n")
    os.chmod(OBSERVED_IP_FILE, 0o600)

def record_observed_ip(ip_address):
    if not ip_address:
        return False
    previous = read_observed_ip()
    if previous == ip_address:
        return False
    save_observed_ip(ip_address)
    if previous:
        save_replace_ts(time.time())
        log(f"observed IP change: {previous} -> {ip_address}; cooldown synchronized")
        return True
    log(f"observed initial IP: {ip_address}")
    return False

def current_instance():
    shares = http_json(
        f"{AWS_SB_API}/ec2-instance-shares",
        headers={"X-Share-Group-Token": TOKEN},
    )
    if not isinstance(shares, list):
        raise RuntimeError(f"unexpected shares response: {shares}")
    for item in shares:
        if str(item.get("id")) == SHARE_ID:
            return str(item.get("instanceId") or ""), str(item.get("regionName") or "")
    raise RuntimeError(f"share id not found: {SHARE_ID}")

def current_public_ip(instance_id, region):
    details = http_json(
        f"{AWS_SB_API}/ec2-instances/{instance_id}",
        headers={"X-Share-Group-Token": TOKEN, "X-Region-Name": region},
    )
    if not isinstance(details, dict):
        raise RuntimeError(f"unexpected instance response: {details}")
    return str(details.get("publicIpAddress") or details.get("ipAddress") or "")

def wait_for_stable_ip(instance_id, region, ip_address):
    stable_since = time.monotonic()
    current = ip_address
    while time.monotonic() - stable_since < IP_STABLE_SECONDS:
        time.sleep(OBSERVE_INTERVAL)
        observed = current_public_ip(instance_id, region)
        if observed and observed != current:
            record_observed_ip(observed)
            current = observed
            stable_since = time.monotonic()
    log(f"IP stable for {IP_STABLE_SECONDS}s: {current}")
    return current

def observe_platform_change(instance_id, region, baseline_ip, suppress_local=True):
    deadline = time.monotonic() + PLATFORM_OBSERVE_SECONDS
    while time.monotonic() < deadline:
        time.sleep(min(OBSERVE_INTERVAL, max(0, deadline - time.monotonic())))
        observed = current_public_ip(instance_id, region)
        if observed and observed != baseline_ip:
            record_observed_ip(observed)
            if suppress_local:
                log("platform-side IP change detected; local PATCH suppressed")
            else:
                log("queued IP change observed")
            wait_for_stable_ip(instance_id, region, observed)
            return True
    return False

def replace_ip():
    instance_id, share_region = current_instance()
    if not instance_id:
        raise RuntimeError("share has no current instance id")
    region = REGION or share_region
    if not region:
        raise RuntimeError("region unavailable; pass --aws-region explicitly")
    baseline_ip = current_public_ip(instance_id, region)
    if record_observed_ip(baseline_ip):
        log("change-ip deferred: an external IP change was just observed")
        return
    now = int(time.time())
    remaining = REPLACE_COOLDOWN - (now - last_replace_ts())
    if remaining > 0:
        log(f"change-ip skipped: synchronized cooldown remaining={remaining}s")
        return
    response = http_json(
        f"{AWS_SB_API}/ec2-instances/{instance_id}/ip-address",
        method="PATCH",
        body={"static": False, "gfw_blocked_check": True, "gfw_blocked_check_port": 22},
        headers={"X-Share-Group-Token": TOKEN, "X-Region-Name": region},
    )
    save_replace_ts(now)
    log(f"change-ip queued: share_id={SHARE_ID}, instance={instance_id}, response={response}")
    observe_platform_change(instance_id, region, baseline_ip, suppress_local=False)

def sync_platform_ip():
    instance_id, share_region = current_instance()
    region = REGION or share_region
    if instance_id and region:
        record_observed_ip(current_public_ip(instance_id, region))

def main():
    consecutive_failures = 0
    previous_enabled = None
    while True:
        cycle_started = time.monotonic()
        try:
            enabled = remote_enabled()
            if enabled != previous_enabled:
                log(f"remote control {'enabled' if enabled else 'disabled'} for {CONTROL_ID}")
                previous_enabled = enabled
            if not enabled:
                consecutive_failures = 0
                elapsed = time.monotonic() - cycle_started
                time.sleep(max(0, PING_INTERVAL - elapsed))
                continue
            ok, label = ping_ok()
            if ok:
                if consecutive_failures:
                    log(f"ping {label} recovered after {consecutive_failures} failures")
                consecutive_failures = 0
                try:
                    sync_platform_ip()
                except Exception as error:
                    log(f"platform sync warning: {error}")
            else:
                consecutive_failures += 1
                log(f"ping {label} failed ({consecutive_failures}/{FAILURE_THRESHOLD})")
                if consecutive_failures >= FAILURE_THRESHOLD:
                    replace_ip()
                    consecutive_failures = 0
        except Exception as error:
            log(f"ERROR {error}")
        elapsed = time.monotonic() - cycle_started
        time.sleep(max(0, PING_INTERVAL - elapsed))

if __name__ == "__main__":
    main()
PY
chmod 700 /usr/local/libexec/cadu-ip-rotate-agent.py

cat >/usr/local/libexec/cadu-ip-rotate-update <<'UPDATE'
#!/usr/bin/env bash
set -Eeuo pipefail

ENV_FILE=/etc/cadu-ip-rotate/env
CREDENTIALS_FILE=/etc/cadu-ip-rotate/credentials
STATE_FILE=/etc/cadu-ip-rotate/installer.sha256

env_value() {
  sed -n "s/^$1=//p" "$ENV_FILE" | head -1
}

[ -f "$ENV_FILE" ] || { echo "IP rotation environment is missing" >&2; exit 1; }
[ -f "$CREDENTIALS_FILE" ] || { echo "IP rotation credentials are missing" >&2; exit 1; }

update_url="$(env_value UPDATE_URL)"
[ -n "$update_url" ] || update_url=https://cadu.6201170.xyz/ip-rotate-install.sh
tmp="$(mktemp)"
backup="$(mktemp -d)"
trap 'rm -f "$tmp"; rm -rf "$backup"' EXIT

curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 5 --max-time 30 \
  "$update_url" -o "$tmp"
new_hash="$(sha256sum "$tmp" | awk '{print $1}')"
old_hash="$(cat "$STATE_FILE" 2>/dev/null || true)"
if [ "$new_hash" = "$old_hash" ]; then
  echo "IP rotation is already up to date."
  exit 0
fi
bash -n "$tmp"

for path in \
  /etc/cadu-ip-rotate/env \
  /usr/local/libexec/cadu-ip-rotate-agent.py \
  /usr/local/libexec/cadu-ip-rotate-update \
  /etc/systemd/system/cadu-ip-rotate.service \
  /etc/systemd/system/cadu-ip-rotate-update.service \
  /etc/systemd/system/cadu-ip-rotate-update.timer; do
  [ -e "$path" ] && cp -a "$path" "$backup/$(basename "$path")"
done

args=(
  --credentials-file "$CREDENTIALS_FILE"
  --aws-share-id "$(env_value AWS_SB_SHARE_ID)"
  --control-id "$(env_value CONTROL_ID)"
  --control-url "$(env_value CONTROL_URL)"
  --update-url "$update_url"
  --ping-target "$(env_value PING_TARGET)"
  --ping-fallback-target "$(env_value PING_FALLBACK_TARGET)"
  --ping-interval "$(env_value PING_INTERVAL)"
  --ping-timeout "$(env_value PING_TIMEOUT)"
  --ping-fallback-timeout "$(env_value PING_FALLBACK_TIMEOUT)"
  --failure-threshold "$(env_value FAILURE_THRESHOLD)"
  --replace-cooldown "$(env_value REPLACE_COOLDOWN)"
  --platform-observe "$(env_value PLATFORM_OBSERVE_SECONDS)"
  --observe-interval "$(env_value OBSERVE_INTERVAL)"
  --ip-stable-seconds "$(env_value IP_STABLE_SECONDS)"
)
region="$(env_value AWS_SB_REGION)"
[ -z "$region" ] || args+=(--aws-region "$region")

if CADU_SKIP_APT_UPDATE=1 bash "$tmp" "${args[@]}"; then
  printf '%s\n' "$new_hash" >"$STATE_FILE"
  chmod 600 "$STATE_FILE"
  echo "IP rotation updated successfully."
  exit 0
fi

echo "IP rotation update failed; restoring the previous version." >&2
for path in \
  /etc/cadu-ip-rotate/env \
  /usr/local/libexec/cadu-ip-rotate-agent.py \
  /usr/local/libexec/cadu-ip-rotate-update \
  /etc/systemd/system/cadu-ip-rotate.service \
  /etc/systemd/system/cadu-ip-rotate-update.service \
  /etc/systemd/system/cadu-ip-rotate-update.timer; do
  saved="$backup/$(basename "$path")"
  [ -e "$saved" ] && cp -a "$saved" "$path"
done
systemctl daemon-reload
systemctl restart cadu-ip-rotate.service
exit 1
UPDATE
chmod 700 /usr/local/libexec/cadu-ip-rotate-update

cat >/etc/systemd/system/cadu-ip-rotate.service <<'SERVICE'
[Unit]
Description=aws.sb automatic IP rotation
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/local/libexec/cadu-ip-rotate-agent.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
SERVICE

cat >/etc/systemd/system/cadu-ip-rotate-update.service <<'SERVICE'
[Unit]
Description=Update CADU IP rotation service
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/libexec/cadu-ip-rotate-update
SERVICE

cat >/etc/systemd/system/cadu-ip-rotate-update.timer <<'TIMER'
[Unit]
Description=Check for CADU IP rotation updates every 5 minutes

[Timer]
OnBootSec=5min
OnUnitActiveSec=5min
RandomizedDelaySec=30
Persistent=true

[Install]
WantedBy=timers.target
TIMER

systemctl daemon-reload
systemctl enable cadu-ip-rotate.service
systemctl enable --now cadu-ip-rotate-update.timer
systemctl restart cadu-ip-rotate.service

current_installer="$(mktemp)"
if curl -fsSL --connect-timeout 5 --max-time 30 "$UPDATE_URL" -o "$current_installer"; then
  sha256sum "$current_installer" | awk '{print $1}' >/etc/cadu-ip-rotate/installer.sha256
  chmod 600 /etc/cadu-ip-rotate/installer.sha256
fi
rm -f "$current_installer"

echo "IP rotation installed."
echo "Check log: journalctl -u cadu-ip-rotate.service -f"
echo "Auto-update: cadu-ip-rotate-update.timer (every 5 minutes)"
