#!/bin/bash
# =============================================================================
#  block-games.sh v2 — блокировка игр, VPN и лишних сайтов на школьных ПК
#  МОС 12 (ОС Московской электронной школы), ALT Linux, РЕД ОС, Astra, ROSA.
#
#  Команды (от root):
#     ./block-games.sh install        включить блокировку и автозапуск
#     ./block-games.sh status         что включено + самопроверка
#     ./block-games.sh add <домен>    добавить сайт в блокировку
#     ./block-games.sh allow <домен>  разрешить сайт (исключение)
#     ./block-games.sh pause          выключить на время (учителю)
#     ./block-games.sh resume         включить обратно
#     ./block-games.sh list           показать списки
#     ./block-games.sh remove         убрать всё и вернуть как было
#     DRY_RUN=1 ./block-games.sh install    только показать план
#
#  Что исправлено против v1:
#   1) убрана фильтрация по словам "play","game","igr","tor","vpn" во всём
#      пакете (--to 65535) — она рубила нормальные сайты (display, player,
#      history, repository). Теперь только область TLS-приветствия (40..512)
#      и только различимые названия брендов от 5 символов;
#   2) записи с путями (vk.com/game) и IP в /etc/hosts больше не пишутся —
#      там они бесполезны; пути блокируются в браузерах, где это работает;
#   3) добавлены групповые политики браузеров: Chromium/Яндекс/Chrome и
#      Firefox — с блокировкой игровых адресов, отключением DoH, инкогнито и
#      расширений. Без этого ученик включал "Безопасный DNS" — и hosts не помогал;
#   4) интерфейс определяется автоматически, а школьный DNS сначала
#      проверяется: если он не отвечает — блокировка DNS не включается и
#      интернет не пропадает (в v1 при жёстком wlp2s0 это ломало сеть);
#   5) глухие "|| true" убраны: скрипт считает, что реально применилось;
#   6) systemd-служба больше не ссылается на /home/admin/... (из-за этого
#      блокировка исчезала после перезагрузки) — скрипт ставится в /usr/local/sbin;
#   7) удаление /etc/hosts возможно только при наличии закрывающей метки,
#      чтобы никогда не стереть файл целиком;
#   8) учителю (пользователю, вызвавшему sudo) разрешён полный доступ,
#      есть pause/resume и allow.
# =============================================================================

set -uo pipefail

# ------------------------------- НАСТРОЙКИ ----------------------------------
SCHOOL_DNS=("10.162.128.13" "10.89.87.21")   # пусто () — не трогать DNS вовсе

BLOCK_MESSENGERS=0        # 1 = блокировать Telegram/Discord (на вашем сайте есть
                          #     кнопка "Прокси для ТГ" — будет конфликт)
BLOCK_YOUTUBE=0           # 1 = блокировать YouTube целиком
BLOCK_SOCIAL=0            # 1 = блокировать TikTok/Instagram и подобное
BLOCK_YANDEX_WHOLE=0      # 1 = блокировать весь yandex.ru (0 = только игры)
BLOCK_REMOTE_DESKTOP=0    # 1 = блокировать TeamViewer/AnyDesk
BLOCK_EXTENSION_STORES=0  # 1 = блокировать магазины расширений
BLOCK_SCREENSHOTS=1       # 1 = запретить скриншоты средствами браузера
BLOCK_EXTENSIONS=1        # 1 = запретить установку расширений (иначе VPN)
BLOCK_INCOGNITO=1         # 1 = выключить режим инкогнито
BLOCK_DEVTOOLS=0          # 1 = запретить F12
ALLOW_ADMIN_BYPASS=1      # 1 = учителю (кто вызвал sudo) всё разрешено
ALLOW_ROOT_BYPASS=1       # 1 = root тоже не ограничивать
WITH_BUTTON=1             # 1 = сделать кнопку «Блокиратор игр» (меню + рабочий стол)

# ------------------------------- СЛУЖЕБНОЕ ----------------------------------
SCRIPT_VERSION="v2"
BG_ROOT="${BG_ROOT:-}"                      # служебное: префикс пути для проверок
STATE_DIR="$BG_ROOT/etc/block-games"
HOSTS_FILE="$BG_ROOT/etc/hosts"
LOG_FILE="$BG_ROOT/var/log/block-games.log"
BACKUP_DIR="$BG_ROOT/var/backups/block-games"
SERVICE_NAME="block-games.service"
SERVICE_FILE="$BG_ROOT/etc/systemd/system/$SERVICE_NAME"
INSTALL_PATH="$BG_ROOT/usr/local/sbin/block-games"
CHAIN="BLOCK_GAMES"
HOSTS_BEGIN="# >>> block-games >>>"
HOSTS_END="# <<< block-games <<<"
CUSTOM_FILE="$STATE_DIR/custom-domains.txt"
ALLOW_FILE="$STATE_DIR/allow-domains.txt"
STATE_FILE="$STATE_DIR/state"
CREATED_LIST="$STATE_DIR/written-files.txt"
POLICY_NAME="99-block-games.json"

DRY_RUN="${DRY_RUN:-0}"
OK_COUNT=0
SKIP_COUNT=0
WARN_COUNT=0

CHROMIUM_DIRS=(
  "/etc/opt/yandex/browser/policies/managed"
  "/etc/yandex-browser/policies/managed"
  "/etc/chromium/policies/managed"
  "/etc/chromium-browser/policies/managed"
  "/etc/opt/chrome/policies/managed"
)
CHROMIUM_BINS=(yandex-browser yandex-browser-beta chromium chromium-browser google-chrome google-chrome-stable)
FIREFOX_FILES=(
  "/etc/firefox/policies/policies.json"
  "/usr/lib/firefox/distribution/policies.json"
  "/usr/lib64/firefox/distribution/policies.json"
)

# --------------------------- СПИСКИ БЛОКИРОВКИ -------------------------------
GAME_DOMAINS=(
  games.yandex.ru games-sdk.yandex.ru sse.games.yandex.ru igrotok.yandex.ru
  games.s3.yandex.net static.games.yandex.net
  y8.com ru.y8.com y8y8.com crazygames.com crazygames.ru poki.com
  igry.ru 1games.ru igroutka.ru igroutka.net vseigru.net vseigru.com
  game-game.ru game-game.com gamer.ru playhop.com gamesgames.com
  gamejolt.com kongregate.com armorgames.com miniclip.com agame.com
  spielaffe.de spelletjes.nl games.mail.ru
  agar.io slither.io wormax.io diep.io hole-io.com paper-io.com
  surviv.io zombs.io zombsroyale.io moomoo.io deeeep.io mope.io
  shellshock.io arras.io krunker.io tanki.io 1v1.lol
  roblox.com rbxcdn.com robloxlabs.com
  minecraft.net minecraftforum.net planetminecraft.com minecraftservers.org
  steamcommunity.com store.steampowered.com steampowered.com
  epicgames.com epicgames.net gog.com origin.com ea.com eaplay.com
  ubisoft.com uplay.com rockstargames.com socialclub.rockstargames.com
  battlenet.net blizzard.com overwatch.com hearthstone.com diablo3.com
  valorant.com leagueoflegends.com fortnite.com fortnitetracker.com
  dota2.com dotabuff.com worldoftanks.ru wargaming.net
  bluestacks.com noxplayer.com gameloop.com geforcenow.com xbox.com
  playstation.com nintendo.com
  online-games.ru flashplayer.ru igri.ru igraem.ru mestogame.ru igray.ru
  igranium.ru igromania.ru stopgame.ru kanobu.ru playground.ru
  game01.ru game02.ru karusel-tv.ru vkplay.ru
  igra1.ru igra2.ru igra3.ru gamesfree.ru gamefree.ru
  gameboss.ru gamespirit.ru gamearena.ru gamezone.ru
  multoigri.ru superigri.ru milligri.ru igri-online.ru igri-besplatno.ru
  best-games.ru top-igri.ru
)
GAME_URL_PATTERNS=(
  "yandex.ru/games" "yandex.ru/*games*" "yandex.com/games" "games.yandex.ru"
  "vk.com/games" "vk.com/app*" "vkplay.ru" "ok.ru/games" "ok.ru/app*"
  "mail.ru/games" "my.mail.ru/*games*" "youtube.com/gaming"
)
VPN_DOMAINS=(
  vpn.net expressvpn.com nordvpn.com hidemyass.com protonvpn.com proton.me
  ivacy.com cyberghostvpn.com tunnelbear.com hotspotshield.com vyprvpn.com
  privateinternetaccess.com windscribe.com surfshark.com zenmate.com
  psiphon3.com psiphon.ca torproject.org ultrasurf.us ultrasurf.eu
  bestproxy.ru spys.one proxy-list.org hideme.ru foxvpn.com
  vpnbook.com freevpn.me vpngate.net hola.org brovpn.com
  proxysite.com proxybay.org hidemy.name fineproxy.org proxy6.net
)
TORRENT_DOMAINS=(rutracker.org rutor.info thepiratebay.org limetorrents.info utorrent.com)
SOCIAL_DOMAINS=(tiktok.com instagram.com facebook.com twitter.com x.com)
MESSENGER_DOMAINS=(discord.com discord.gg discordapp.net web.telegram.org t.me)
REMOTE_DESKTOP_DOMAINS=(teamviewer.com anydesk.com logmein.com gotomypc.com ammyy.com remotedesktop.google.com)
EXTENSION_STORES=(chrome.google.com addons.mozilla.org)

# SNI-фильтр: только различимые бренды от 5 символов.
# Общих слов ("game","play","igr","tor","vpn") здесь быть не должно!
SNI_FRAGMENTS=(
  roblox krunker crazygames miniclip "poki.com" "slither.io" "agar.io"
  discord steampowered steamcommunity epicgames minecraft wargaming
  igroutka vseigru playhop zombs moomoo deeeep "surviv.io"
  "twitch.tv" rutracker thepiratebay torrent bluestacks gameloop geforcenow
  blizzard battlenet karusel-tv vkplay
)
VPN_PORTS_UDP=(1194 1195 1196 1197 1198 51820 51821 51822 51823 51824 500 4500)
VPN_PORTS_TCP=(1723 1701 1080 3128 9050 9051)
DOH_IPS=(1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 9.9.9.9 149.112.112.112)
DEFAULT_ALLOW=(1619.info)

# =============================== УТИЛИТЫ =====================================
say()  { printf '%s\n' "$*"; }
warn() { WARN_COUNT=$((WARN_COUNT+1)); printf '  ! %s\n' "$*" >&2; }
skip() { SKIP_COUNT=$((SKIP_COUNT+1)); printf '  - %s\n' "$*"; }
good() { OK_COUNT=$((OK_COUNT+1)); printf '  + %s\n' "$*"; }
die()  { printf 'ОШИБКА: %s\n' "$*" >&2; exit 1; }

log() { printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >>"$LOG_FILE" 2>/dev/null || true; }

# выполнить действие с учётом DRY_RUN
run() {
  if [ "$DRY_RUN" = "1" ]; then printf '  [план] %s\n' "$*"; return 0; fi
  "$@"
}

need_root() {
  [ -n "$BG_ROOT" ] && return 0
  [ "$(id -u)" -eq 0 ] || die "запустите с sudo: sudo $0 ${1:-install}"
}

have() { command -v "$1" >/dev/null 2>&1; }

# python3 бывает «пустышкой» (заглушка-алиас) и может подвиснуть — зовём только с таймаутом
have_python() {
  have python3 || return 1
  have timeout || return 1
  [ "$(timeout 3 python3 -c 'print(1)' 2>/dev/null)" = "1" ]
}

# корректный ли JSON в файле (0 — да, 1 — нет, 2 — проверить нечем)
py_json_check() {
  have_python || return 2
  if have timeout; then
    timeout 5 python3 -c 'import json,sys; json.load(open(sys.argv[1]))' "$1" 2>/dev/null
  else
    python3 -c 'import json,sys; json.load(open(sys.argv[1]))' "$1" 2>/dev/null
  fi
}

# Список файлов, которые создали или изменили мы — чтобы remove убрал ровно их
record_file() {   # record_file created|merged <путь>
  [ "$DRY_RUN" = "1" ] && return 0
  mkdir -p "$STATE_DIR" 2>/dev/null
  printf '%s|%s\n' "$1" "$2" >>"$CREATED_LIST" 2>/dev/null || true
}

detect_iface() {
  have ip || return 1
  ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}'
}

# отвечает ли конкретный DNS-сервер (0 — да, 1 — нет, 2 — нечем проверить)
dns_server_answers() {
  local server="$1" name="ya.ru"
  if have dig; then dig +time=2 +tries=1 "@$server" "$name" +short >/dev/null 2>&1 && return 0 || return 1; fi
  if have nslookup; then nslookup "$name" "$server" >/dev/null 2>&1 && return 0 || return 1; fi
  if have host; then host "$name" "$server" >/dev/null 2>&1 && return 0 || return 1; fi
  return 2
}

resolve_local() {   # 0 — имя разрешается, 2 — нечем проверить
  have getent || return 2
  getent hosts "$1" >/dev/null 2>&1 && return 0 || return 1
}

backup_file() {
  local f="$1" dest
  [ -f "$f" ] || return 0
  if [ "$DRY_RUN" = "1" ]; then printf '  [план] копия %s\n' "$f"; return 0; fi
  mkdir -p "$BACKUP_DIR"
  dest="$BACKUP_DIR/$(basename "$f").$(date '+%Y%m%d_%H%M%S')"
  cp -p "$f" "$dest" 2>/dev/null && good "резервная копия: $dest"
  ls -1t "$BACKUP_DIR" 2>/dev/null | tail -n +21 | while read -r old; do rm -f "$BACKUP_DIR/$old"; done
}

is_valid_domain() {
  case "$1" in
    ""|*"/"*|*" "*) return 1 ;;
    *[!a-zA-Z0-9._-]*) return 1 ;;
    [0-9]*.[0-9]*.[0-9]*.[0-9]*) return 1 ;;
  esac
  case "$1" in *.*) return 0 ;; *) return 1 ;; esac
}

domains_from_file() {
  local f="$1"
  [ -f "$f" ] || return 0
  tr -d '\r' <"$f" | sed -e 's/#.*//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' |
    while IFS= read -r line; do [ -n "$line" ] && printf '%s\n' "$line"; done
}

is_allowed() {
  local d="$1" a
  for a in $(domains_from_file "$ALLOW_FILE"); do
    [ "$d" = "$a" ] && return 0
    case "$d" in *".$a") return 0 ;; esac
  done
  return 1
}

build_block_domains() {
  local list=("${GAME_DOMAINS[@]}" "${VPN_DOMAINS[@]}" "${TORRENT_DOMAINS[@]}")
  [ "$BLOCK_MESSENGERS" = "1" ] && list+=("${MESSENGER_DOMAINS[@]}")
  [ "$BLOCK_SOCIAL" = "1" ] && list+=("${SOCIAL_DOMAINS[@]}")
  [ "$BLOCK_YOUTUBE" = "1" ] && list+=(youtube.com youtu.be googlevideo.com)
  [ "$BLOCK_YANDEX_WHOLE" = "1" ] && list+=(yandex.ru yandex.net yastatic.net)
  [ "$BLOCK_REMOTE_DESKTOP" = "1" ] && list+=("${REMOTE_DESKTOP_DOMAINS[@]}")
  [ "$BLOCK_EXTENSION_STORES" = "1" ] && list+=("${EXTENSION_STORES[@]}")
  list+=($(domains_from_file "$CUSTOM_FILE"))
  printf '%s\n' "${list[@]}" | while IFS= read -r d; do
    is_valid_domain "$d" || continue
    is_allowed "$d" && continue
    printf '%s\n' "$d"
  done | sort -u
}

# ============================ /etc/hosts =====================================
hosts_block_present() { grep -qF "$HOSTS_BEGIN" "$HOSTS_FILE" 2>/dev/null; }

hosts_remove_block() {
  local tmp
  [ -f "$HOSTS_FILE" ] || return 0
  hosts_block_present || return 0
  if ! grep -qF "$HOSTS_END" "$HOSTS_FILE"; then
    warn "в $HOSTS_FILE нет закрывающей метки — блок не удаляю, чтобы не стереть файл"
    return 1
  fi
  backup_file "$HOSTS_FILE"
  tmp="$(mktemp)"
  if ! awk -v b="$HOSTS_BEGIN" -v e="$HOSTS_END" '
        $0 == b {inside=1; next}
        $0 == e {inside=0; next}
        !inside {print}' "$HOSTS_FILE" >"$tmp"; then
    rm -f "$tmp"; warn "не удалось обработать $HOSTS_FILE"; return 1
  fi
  run cp "$tmp" "$HOSTS_FILE" && good "старый блок вычищен из /etc/hosts"
  rm -f "$tmp"
}

hosts_write_block() {
  local domains count tmp
  domains="$(build_block_domains)"
  count="$(printf '%s\n' "$domains" | grep -c . )"
  if [ "$count" -eq 0 ]; then skip "список доменов пуст — /etc/hosts не меняю"; return 0; fi
  hosts_remove_block || true
  backup_file "$HOSTS_FILE"
  [ -f "$HOSTS_FILE" ] || run touch "$HOSTS_FILE"
  tmp="$(mktemp)"
  cat "$HOSTS_FILE" >"$tmp" 2>/dev/null || true
  {
    printf '%s\n' "$HOSTS_BEGIN"
    printf '# обновлено %s (block-games %s)\n' "$(date '+%Y-%m-%d %H:%M')" "$SCRIPT_VERSION"
    printf '%s\n' "$domains" | while IFS= read -r d; do
      [ -n "$d" ] || continue
      printf '0.0.0.0 %s\n' "$d"
      printf '0.0.0.0 www.%s\n' "$d"
    done
    for d in $(domains_from_file "$ALLOW_FILE"); do printf '# разрешено: %s\n' "$d"; done
    printf '%s\n' "$HOSTS_END"
  } >>"$tmp"
  if run cp "$tmp" "$HOSTS_FILE"; then good "в /etc/hosts записано доменов: $count"; fi
  rm -f "$tmp"
}

# ======================= ПОЛИТИКИ БРАУЗЕРОВ (главное) =========================
# Одна строка JSON-массива из файла/набора значений
json_items() {   # json_items [префикс] < список
  local first=1 item
  while IFS= read -r item; do
    [ -n "$item" ] || continue
    [ "$first" = "1" ] || printf ',\n'
    printf '    "%s"' "$(printf '%s' "$item" | sed 's/\\/\\\\/g; s/"/\\"/g')"
    first=0
  done
  printf '\n'
}

chromium_blocklist() {   # домены + пути для URLBlocklist
  { printf '%s\n' "$(build_block_domains)"
    printf '%s\n' "${GAME_URL_PATTERNS[@]}"
    for d in "${GAME_DOMAINS[@]}"; do printf '[*.]%s\n' "$d"; done
    printf '%s\n' "${VPN_DOMAINS[@]}"
  } | sed '/^$/d' | sort -u
}

chromium_json() {
  printf '{\n'
  printf '  "URLBlocklist": [\n'
  chromium_blocklist | json_items
  printf '  ],\n'
  printf '  "URLAllowlist": [\n'
  { printf '%s\n' "${DEFAULT_ALLOW[@]}"; domains_from_file "$ALLOW_FILE"; printf 'file://*\nchrome://*\n'; } | sed '/^$/d' | sort -u | json_items
  printf '  ],\n'
  printf '  "DnsOverHttpsMode": "off"'
  [ "$BLOCK_INCOGNITO" = "1" ] && printf ',\n  "IncognitoModeAvailability": 2'
  [ "$BLOCK_SCREENSHOTS" = "1" ] && printf ',\n  "DisableScreenshots": true'
  [ "$BLOCK_EXTENSIONS" = "1" ] && printf ',\n  "ExtensionInstallBlocklist": ["*"]'
  [ "$BLOCK_DEVTOOLS" = "1" ] && printf ',\n  "DeveloperToolsAvailability": 2'
  printf '\n}\n'
}

chromium_browser_installed() {
  local b
  for b in "${CHROMIUM_BINS[@]}"; do have "$b" && return 0; done
  return 1
}

chromium_write() {
  local dir count=0
  for dir in "${CHROMIUM_DIRS[@]}"; do
    dir="$BG_ROOT$dir"
    if [ ! -d "$dir" ] && ! chromium_browser_installed; then
      skip "не найден Chromium/Яндекс — пропускаю $dir"; continue
    fi
    if run mkdir -p "$dir" 2>/dev/null; then
      chromium_json >"$dir/$POLICY_NAME.tmp" 2>/dev/null || { warn "не собрал политику для $dir"; continue; }
      if py_json_check "$dir/$POLICY_NAME.tmp"; then :
      else
        case $? in
          1) warn "получился некорректный JSON для $dir — не записываю"; rm -f "$dir/$POLICY_NAME.tmp"; continue ;;
          *) : ;;   # проверить нечем — пишем как есть, генератор детерминированный
        esac
      fi
      run mv "$dir/$POLICY_NAME.tmp" "$dir/$POLICY_NAME" && { good "политика Chromium/Яндекс: $dir/$POLICY_NAME"; record_file created "$dir/$POLICY_NAME"; count=$((count+1)); }
    else
      warn "нет доступа к $dir"
    fi
  done
  [ "$count" -gt 0 ] || skip "политики Chromium записать не удалось (браузер не установлен?)"
}

firefox_json() {
  printf '{\n  "policies": {\n'
  printf '    "WebsiteFilter": {\n      "Block": [\n'
  { browser_filter_patterns; } | sed '/^$/d' | sort -u | json_items
  printf '      ],\n      "Exceptions": [\n'
  { for d in "${DEFAULT_ALLOW[@]}" $(domains_from_file "$ALLOW_FILE"); do
      printf '*://%s/*\n*://*.%s/*\n' "$d" "$d"
    done
    printf 'file:///*\n'
  } | sed '/^$/d' | sort -u | json_items
  printf '      ]\n    },\n'
  printf '    "DNSOverHTTPS": { "Enabled": false }'
  [ "$BLOCK_INCOGNITO" = "1" ] && printf ',\n    "DisablePrivateBrowsing": true'
  [ "$BLOCK_SCREENSHOTS" = "1" ] && printf ',\n    "DisableFirefoxScreenshots": true'
  [ "$BLOCK_EXTENSIONS" = "1" ] && printf ',\n    "ExtensionSettings": { "*": { "installation_mode": "blocked" } }'
  [ "$BLOCK_DEVTOOLS" = "1" ] && printf ',\n    "DisableDeveloperTools": true'
  printf '\n  }\n}\n'
}

browser_filter_patterns() {
  { printf '%s\n' "$(build_block_domains)"
    printf '%s\n' "${GAME_URL_PATTERNS[@]}"
  }
}

firefox_write() {
  local f dir written=0
  for f in "${FIREFOX_FILES[@]}"; do
    f="$BG_ROOT$f"
    dir="$(dirname "$f")"
    if [ ! -d "$dir" ]; then skip "нет каталога $dir — пропускаю"; continue; fi
    if [ -s "$f" ]; then
      # файл уже есть: объединяем, чужие политики не теряем
      backup_file "$f"
      if have_python; then
        firefox_json >"$f.new" 2>/dev/null
        if python3 - "$f" "$f.new" <<'PY' 2>/dev/null
import json, sys
old = json.load(open(sys.argv[1], encoding='utf-8'))
new = json.load(open(sys.argv[2], encoding='utf-8'))
pol = old.setdefault('policies', {})
pol.update(new.get('policies', {}))
json.dump(old, open(sys.argv[1], 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
PY
        then good "политики Firefox объединены с существующими: $f"; record_file merged "$f"
        else warn "не удалось объединить политики в $f — файл не тронут"; rm -f "$f.new"; continue; fi
        rm -f "$f.new"
      else
        warn "$f уже занят другими политиками, а python3 нет — не трогаю (копия сохранена)"
        continue
      fi
    else
      firefox_json >"$f.new" 2>/dev/null && run mv "$f.new" "$f" && { record_file created "$f"; good "политики Firefox: $f"; }
    fi
    written=$((written+1))
  done
  [ "$written" -gt 0 ] || skip "Firefox не найден — политики Firefox не записаны"
}

# =============================== СЕТЕВОЙ СЛОЙ ================================
iptables_available() { have iptables; }

ipt_rules_count() { iptables -S "$CHAIN" 2>/dev/null | grep -c '^-A' || true; }

iptables_clear() {
  iptables_available || return 0
  iptables -D OUTPUT -j "$CHAIN" 2>/dev/null || true
  iptables -F "$CHAIN" 2>/dev/null || true
  iptables -X "$CHAIN" 2>/dev/null || true
}

# Школьный DNS ставим только если он отвечает. Иначе — не трогаем, чтобы не отключить сеть.
dns_apply() {
  local iface d ans=unknown
  if [ "${#SCHOOL_DNS[@]}" -eq 0 ]; then skip "DNS не трогаю (SCHOOL_DNS пуст)"; return 1; fi
  for d in "${SCHOOL_DNS[@]}"; do
    dns_server_answers "$d"
    case $? in
      0) ans=yes; break ;;
      2) ans=unknown ;;
      1) [ "$ans" = "unknown" ] || ans=no ;;
    esac
  done
  if [ "$ans" = "no" ]; then
    warn "школьный DNS (${SCHOOL_DNS[*]}) не отвечает — DNS не трогаю, чтобы не отключить интернет"
    return 1
  fi
  [ "$ans" = "unknown" ] && warn "проверить школьный DNS нечем (нет dig/nslookup) — применяю на свой риск"
  iface="$(detect_iface || true)"
  if [ -z "$iface" ]; then
    warn "не определил сетевой интерфейс — DNS не меняю"
    return 1
  fi
  if have resolvectl; then
    run resolvectl dns "$iface" "${SCHOOL_DNS[@]}" 2>/dev/null && good "DNS на $iface → ${SCHOOL_DNS[*]}"
    run resolvectl flush-caches 2>/dev/null || true
    return 0
  fi
  skip "resolvectl нет — DNS оставляю как есть (настройте серверы в параметрах сети)"
  return 1
}

iptables_setup() {
  local dns_ok="$1" w p d
  if ! iptables_available; then
    skip "iptables не установлен — сетевой слой пропущен (браузеры и hosts всё равно работают)"
    return 0
  fi
  if [ "$DRY_RUN" = "1" ]; then
    printf '  [план] создать цепочку %s: SNI по брендам, DoT/DoH, VPN-порты\n' "$CHAIN"
    return 0
  fi
  iptables_clear
  iptables -N "$CHAIN" || { warn "не удалось создать цепочку $CHAIN"; return 1; }

  if [ "$ALLOW_ROOT_BYPASS" = "1" ]; then
    iptables -A "$CHAIN" -m owner --uid-owner 0 -j RETURN 2>/dev/null && good "root не ограничен"
  fi
  if [ "$ALLOW_ADMIN_BYPASS" = "1" ] && [ -n "${SUDO_USER:-}" ]; then
    iptables -A "$CHAIN" -m owner --uid-owner "$SUDO_USER" -j RETURN 2>/dev/null \
      && good "учителю ($SUDO_USER) доступ не ограничен"
  fi

  # 1. Разрешённые домены не режем, даже если совпали с фильтром
  for d in "${DEFAULT_ALLOW[@]}" $(domains_from_file "$ALLOW_FILE"); do
    iptables -A "$CHAIN" -p tcp --dport 443 -m string --algo bm --string "$d" --from 40 --to 512 -j RETURN 2>/dev/null || true
  done

  # 2. SNI-фильтр — ТОЛЬКО в области TLS-приветствия (байты 40..512), не весь пакет
  for w in "${SNI_FRAGMENTS[@]}"; do
    iptables -A "$CHAIN" -p tcp --dport 443 -m string --algo bm --string "$w" --from 40 --to 512 -j DROP 2>/dev/null || true
    iptables -A "$CHAIN" -p tcp --dport 80 -m string --algo bm --string "$w" --from 0 --to 512 -j DROP 2>/dev/null || true
  done

  # 3. DNS: только школьные серверы (только если они настроены и отвечают)
  if [ "$dns_ok" = "1" ]; then
    for d in "${SCHOOL_DNS[@]}"; do
      iptables -A "$CHAIN" -p udp --dport 53 -d "$d" -j RETURN 2>/dev/null || true
      iptables -A "$CHAIN" -p tcp --dport 53 -d "$d" -j RETURN 2>/dev/null || true
    done
    iptables -A "$CHAIN" -p udp --dport 53 -d 127.0.0.53 -j RETURN 2>/dev/null || true
    iptables -A "$CHAIN" -p udp --dport 53 -j DROP 2>/dev/null || true
    iptables -A "$CHAIN" -p tcp --dport 53 -j DROP 2>/dev/null || true
    good "DNS разрешён только школьным серверам"
  fi

  # 4. DNS-over-TLS и публичные DoH
  iptables -A "$CHAIN" -p tcp --dport 853 -j DROP 2>/dev/null || true
  for p in "${DOH_IPS[@]}"; do
    iptables -A "$CHAIN" -p tcp --dport 443 -d "$p" -j DROP 2>/dev/null || true
  done

  # 5. Порты VPN
  for p in "${VPN_PORTS_UDP[@]}"; do iptables -A "$CHAIN" -p udp --dport "$p" -j DROP 2>/dev/null || true; done
  for p in "${VPN_PORTS_TCP[@]}"; do iptables -A "$CHAIN" -p tcp --dport "$p" -j DROP 2>/dev/null || true; done

  iptables -A "$CHAIN" -j RETURN
  iptables -I OUTPUT 1 -j "$CHAIN" || { warn "не удалось вставить цепочку в OUTPUT"; return 1; }

  local n
  n="$(ipt_rules_count)"
  if [ "${n:-0}" -lt 5 ]; then
    warn "правил применилось подозрительно мало ($n) — возможно, в системе nftables без iptables"
    return 1
  fi
  good "iptables: правил в цепочке $CHAIN — $n"
  return 0
}

# ============================== АВТОЗАПУСК ===================================
systemd_install() {
  if [ -n "$BG_ROOT" ]; then printf '  [план] systemd пропущен (режим проверки)\n'; return 0; fi
  if ! have systemctl; then skip "systemctl нет — автозапуск не настроен"; return 0; fi
  # Защита от запуска вида "curl ... | bash": тогда $0 — это "bash", и копировать его нельзя
  case "$(basename "${0:-bash}")" in
    bash|sh|dash|zsh|-bash|"")
      warn "скрипт запущен не из файла — автозапуск не настраиваю."
      warn "Скачайте файл и запустите его: curl -fsSL -o /tmp/block-games.sh <ссылка> && sudo bash /tmp/block-games.sh install"
      return 1 ;;
  esac
  [ -f "$0" ] && [ -r "$0" ] || { warn "файл скрипта $0 недоступен — автозапуск не настраиваю"; return 1; }
  run install -m 0755 "$0" "$INSTALL_PATH" || { warn "не удалось скопировать скрипт в $INSTALL_PATH"; return 1; }
  run mkdir -p "$(dirname "$SERVICE_FILE")"
  if [ "$DRY_RUN" = "1" ]; then printf '  [план] создать %s\n' "$SERVICE_FILE"; return 0; fi
  cat >"$SERVICE_FILE" <<UNIT
[Unit]
Description=block-games: блокировка игр и VPN
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=$INSTALL_PATH boot
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
UNIT
  run systemctl daemon-reload 2>/dev/null
  run systemctl enable "$SERVICE_NAME" >/dev/null 2>&1 && good "автозапуск включён ($SERVICE_NAME)"
}

systemd_remove() {
  [ -n "$BG_ROOT" ] && return 0
  have systemctl || return 0
  systemctl disable --now "$SERVICE_NAME" >/dev/null 2>&1 || true
  rm -f "$SERVICE_FILE"
  systemctl daemon-reload 2>/dev/null || true
  rm -f "$INSTALL_PATH"
  good "автозапуск убран"
}

# ============================== САМОПРОВЕРКА =================================
selftest() {
  local allowed blocked_ok=0 blocked_bad=0 code d
  allowed="${DEFAULT_ALLOW[0]}"
  say ""
  say "Проверка:"
  for d in "$allowed"; do
    if have curl; then
      code="$(curl -s -o /dev/null -m 8 -w '%{http_code}' "https://$d" 2>/dev/null || echo 000)"
      case "$code" in
        2*|3*) good "разрешённый сайт $d открывается ($code)" ;;
        *) warn "разрешённый сайт $d НЕ открывается (код $code) — проверьте сеть и DNS" ;;
      esac
    else
      skip "curl нет — проверить доступность сайтов не могу"
    fi
  done
  for d in roblox.com miniclip.com crazygames.com discord.com; do
    if have getent; then
      if getent hosts "$d" 2>/dev/null | grep -q '^0\.0\.0\.0'; then blocked_ok=$((blocked_ok+1)); else blocked_bad=$((blocked_bad+1)); fi
    fi
  done
  if have getent; then
    if [ "$blocked_bad" -eq 0 ]; then good "игровые домены закрыты (проверено $blocked_ok)"
    else warn "не закрыты некоторые игровые домены ($blocked_bad) — возможно, кэш DNS: перезагрузите ПК"; fi
  fi
  say ""
  say "Итог: применено $OK_COUNT, пропущено $SKIP_COUNT, предупреждений $WARN_COUNT"
  if [ "$WARN_COUNT" -gt 0 ]; then
    say "Если интернет пропал — выполните: sudo $0 remove   (потом напишите мне, разберёмся)"
  fi
}

# ========================= КНОПКА «БЛОКИРАТОР ИГР» ===========================
BUTTON_NAME="Блокиратор игр"
ICON_FILE="$BG_ROOT/usr/share/icons/hicolor/scalable/apps/block-games.svg"
APPS_FILE="$BG_ROOT/usr/share/applications/block-games.desktop"
GUI_PATH="$BG_ROOT/usr/local/sbin/block-games-gui"

passwd_file() {
  if [ -n "$BG_ROOT" ] && [ -f "$BG_ROOT/etc/passwd" ]; then printf '%s' "$BG_ROOT/etc/passwd"; else printf '/etc/passwd'; fi
}

button_users() {   # кому положить ярлык на рабочий стол
  { [ -n "${SUDO_USER:-}" ] && printf '%s\n' "$SUDO_USER"
    awk -F: '$3>=1000 && $3<65000 && $6!="" {print $1}' "$(passwd_file)" 2>/dev/null
  } | sort -u
}

home_of() { awk -F: -v u="$1" '$1==u {print $6}' "$(passwd_file)" 2>/dev/null; }

desktop_dir_for() {   # у KDE рабочий стол может быть локализован
  local h="$1" d=""
  if [ -f "$BG_ROOT$h/.config/user-dirs.dirs" ]; then
    d="$(sed -n 's/^XDG_DESKTOP_DIR="\(.*\)"/\1/p' "$BG_ROOT$h/.config/user-dirs.dirs" | head -1)"
    [ -n "$d" ] && d="$(printf '%s' "$d" | sed "s|\$HOME|$h|")"
  fi
  [ -n "${d:-}" ] || d="$h/Desktop"
  printf '%s' "$d"
}

write_icon() {
  run mkdir -p "$(dirname "$ICON_FILE")"
  if [ "$DRY_RUN" = "1" ]; then printf '  [план] иконка %s\n' "$ICON_FILE"; return 0; fi
  cat >"$ICON_FILE" <<'SVG'
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
  <rect x="4" y="10" width="56" height="40" rx="12" fill="#2b3a55"/>
  <circle cx="21" cy="34" r="7" fill="#8ea3c9"/>
  <circle cx="43" cy="30" r="4" fill="#8ea3c9"/>
  <circle cx="45" cy="40" r="4" fill="#8ea3c9"/>
  <rect x="14" y="16" width="36" height="28" rx="8" fill="none" stroke="#cfd8ea" stroke-width="2"/>
  <path d="M10 52 L54 12" stroke="#e5484d" stroke-width="6" stroke-linecap="round"/>
  <circle cx="32" cy="32" r="0" fill="#000"/>
</svg>
SVG
  good "иконка: $ICON_FILE"
}

write_desktop_file() {   # $1 — путь
  local f="$1"
  run mkdir -p "$(dirname "$f")"
  if [ "$DRY_RUN" = "1" ]; then printf '  [план] кнопка %s\n' "$f"; return 0; fi
  cat >"$f" <<EOF
[Desktop Entry]
Type=Application
Version=1.0
Name=$BUTTON_NAME
Comment=Включить или выключить блокировку игр, VPN и лишних сайтов
Exec=$GUI_PATH
Icon=block-games
Terminal=false
Categories=System;Security;Settings;
StartupNotify=true
EOF
  chmod 0644 "$f"
}

write_gui() {
  run mkdir -p "$(dirname "$GUI_PATH")"
  if [ "$DRY_RUN" = "1" ]; then printf '  [план] окно управления %s\n' "$GUI_PATH"; return 0; fi
  cat >"$GUI_PATH" <<'GUI'
#!/bin/bash
# Окно управления «Блокиратор игр» — запускается кнопкой из меню
set -u
SELF="$(readlink -f "$0" 2>/dev/null || echo "$0")"
BIN="/usr/local/sbin/block-games"
[ -x "$BIN" ] || BIN="$(command -v block-games 2>/dev/null || echo "$BIN")"

if [ "$(id -u)" -ne 0 ]; then
  if command -v pkexec >/dev/null 2>&1; then exec pkexec "$SELF"
  elif command -v kdesu >/dev/null 2>&1; then exec kdesu -c "$SELF"
  elif command -v sudo >/dev/null 2>&1; then exec sudo "$SELF"
  else echo "Нужны права администратора. Запустите: sudo $BIN install"; read -r -p 'Enter — выход'; exit 1
  fi
fi

TMP="$(mktemp)"; trap 'rm -f "$TMP"' EXIT

run_and_show() {
  "$BIN" "$1" >"$TMP" 2>&1 || true
  if command -v kdialog >/dev/null 2>&1; then kdialog --title "Блокиратор игр" --textbox "$TMP" 640 480
  elif command -v zenity >/dev/null 2>&1; then zenity --text-info --title="Блокиратор игр" --filename="$TMP" --width=660 --height=500
  else cat "$TMP"; read -r -p 'Enter — выход'
  fi
}

choose() {
  if command -v kdialog >/dev/null 2>&1; then
    kdialog --title "Блокиратор игр" --menu "Что сделать?" \
      enable "Включить блокировку" disable "Выключить на время (пауза)" status "Показать состояние"
  elif command -v zenity >/dev/null 2>&1; then
    zenity --title="Блокиратор игр" --list --text="Что сделать?" --column=Действие --hide-header \
      enable disable status 2>/dev/null
  else
    echo "1 — включить, 2 — выключить, 3 — состояние"
    read -r -p 'Номер: ' n
    case "$n" in 1) echo enable ;; 2) echo disable ;; 3) echo status ;; *) echo "" ;; esac
  fi
}

case "${1:-}" in
  enable) run_and_show install ;;
  disable) run_and_show pause ;;
  status) run_and_show status ;;
  *)
    case "$(choose)" in
      enable) run_and_show install ;;
      disable) run_and_show pause ;;
      status) run_and_show status ;;
      *) exit 0 ;;
    esac ;;
esac
GUI
  chmod 0755 "$GUI_PATH"
  good "окно управления: $GUI_PATH"
}

cmd_button() {
  write_icon
  write_gui
  write_desktop_file "$APPS_FILE" && good "кнопка в меню: $APPS_FILE"
  local u h d f
  for u in $(button_users); do
    h="$(home_of "$u")"
    [ -n "$h" ] && [ -d "$BG_ROOT$h" ] || continue
    d="$(desktop_dir_for "$h")"
    f="$BG_ROOT$d/$BUTTON_NAME.desktop"
    if write_desktop_file "$f"; then
      chmod 0755 "$f" 2>/dev/null || true
      chown "$u" "$f" 2>/dev/null || true
      [ -x "$f" ] || warn "ярлык $f создан, но флаг запуска не встал — на рабочем столе нажмите «Разрешить запуск»"
      good "ярлык на рабочем столе: $d"
    fi
  done
  if have update-desktop-database; then run update-desktop-database "$(dirname "$APPS_FILE")" >/dev/null 2>&1; fi
}

button_remove() {
  local u h d f
  rm -f "$APPS_FILE" "$GUI_PATH" "$ICON_FILE" 2>/dev/null || true
  for u in $(button_users); do
    h="$(home_of "$u")"; [ -n "$h" ] || continue
    d="$(desktop_dir_for "$h")"
    f="$BG_ROOT$d/$BUTTON_NAME.desktop"
    [ -f "$f" ] && rm -f "$f" && good "убран ярлык с рабочего стола: $d"
  done
}

# =============================== КОМАНДЫ =====================================
ensure_state() {
  run mkdir -p "$STATE_DIR"
  [ -f "$ALLOW_FILE" ] || { [ "$DRY_RUN" = "1" ] || printf '%s\n' "${DEFAULT_ALLOW[@]}" >"$ALLOW_FILE"; }
  [ -f "$CUSTOM_FILE" ] || { [ "$DRY_RUN" = "1" ] || : >"$CUSTOM_FILE"; }
}

cmd_install() {
  ensure_state
  say "block-games $SCRIPT_VERSION → включение"
  local dns_ok=0
  [ "$DRY_RUN" = "1" ] || : >"$CREATED_LIST"
  hosts_write_block
  dns_apply && dns_ok=1
  iptables_setup "$dns_ok"
  chromium_write
  firefox_write
  systemd_install
  [ "$WITH_BUTTON" = "1" ] && cmd_button
  if [ "$DRY_RUN" != "1" ]; then run mkdir -p "$STATE_DIR"; printf 'on\n' >"$STATE_FILE"; fi
  log "install: ok=$OK_COUNT skip=$SKIP_COUNT warn=$WARN_COUNT"
  selftest
}

cmd_remove() {
  say "block-games $SCRIPT_VERSION → удаление"
  local dir path kind newest
  iptables_clear
  hosts_remove_block
  for dir in "${CHROMIUM_DIRS[@]}"; do rm -f "$BG_ROOT$dir/$POLICY_NAME" 2>/dev/null && good "убрана политика $dir/$POLICY_NAME"; done
  # удаляем ровно те файлы, что создали мы; объединённые — восстанавливаем из копии
  if [ -f "$CREATED_LIST" ]; then
    while IFS='|' read -r kind path; do
      [ -n "${path:-}" ] || continue
      case "$kind" in
        created) rm -f "$path" && good "удалён наш файл: $path" ;;
        merged)
          newest="$(ls -1t "$BACKUP_DIR/$(basename "$path")".* 2>/dev/null | head -1)"
          if [ -n "$newest" ]; then cp -p "$newest" "$path" && good "восстановлен прежний файл: $path"
          else warn "нет резервной копии для $path — файл оставлен как есть"; fi ;;
      esac
    done <"$CREATED_LIST"
    rm -f "$CREATED_LIST" 2>/dev/null || true
  fi
  systemd_remove
  button_remove
  if have resolvectl; then
    dir="$(detect_iface || true)"
    [ -n "$dir" ] && run resolvectl revert "$dir" >/dev/null 2>&1
    run resolvectl flush-caches >/dev/null 2>&1
  fi
  printf 'off\n' >"$STATE_FILE" 2>/dev/null || true
  log "remove: ok=$OK_COUNT skip=$SKIP_COUNT warn=$WARN_COUNT"
  say "Готово. Списки доменов сохранены в $STATE_DIR (их не удаляю)."
  say "Итог: действий $OK_COUNT, пропущено $SKIP_COUNT, предупреждений $WARN_COUNT"
}

cmd_status() {
  local n
  say "block-games $SCRIPT_VERSION — состояние"
  say "  режим: $([ -f "$STATE_FILE" ] && cat "$STATE_FILE" || echo 'неизвестно')   dry-run: $DRY_RUN"
  if hosts_block_present; then
    n="$(awk -v b="$HOSTS_BEGIN" -v e="$HOSTS_END" '$0==b{i=1;next} $0==e{i=0} i&&/^0\.0\.0\.0/{c++} END{print c+0}' "$HOSTS_FILE")"
    good "/etc/hosts: записей блокировки $n"
  else skip "/etc/hosts: блокировки нет"; fi
  if iptables_available; then
    n="$(ipt_rules_count)"
    [ "${n:-0}" -gt 0 ] && good "iptables: цепочка $CHAIN, правил $n" || skip "iptables: правила не стоят"
  else skip "iptables не установлен"; fi
  for d in "${CHROMIUM_DIRS[@]}"; do [ -f "$BG_ROOT$d/$POLICY_NAME" ] && good "политика браузера: $d/$POLICY_NAME"; done
  for f in "${FIREFOX_FILES[@]}"; do [ -f "$BG_ROOT$f" ] && grep -q '"block-games"\|WebsiteFilter' "$BG_ROOT$f" 2>/dev/null && good "политика Firefox: $f"; done
  [ -f "$SERVICE_FILE" ] && good "автозапуск: $SERVICE_SERVICE" 2>/dev/null
  selftest
}

cmd_add() {
  local d="${1:-}"
  [ -n "$d" ] || die "укажите домен: $0 add example.com"
  is_valid_domain "$d" || die "«$d» не похоже на домен (нужно без http:// и без пути)"
  ensure_state
  printf '%s\n' "$d" >>"$CUSTOM_FILE" 2>/dev/null || true
  good "добавлено в список: $d"
  log "add $d"
  hosts_write_block
  chromium_write
  firefox_write
}

cmd_allow() {
  local d="${1:-}"
  [ -n "$d" ] || die "укажите домен: $0 allow example.com"
  is_valid_domain "$d" || die "«$d» не похоже на домен"
  ensure_state
  printf '%s\n' "$d" >>"$ALLOW_FILE" 2>/dev/null || true
  good "разрешено: $d"
  log "allow $d"
  hosts_write_block
  chromium_write
  firefox_write
}

cmd_list() {
  say "Домены в /etc/hosts (из настроек и custom-domains.txt):"
  build_block_domains | sed 's/^/  /'
  say ""
  say "Разрешено (allow-domains.txt):"
  domains_from_file "$ALLOW_FILE" | sed 's/^/  /'
}

case "${1:-help}" in
  install) cmd_install ;;
  remove|uninstall) cmd_remove ;;
  status) cmd_status ;;
  add) shift; cmd_add "$@" ;;
  allow) shift; cmd_allow "$@" ;;
  list) cmd_list ;;
  button) cmd_button ;;
  pause)
    say "Пауза: снимаю сетевую блокировку (игры в браузере остаются закрыты)"
    iptables_clear; hosts_remove_block
    have systemctl && [ -z "$BG_ROOT" ] && systemctl stop "$SERVICE_NAME" >/dev/null 2>&1 || true
    printf 'paused\n' >"$STATE_FILE" 2>/dev/null || true
    good "интернет без ограничений. Вернуть: sudo $0 resume" ;;
  resume) cmd_install ;;
  boot)
    ensure_state
    dns_apply && dns_ok=1 || dns_ok=0
    hosts_write_block
    iptables_setup "${dns_ok:-0}"
    log "boot"
    ;;
  help|--help|-h|*)
    sed -n '2,20p' "$0" 2>/dev/null || say "запустите: $0 install|status|add|allow|pause|resume|remove|list" ;;
esac
