#!/bin/sh
# ENCO Gateway one-line installer (Agent-less provisioning).
#
# Run this ON the Wiren Board controller (you already have SSH access):
#   curl -fsSL https://encohub.ru/install.sh | sudo sh -s -- --token <INSTALLER_TOKEN>
#
# Обновить уже стоящий шлюз, не трогая дом (комнаты, устройства, сцены, доступы):
#   curl -fsSL https://encohub.ru/install.sh | sudo sh -s -- --update
#
# It fetches the Gateway bundle from the Cloud using the installer token, lays it
# down in /opt/enco-gateway, sets up the venv (system paho-mqtt, no internet
# needed), installs the systemd unit and starts the service. This does the same
# work the resident ENCO Agent used to do over SSH — without any app on a PC.
set -eu

API_BASE="https://encohub.ru"
TOKEN=""
MODE="install_new"   # install_new | replace_after_backup | update | restore | backups | uninstall
DRY_RUN=0
RESTORE_NAME=""
WIPE_CONFIRMED=0

while [ $# -gt 0 ]; do
  case "$1" in
    --token) TOKEN="${2:-}"; shift 2 ;;
    --token=*) TOKEN="${1#*=}"; shift ;;
    --api) API_BASE="${2:-}"; shift 2 ;;
    --api=*) API_BASE="${1#*=}"; shift ;;
    --replace) MODE="replace_after_backup"; shift ;;
    --yes-wipe-home) WIPE_CONFIRMED=1; shift ;;
    --update) MODE="update"; shift ;;
    # Без имени берётся самая свежая копия; конкретную задаёт --restore=<имя>.
    --restore) MODE="restore"; shift ;;
    --restore=*) MODE="restore"; RESTORE_NAME="${1#*=}"; shift ;;
    --backups) MODE="backups"; shift ;;
    --uninstall) MODE="uninstall"; shift ;;
    --dry-run) DRY_RUN=1; shift ;;
    *) echo "ENCO installer: unknown argument '$1'" >&2; exit 2 ;;
  esac
done

if [ "$(id -u)" != "0" ]; then
  echo "ENCO installer: run as root (prefix with sudo)" >&2
  exit 2
fi

# --- full removal: everything ENCO ever put on this controller, nothing else ---
if [ "$MODE" = "uninstall" ]; then
  echo "ENCO uninstaller: removing everything ENCO from this controller…"
  systemctl stop enco-gateway enco-go2rtc 2>/dev/null || true
  systemctl disable enco-gateway enco-go2rtc 2>/dev/null || true
  rm -f /etc/systemd/system/enco-gateway.service /etc/systemd/system/enco-go2rtc.service
  rm -rf /opt/enco-gateway /root/enco-backups /tmp/enco-install
  systemctl daemon-reload
  echo "ENCO uninstaller: done. Removed:"
  echo "  - services enco-gateway, enco-go2rtc"
  echo "  - /opt/enco-gateway (gateway, config, access registry, static UI, go2rtc)"
  echo "  - /root/enco-backups"
  echo "Wiren Board's own services (mosquitto, wb-rules, …) were not touched."
  exit 0
fi
if ! command -v python3 >/dev/null 2>&1; then
  echo "ENCO installer: python3 is required on the controller" >&2
  exit 41
fi

API_BASE="${API_BASE%/}"

# --- обновление: меняется только код, дом остаётся на месте --------------------
#
# Отдельный режим существует потому, что --replace ставит шлюз заново и кладёт
# конфигурацию из облака поверх местной: без токена это пустой дом. Обновление
# не имеет права трогать config.json, cloud.json и clients.json, поэтому вся
# работа уходит в updater.py — он же обеспечивает проверку и автооткат.
#
# Скрипт обновления берётся из свежего бандла, а не с диска: так обновляется и
# контроллер, на котором стоит версия старше самого механизма обновления.
# --- показать копии / вернуться к копии -------------------------------------
#
# Копии создавались всегда — при установке, при замене, перед каждым
# обновлением, — а вернуться из продукта было нельзя: только разбирать архив
# руками, что в поле означает «никак». Восстановление возвращает и конфигурацию
# дома: в этом его отличие от обновления и весь его смысл.
if [ "$MODE" = "backups" ] || [ "$MODE" = "restore" ]; then
  if [ ! -x /opt/enco-gateway/gateway_runtime/updater.py ] && [ ! -f /opt/enco-gateway/gateway_runtime/updater.py ]; then
    echo "ENCO: на этом контроллере нет ENCO Gateway." >&2
    exit 18
  fi
  RESTORE_PY=/opt/enco-gateway/venv/bin/python
  [ -x "$RESTORE_PY" ] || RESTORE_PY="$(command -v python3)"
  mkdir -p /tmp/enco-update
  cp /opt/enco-gateway/gateway_runtime/updater.py /tmp/enco-update/updater.py
  if [ "$MODE" = "backups" ]; then
    "$RESTORE_PY" /tmp/enco-update/updater.py --backups --dir /opt/enco-gateway
    exit 0
  fi
  echo "ENCO: восстановление состояния из резервной копии…"
  RESTORE_RC=0
  if [ -n "$RESTORE_NAME" ]; then
    "$RESTORE_PY" /tmp/enco-update/updater.py --restore --backup "$RESTORE_NAME" \
      --dir /opt/enco-gateway --source terminal || RESTORE_RC=$?
  else
    "$RESTORE_PY" /tmp/enco-update/updater.py --restore \
      --dir /opt/enco-gateway --source terminal || RESTORE_RC=$?
  fi
  if [ -s /opt/enco-gateway/update-status.json ]; then
    "$RESTORE_PY" -c "import json;d=json.load(open('/opt/enco-gateway/update-status.json',encoding='utf-8'));print('');print('  '+(d.get('message') or ''));print('')"
  fi
  exit $RESTORE_RC
fi

if [ "$MODE" = "update" ]; then
  if [ ! -d /opt/enco-gateway ]; then
    echo "ENCO: на этом контроллере нет ENCO Gateway — обновлять нечего." >&2
    echo "  Установить:  curl -fsSL $API_BASE/install.sh | sudo sh" >&2
    exit 18
  fi
  echo "ENCO: обновление шлюза (конфигурация дома не затрагивается)…"
  mkdir -p /tmp/enco-update
  python3 - "$API_BASE" <<'ENCO_FETCH_UPDATER'
import json, sys, urllib.request

api = sys.argv[1]
try:
    req = urllib.request.Request(api + "/install/bundle", headers={"User-Agent": "enco-install.sh"})
    with urllib.request.urlopen(req, timeout=60) as resp:
        payload = json.loads(resp.read().decode("utf-8"))
except Exception as exc:  # noqa: BLE001
    sys.stderr.write("ENCO: не удалось получить обновление: %s\n" % exc)
    sys.exit(43)

script = (payload.get("gateway_files") or {}).get("gateway_runtime/updater.py")
if not script:
    sys.stderr.write("ENCO: облако отдало сборку без модуля обновления\n")
    sys.exit(44)
with open("/tmp/enco-update/updater.py", "w", encoding="utf-8") as fh:
    fh.write(script)
print(payload.get("version") or "unknown")
ENCO_FETCH_UPDATER

  UPDATE_PY=/opt/enco-gateway/venv/bin/python
  [ -x "$UPDATE_PY" ] || UPDATE_PY="$(command -v python3)"
  # `set -e` не должен оборвать нас на неудачном обновлении: откат уже случился
  # внутри, и отчёт ниже — единственное, ради чего человек это запускал.
  UPDATE_RC=0
  "$UPDATE_PY" /tmp/enco-update/updater.py --apply --dir /opt/enco-gateway --source terminal || UPDATE_RC=$?
  if [ -s /opt/enco-gateway/update-status.json ]; then
    "$UPDATE_PY" - <<'ENCO_REPORT'
import json
try:
    data = json.load(open("/opt/enco-gateway/update-status.json", encoding="utf-8"))
except Exception:
    raise SystemExit(0)
print("")
print("  %s" % (data.get("message") or ""))
to = data.get("to") or {}
if data.get("state") == "done" and to:
    print("  Версия: %s (%s)" % (to.get("version"), to.get("revision")))
if data.get("rolledBack"):
    print("  Прошлая версия возвращена, дом продолжает работать.")
print("")
ENCO_REPORT
  fi
  exit $UPDATE_RC
fi
echo "ENCO Gateway installer → fetching bundle from ${API_BASE}"

# --- замена без токена стирает дом: без явного согласия не начинаем ------------
#
# --replace ставит шлюз заново и кладёт конфигурацию из пакета поверх местной.
# С токеном облако присылает зеркало дома, и он выживает; без токена приходит
# ПУСТОЙ дом — комнаты, устройства, сцены и доступы будут стёрты. Это законная
# операция «начать с чистого листа», но одна перепутанная буква в аргументе не
# должна стоить дома, поэтому спрашиваем прямо. Для скриптов есть --yes-wipe-home.
if [ "$MODE" = "replace_after_backup" ] && [ -z "$TOKEN" ] && [ "$WIPE_CONFIRMED" != "1" ] && [ "$DRY_RUN" = "0" ]; then
  if [ -r /dev/tty ]; then
    echo ""
    echo "  ВНИМАНИЕ: замена без токена ставит ПУСТОЙ дом."
    echo "  Комнаты, устройства, сцены и доступы на этом контроллере будут стёрты"
    echo "  (резервная копия сохранится в /root/enco-backups)."
    echo ""
    echo "  Обновить код, НЕ трогая дом:   ... | sudo sh -s -- --update"
    echo "  Вернуть дом из копии:          ... | sudo sh -s -- --restore"
    echo ""
    printf "  Стереть дом и поставить заново? Введи слово ЗАМЕНИТЬ: "
    read -r ANSWER < /dev/tty || ANSWER=""
    if [ "$ANSWER" != "ЗАМЕНИТЬ" ] && [ "$ANSWER" != "ZAMENIT" ]; then
      echo "  Отменено — на контроллере ничего не изменилось."
      exit 3
    fi
  else
    echo "ENCO: --replace без токена стирает дом. Терминала для подтверждения нет —" >&2
    echo "  добавь --yes-wipe-home, если это осознанный чистый старт," >&2
    echo "  или используй --update (код без конфигурации) / --restore (вернуть из копии)." >&2
    exit 3
  fi
fi

# --- existing-install guard (mirrors the Agent's install_new / replace rules) ---
HAS_GATEWAY=0
if [ -s /opt/enco-gateway/cloud.json ] \
  || [ -e /etc/systemd/system/enco-gateway.service ] \
  || systemctl list-unit-files 2>/dev/null | grep -q '^enco-gateway\.service'; then
  HAS_GATEWAY=1
fi
if [ "$MODE" = "install_new" ] && [ "$HAS_GATEWAY" = "1" ] && [ "$DRY_RUN" = "0" ]; then
  echo "ENCO installer: this controller already has an ENCO Gateway." >&2
  echo "  Re-run with --replace to back up and replace it, or remove it first." >&2
  exit 17
fi

# --- fetch payload from Cloud and write files (python3 stdlib only) ---
python3 - "$API_BASE" "$TOKEN" <<'ENCO_PY'
import json, os, ssl, sys, urllib.parse, urllib.request

api, token = sys.argv[1], sys.argv[2]
# Без токена — публичный бандл: шлюз ставится «ничей», дом привяжется, когда
# владелец зарегистрируется прямо на контроллере. С токеном — старый путь.
if token:
    url = api + "/install/" + urllib.parse.quote(token, safe="") + "/agent-payload"
else:
    url = api + "/install/bundle"
try:
    req = urllib.request.Request(url, headers={"User-Agent": "enco-install.sh"})
    with urllib.request.urlopen(req, timeout=25) as resp:
        payload = json.loads(resp.read().decode("utf-8"))
except Exception as exc:  # noqa: BLE001
    sys.stderr.write("ENCO installer: cannot fetch Gateway bundle: %s\n" % exc)
    sys.exit(43)

required_keys = ["cloud_json", "config_json", "service_text"]
if token:
    required_keys.append("gateway_id")   # привязанная установка по токену
for key in required_keys:
    if not isinstance(payload.get(key), str) or not payload.get(key):
        sys.stderr.write("ENCO installer: incomplete Cloud payload (missing %s)\n" % key)
        sys.exit(44)

files = payload.get("gateway_files")
if not isinstance(files, dict) or not files:
    sys.stderr.write("ENCO installer: Cloud payload has no gateway_files\n")
    sys.exit(44)

required = {
    "server.py",
    "gateway_runtime/__init__.py",
    "gateway_runtime/settings.py",
    "gateway_runtime/mqtt.py",
    "gateway_runtime/cloud_bridge.py",
    "gateway_runtime/state.py",
    "gateway_runtime/schedules.py",
    "gateway_runtime/gateway.py",
    "gateway_runtime/http_api.py",
    "gateway_runtime/local_auth.py",
    "gateway_runtime/history.py",
    "gateway_runtime/version.py",
    "gateway_runtime/updater.py",
}
missing = sorted(required - set(files))
if missing:
    sys.stderr.write("ENCO installer: Cloud payload is missing files: %s\n" % ", ".join(missing))
    sys.exit(44)

base = "/tmp/enco-install"
os.makedirs(base, exist_ok=True)


def write(rel, content):
    # Path safety: no absolute, no traversal, only server.py or gateway_runtime/*.
    if rel.startswith("/") or "\\" in rel or ".." in rel.split("/"):
        sys.stderr.write("ENCO installer: unsafe path in payload: %s\n" % rel)
        sys.exit(44)
    if rel != "server.py" and not rel.startswith("gateway_runtime/") and not rel.startswith("static/"):
        sys.stderr.write("ENCO installer: unexpected path in payload: %s\n" % rel)
        sys.exit(44)
    target = os.path.join(base, rel)
    os.makedirs(os.path.dirname(target) or base, exist_ok=True)
    with open(target, "w", encoding="utf-8") as fh:
        fh.write(content)


for rel, content in files.items():
    if not isinstance(rel, str) or not isinstance(content, str) or not content:
        sys.stderr.write("ENCO installer: invalid file entry in payload\n")
        sys.exit(44)
    write(rel, content)

for name, key in (("cloud.json", "cloud_json"), ("config.json", "config_json"),
                  ("enco-gateway.service", "service_text")):
    with open(os.path.join(base, name), "w", encoding="utf-8") as fh:
        fh.write(payload[key])

# Чем стал шлюз. Без этой отметки обновление не может ответить «нужно ли
# обновляться» и не знает, куда откатываться.
import time
with open(os.path.join(base, "VERSION"), "w", encoding="utf-8") as fh:
    json.dump({"version": payload.get("version") or "unknown",
               "revision": payload.get("revision") or "unknown",
               "installedAt": int(time.time())}, fh, ensure_ascii=False, indent=2)

print(payload.get("gateway_id") or "unbound")
ENCO_PY

if [ "$DRY_RUN" = "1" ]; then
  echo "ENCO installer: --dry-run — bundle staged at /tmp/enco-install, /opt and systemd untouched:"
  find /tmp/enco-install -type f | sort
  exit 0
fi

echo "ENCO installer: bundle written, installing…"

# --- back up any existing install, lay down files, systemd (mirrors the Agent) ---
mkdir -p /root/enco-backups /opt/enco-gateway
BEFORE_BACKUP="/root/enco-backups/enco-gateway-before-install.tgz"
if [ "$(find /opt/enco-gateway -mindepth 1 -maxdepth 1 2>/dev/null | wc -l)" -gt 0 ]; then
  tar --ignore-failed-read -czf "$BEFORE_BACKUP.partial" -C /opt enco-gateway >/dev/null 2>&1 || true
  if [ -s "$BEFORE_BACKUP.partial" ]; then mv -f "$BEFORE_BACKUP.partial" "$BEFORE_BACKUP"; else rm -f "$BEFORE_BACKUP.partial"; fi
fi

cp /tmp/enco-install/server.py /opt/enco-gateway/server.py
rm -rf /opt/enco-gateway/gateway_runtime
cp -R /tmp/enco-install/gateway_runtime /opt/enco-gateway/gateway_runtime
if [ -d /tmp/enco-install/static ]; then
  rm -rf /opt/enco-gateway/static
  cp -R /tmp/enco-install/static /opt/enco-gateway/static
fi
cp /tmp/enco-install/cloud.json /opt/enco-gateway/cloud.json
cp /tmp/enco-install/config.json /opt/enco-gateway/config.json
cp /tmp/enco-install/VERSION /opt/enco-gateway/VERSION
cp /tmp/enco-install/enco-gateway.service /etc/systemd/system/enco-gateway.service
chmod 600 /opt/enco-gateway/cloud.json

if [ ! -x /opt/enco-gateway/venv/bin/python ]; then
  python3 -m venv --system-site-packages /opt/enco-gateway/venv || {
    if command -v apt-get >/dev/null 2>&1; then
      apt-get update && apt-get install -y python3 python3-venv python3-pip
      python3 -m venv --system-site-packages /opt/enco-gateway/venv
    else
      echo "ENCO installer: cannot create python venv" >&2
      exit 41
    fi
  }
fi

# paho-mqtt is REQUIRED. Wiren Board ships python3-paho-mqtt as a system package,
# visible to the --system-site-packages venv with no internet. pip is a fallback.
if ! /opt/enco-gateway/venv/bin/python -c "import paho.mqtt.client" >/dev/null 2>&1; then
  /opt/enco-gateway/venv/bin/pip install --upgrade pip >/dev/null 2>&1 || true
  /opt/enco-gateway/venv/bin/pip install paho-mqtt >/dev/null 2>&1 || {
    echo "ENCO installer: paho-mqtt unavailable and no internet to install it" >&2
    exit 42
  }
fi
# websockets is OPTIONAL (HTTP command polling is the fallback); never fail on it.
if ! /opt/enco-gateway/venv/bin/python -c "import websockets" >/dev/null 2>&1; then
  /opt/enco-gateway/venv/bin/pip install websockets >/dev/null 2>&1 || true
fi

# go2rtc is OPTIONAL: the media gateway that turns RTSP cameras into browser
# WebRTC (same engine Home Assistant uses). Best-effort: no internet or an
# unknown arch must never break the Gateway install. MJPEG/HLS cameras work
# without it; the Gateway writes /opt/enco-gateway/go2rtc.yaml either way.
if [ ! -x /opt/enco-gateway/go2rtc ]; then
  GO2RTC_ARCH=""
  case "$(uname -m)" in
    armv7l|armv6l) GO2RTC_ARCH="arm" ;;
    aarch64) GO2RTC_ARCH="arm64" ;;
    x86_64) GO2RTC_ARCH="amd64" ;;
  esac
  if [ -n "$GO2RTC_ARCH" ]; then
    echo "ENCO installer: fetching go2rtc ($GO2RTC_ARCH) for RTSP cameras…"
    curl -fsSL --max-time 120 -o /opt/enco-gateway/go2rtc.new \
      "https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_linux_${GO2RTC_ARCH}" \
      && chmod +x /opt/enco-gateway/go2rtc.new \
      && mv -f /opt/enco-gateway/go2rtc.new /opt/enco-gateway/go2rtc \
      || { rm -f /opt/enco-gateway/go2rtc.new; echo "ENCO installer: go2rtc skipped (no internet?) — RTSP cameras will wait" >&2; }
  fi
fi
if [ -x /opt/enco-gateway/go2rtc ]; then
  [ -f /opt/enco-gateway/go2rtc.yaml ] || printf 'api:\n  listen: ":1984"\nwebrtc:\n  listen: ":8555"\nstreams:\n  {}\n' > /opt/enco-gateway/go2rtc.yaml
  cat > /etc/systemd/system/enco-go2rtc.service <<'GO2RTC_UNIT'
[Unit]
Description=ENCO go2rtc media gateway (RTSP cameras to browser)
After=network-online.target

[Service]
ExecStart=/opt/enco-gateway/go2rtc -config /opt/enco-gateway/go2rtc.yaml
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
GO2RTC_UNIT
  systemctl daemon-reload
  systemctl enable enco-go2rtc >/dev/null 2>&1 || true
  systemctl restart enco-go2rtc >/dev/null 2>&1 || true
fi

systemctl daemon-reload
systemctl enable enco-gateway >/dev/null 2>&1 || true
systemctl restart enco-gateway
sleep 2

if systemctl is-active --quiet enco-gateway; then
  # Адрес в домашней сети: берём тот, с которого контроллер реально ходит наружу.
  # `hostname -I` на Wiren Board первым отдаёт внутренний USB-интерфейс (192.168.42.1).
  LAN_IP="$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}')"
  [ -n "$LAN_IP" ] || LAN_IP="$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -v '^192\.168\.42\.' | head -1)"
  [ -n "$LAN_IP" ] || LAN_IP="<ip-контроллера>"
  echo ""
  echo "  ENCO Gateway установлен и работает."
  echo ""
  echo "  Что дальше:"
  echo "   1. Открой в браузере (телефон или ноут в этой же сети):  http://$LAN_IP:8088/"
  echo "   2. Зарегистрируйся прямо там (почта + пароль) — этот аккаунт станет"
  echo "      владельцем дома. Тем же логином входишь в облако encohub.ru."
  echo "   3. 🔧 Устройства — собери устройства из каналов контроллера."
  echo "   4. 👤 Доступ — пригласи жильцов и гостей по QR или ссылке."
  echo ""
  echo "  Сбросить локальные сессии на контроллере:"
  echo "   sudo /opt/enco-gateway/venv/bin/python /opt/enco-gateway/server.py --reset-access"
  echo ""
else
  echo "ENCO installer: service did not become active — check: journalctl -u enco-gateway -n 50" >&2
  exit 1
fi
