#!/usr/bin/env bash
# ============================================================
#  soft-repo-manager.sh v1.3 — центр управления личным репозиторием софта Ubuntu
#  v1.3: прогресс-бар закачки: скачано/всего, осталось, скорость, ETA
# ============================================================
set -uo pipefail

# ===================== КОНФИГ =====================
TRACKER_DIR="$HOME/soft-tracker"
OUT="$TRACKER_DIR/out"
REPO="$HOME/ubuntu-repo"
LIST="$TRACKER_DIR/install-list.txt"
NAS_TARGET="${NAS_TARGET:-nikita@192.168.1.150:/volume1/soft/ubuntu-repo}"
REPO_URL="https://a2.njbest.ru/ubuntu-repo/"
CODENAME=$(. /etc/os-release 2>/dev/null && echo "$VERSION_CODENAME")
[ -z "${CODENAME:-}" ] && CODENAME="noble"

NAS_USER="${NAS_TARGET%%@*}"
_REST="${NAS_TARGET#*@}"
NAS_HOST="${_REST%%:*}"
NAS_PATH="${_REST#*:}"

CTRL_SOCK="$TRACKER_DIR/.nas-ctrl"
RSH="ssh -o ControlMaster=auto -o ControlPath=$CTRL_SOCK -o ControlPersist=600 -o ConnectTimeout=8"

nas_remote_rsync() {
  $RSH "$NAS_USER@$NAS_HOST" 'command -v rsync || command -v /usr/syno/bin/rsync || command -v /usr/bin/rsync || true' 2>/dev/null | head -1
}

# ===================== 1) ТРЕКЕР =====================
do_tracker() {
  echo "=== [📊] Отчёт трекера: аудит установленного софта ==="
  mkdir -p "$OUT"
  local SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO=sudo
  $SUDO python3 - "$OUT" <<'PY'
import csv, gzip, json, os, re, subprocess, sys
from datetime import datetime
from pathlib import Path

OUT = Path(sys.argv[1]).expanduser()
OUT.mkdir(parents=True, exist_ok=True)

def run(cmd):
    p = subprocess.run(cmd, shell=True, capture_output=True, text=True, errors='replace')
    return p.stdout

def read_log(path):
    path = Path(path)
    if not path.exists():
        return ''
    if path.suffix == '.gz':
        with gzip.open(path, 'rt', errors='replace') as f:
            return f.read()
    return path.read_text(errors='replace')

records = []

apt_pkgs = set()
files = []
log_dir = Path('/var/log/apt')
if log_dir.exists():
    for f in log_dir.glob('history.log*'):
        m = re.search(r'history\.log(?:\.(\d+))?(?:\.gz)?$', f.name)
        files.append((int(m.group(1)) if m and m.group(1) else 0, f))
for _, f in sorted(files, key=lambda x: -x[0]):
    for block in read_log(f).split('\n\n'):
        sd = re.search(r'Start-Date: ([\d\-]+ [\d:]+)', block)
        if not sd:
            continue
        ts = sd.group(1)
        cmd = re.search(r'Commandline: (.*)', block)
        cmd = cmd.group(1).strip() if cmd else ''
        for field in ('Install', 'Upgrade'):
            m = re.search(field + r': (.*)', block)
            if not m:
                continue
            for name, ver in re.findall(r'([a-z0-9][a-z0-9.+\-]*):[a-z0-9_\-]+ \(([^)]*)\)', m.group(1)):
                records.append({'ts': ts, 'package': name, 'version': ver.split(',')[0],
                                'method': 'apt', 'source': '', 'command': cmd,
                                'action': field.lower()})
                apt_pkgs.add(name)

dpkg_files = []
for f in Path('/var/log').glob('dpkg.log*'):
    m = re.search(r'dpkg\.log(?:\.(\d+))?(?:\.gz)?$', f.name)
    dpkg_files.append((int(m.group(1)) if m and m.group(1) else 0, f))
for _, f in sorted(dpkg_files, key=lambda x: -x[0]):
    for line in read_log(f).splitlines():
        m = re.match(r'([\d\-]+ [\d:]+) install (\S+?):\S+ <none> (\S+)', line)
        if m and m.group(2) not in apt_pkgs:
            records.append({'ts': m.group(1), 'package': m.group(2), 'version': m.group(3),
                            'method': 'dpkg', 'source': '', 'command': 'dpkg -i <локальный .deb>',
                            'action': 'install'})
            apt_pkgs.add(m.group(2))

snap_ver = {}
for line in run('snap list 2>/dev/null').splitlines()[1:]:
    c = line.split()
    if len(c) >= 2:
        snap_ver[c[0]] = c[1]
for line in run('snap changes 2>/dev/null').splitlines()[1:]:
    c = line.split(None, 4)
    if len(c) < 5:
        continue
    m = re.search(r'Install "([^"]+)" snap', c[4])
    if m:
        records.append({'ts': c[2].replace('T', ' ')[:19], 'package': m.group(1),
                        'version': snap_ver.get(m.group(1), '?'), 'method': 'snap',
                        'source': 'snap store (snapcraft.io)',
                        'command': 'snap install ' + m.group(1), 'action': 'install'})

remote_url = {}
for line in run('flatpak remotes --columns=name,url 2>/dev/null').splitlines():
    if '\t' in line:
        n, u = line.split('\t', 1)
        remote_url[n] = u
for line in run('flatpak list --app --columns=application,version,origin 2>/dev/null').splitlines():
    if '\t' not in line:
        continue
    app, ver, origin = (line.split('\t') + [''] * 3)[:3]
    d = Path('/var/lib/flatpak/app') / app
    ts = datetime.fromtimestamp(d.stat().st_mtime).strftime('%Y-%m-%d %H:%M:%S') if d.exists() else '0000-00-00 00:00:00'
    records.append({'ts': ts, 'package': app, 'version': ver or '?', 'method': 'flatpak',
                    'source': remote_url.get(origin, origin), 'command': 'flatpak install ' + app,
                    'action': 'install', 'approx': True})

cache = {}
def source_of(pkg):
    if pkg in cache:
        return cache[pkg]
    out = run('apt-cache policy ' + pkg)
    m = re.search(r'\d+ (https?://\S+|file:\S+)', out)
    if m:
        src = m.group(1)
    elif 'Installed: (none)' not in out:
        src = 'локальный .deb (вне репозиториев)'
    else:
        src = 'неизвестно (пакет удалён)'
    cache[pkg] = src
    return src

for r in records:
    if r['method'] in ('apt', 'dpkg') and not r['source']:
        r['source'] = source_of(r['package'])

records.sort(key=lambda r: r['ts'])
for i, r in enumerate(records, 1):
    r['seq'] = i

host = run('hostname').strip()
codename = run('bash -c ". /etc/os-release && echo $VERSION_CODENAME"').strip()
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

with open(OUT / 'install-history.csv', 'w', newline='', encoding='utf-8-sig') as f:
    w = csv.writer(f, delimiter=';')
    w.writerow(['№', 'Дата', 'Пакет', 'Версия', 'Способ', 'Источник', 'Команда'])
    for r in records:
        w.writerow([r['seq'], r['ts'], r['package'], r['version'], r['method'], r['source'], r['command']])

(OUT / 'install-history.json').write_text(json.dumps(records, ensure_ascii=False, indent=1), encoding='utf-8')

md = ['# Отчёт об установках софта — ' + host, '',
      '- **Создан:** ' + now,
      '- **ОС:** Ubuntu ' + codename,
      '- **Всего записей:** ' + str(len(records)), '',
      '## Сводка по способам', '| Способ | Кол-во |', '|---|---|']
for mth in sorted({r['method'] for r in records}):
    md.append('| %s | %d |' % (mth, sum(1 for r in records if r['method'] == mth)))
md += ['', '## Сводка по серверам-источникам', '| Источник | Кол-во |', '|---|---|']
srcs = {}
for r in records:
    srcs[r['source']] = srcs.get(r['source'], 0) + 1
for s, c in sorted(srcs.items(), key=lambda kv: -kv[1]):
    md.append('| %s | %d |' % (s or '—', c))
md += ['', '## Хронология установок',
       '| № | Дата | Пакет | Версия | Способ | Источник | Команда |',
       '|---|---|---|---|---|---|---|']
for r in records:
    md.append('| %d | %s | %s | %s | %s | %s | %s |' % (
        r['seq'], r['ts'], r['package'], r['version'], r['method'],
        r['source'] or '—', r['command'].replace('|', '\\|')[:90]))
(OUT / 'install-history.md').write_text('\n'.join(md) + '\n', encoding='utf-8')

if os.environ.get('SUDO_UID'):
    uid, gid = int(os.environ['SUDO_UID']), int(os.environ.get('SUDO_GID', 0))
    for p in OUT.iterdir():
        os.chown(p, uid, gid)

print('✅ Отчёты готовы в', OUT)
print('   записей:', len(records))
for p in sorted(OUT.iterdir()):
    print('  -', p.name)
PY
}

# ===================== ПУБЛИКАЦИЯ =====================
do_publish_repo() {
  echo "=== [📤] Публикация репозитория на NAS (аддитивно) ==="
  mkdir -p "$REPO/tools"
  cp -f "$0" "$REPO/tools/soft-repo-manager.sh" 2>/dev/null
  [ -f "$LIST" ] && cp -f "$LIST" "$REPO/tools/install-list.txt" 2>/dev/null
  if ! command -v rsync >/dev/null; then
    echo "⚠️ Локально нет rsync — пробую scp..."
    scp -r -o ControlMaster=auto -o ControlPath="$CTRL_SOCK" -o ControlPersist=600 "$REPO/" "$NAS_TARGET/" \
      && { echo "✅ Опубликовано (scp)"; return 0; }
    echo "❌ scp не смог — запусти пункт 10 (Диагностика)"; return 1
  fi
  local rpath; rpath=$(nas_remote_rsync)
  if [ -z "$rpath" ]; then
    echo "⚠️ На NAS не найден rsync — пробую scp..."
    scp -r -o ControlMaster=auto -o ControlPath="$CTRL_SOCK" -o ControlPersist=600 "$REPO/" "$NAS_TARGET/" \
      && { echo "✅ Опубликовано (scp)"; return 0; }
    echo "❌ Не удалось — запусти пункт 10 (Диагностика)"; return 1
  fi
  echo "-- rsync на NAS: $rpath"
  if rsync -av -e "$RSH" --rsync-path="$rpath" "$REPO/" "$NAS_TARGET/"; then
    echo "✅ Опубликовано на NAS (включая tools/ для новых машин)"
    return 0
  fi
  echo "❌ Ошибка публикации — запусти пункт 10 (Диагностика) и пришли лог"
  return 1
}

# ===================== 3) БИЛДЕР =====================
do_builder() {
  echo "=== [📦] Обновление apt-репозитория ==="
  local JSON="$OUT/install-history.json"
  if [ ! -f "$JSON" ]; then echo "⚠️ Отчёта нет — сначала запускаю трекер..."; do_tracker; fi
  mkdir -p "$REPO/pool" "$REPO/dists/$CODENAME/main/binary-amd64"

  local rpath; rpath=$(nas_remote_rsync)
  if [ -n "$rpath" ]; then
    rsync -a -e "$RSH" --rsync-path="$rpath" "$NAS_TARGET/pool/" "$REPO/pool/" 2>/dev/null \
      || echo "⚠️ Не удалось подтянуть pool с NAS — собираю из локального"
  else
    echo "⚠️ NAS недоступен или там нет rsync — собираю из локального pool"
  fi

  local BASE=/tmp/base_pkgs.txt
  if [ -f /var/log/installer/initial-status.gz ]; then
    zcat /var/log/installer/initial-status.gz | grep '^Package:' | awk '{print $2}' | sort -u > "$BASE"
    echo "Базовых пакетов ОС: $(wc -l < "$BASE")"
  else
    : > "$BASE"; echo "⚠️ initial-status.gz не найден — фильтр базы отключён"
  fi

  python3 - "$JSON" <<'PY' | sort -u > /tmp/report.txt
import json, sys
seen = []
for r in json.load(open(sys.argv[1])):
    if r['method'] in ('apt', 'dpkg') and r['package'] not in seen:
        seen.append(r['package'])
print('\n'.join(seen))
PY
  apt-mark showmanual | sort -u > /tmp/manual.txt
  comm -12 /tmp/report.txt /tmp/manual.txt > "$REPO/mysoft.txt"
  grep -vxFf "$BASE" "$REPO/mysoft.txt" > /tmp/set.txt || true
  echo "Твоего софта (manual, вне базы): $(wc -l < /tmp/set.txt)"

  echo "Строю замыкание зависимостей..."
  apt-cache depends --recurse --no-recommends --no-suggests --no-conflicts \
    --no-breaks --no-replaces --no-enhances $(cat /tmp/set.txt) 2>/dev/null \
    | grep -E '^[a-z0-9][a-z0-9.+-]*$' | sort -u > /tmp/closure_all.txt
  grep -vxFf "$BASE" /tmp/closure_all.txt > "$REPO/closure.txt" || true
  echo "К обработке: $(wc -l < "$REPO/closure.txt") пакетов"

  # ---- 4) скачивание с дедупом + прогресс (размер/скорость/ETA) ----
  cd "$REPO/pool" || return 1
  : > ../missing.txt

  echo "Считаю объёмы пакетов..."
  declare -A PVER PSIZE HAVE
  local f
  for f in *.deb; do [ -e "$f" ] && HAVE["${f%_*}"]=1; done
  while read -r p v; do [ -n "$p" ] && PVER["$p"]="$v"; done < <(
    apt-cache policy $(cat ../closure.txt) 2>/dev/null \
      | awk '/^[^ :]+:$/ {p=$1; sub(/:$/,"",p)} /Candidate:/ {print p, $2}'
  )
  while read -r p s; do [ -n "$p" ] && PSIZE["$p"]="$s"; done < <(
    apt-cache show $(cat ../closure.txt) 2>/dev/null \
      | awk '/^Package: /{p=$2} /^Size: /{if (!(p in seen)) {seen[p]=1; print p, $2}}'
  )

  local TOTAL TODO=0 TODOBYTES=0 DONEBYTES=0 SKIP=0 PREMISS=0
  declare -a DLPKG DLSIZE
  TOTAL=$(wc -l < ../closure.txt)
  while read -r p; do
    local ver="${PVER[$p]:-}"
    if [ -z "$ver" ] || [ "$ver" = "(none)" ]; then echo "$p" >> ../missing.txt; PREMISS=$((PREMISS+1)); continue; fi
    if [ -n "${HAVE[${p}_${ver}]:-}" ]; then SKIP=$((SKIP+1)); continue; fi
    DLPKG+=("$p"); DLSIZE+=("${PSIZE[$p]:-0}")
    TODOBYTES=$((TODOBYTES + ${PSIZE[$p]:-0}))
  done < ../closure.txt
  TODO=${#DLPKG[@]}
  echo "Уже в pool: $SKIP; к скачиванию: $TODO пакетов, $(numfmt --to=iec $TODOBYTES)B; недоступно: $PREMISS"
  [ "$TODO" -eq 0 ] && { echo "Скачивать нечего — переходу к индексам"; }

  local START=$SECONDS i p sz el speed rem eta
  for ((i=0; i<TODO; i++)); do
    p="${DLPKG[i]}"
    apt-get download "$p" >/dev/null 2>&1 || echo "$p" >> ../missing.txt
    sz=$(stat -c %s "${p}_${DLPKG[i]:+${PVER[$p]}}"_*.deb 2>/dev/null | head -n1)
    [ -z "$sz" ] && sz=0
    DONEBYTES=$((DONEBYTES + sz))
    el=$((SECONDS - START)); [ $el -lt 1 ] && el=1
    speed=$((DONEBYTES / el))
    rem=$((TODOBYTES - DONEBYTES)); [ $rem -lt 0 ] && rem=0
    if [ $speed -gt 0 ]; then eta=$((rem / speed)); else eta=0; fi
    printf '\r[%d/%d] %s | %sB/%sB (%d%%) | осталось %sB | %sB/s | ETA %s   ' \
      "$((i+1))" "$TODO" "$p" \
      "$(numfmt --to=iec $DONEBYTES)" "$(numfmt --to=iec $TODOBYTES)" \
      "$(( TODOBYTES>0 ? DONEBYTES*100/TODOBYTES : 100 ))" \
      "$(numfmt --to=iec $rem)" \
      "$(numfmt --to=iec $speed)" "$(date -u -d @$eta +%H:%M:%S)"
  done
  [ "$TODO" -gt 0 ] && echo
  echo "Уже в pool: $SKIP; докачано: $TODO; недоступно: $(wc -l < ../missing.txt)"

  # ---- 5) индексы ----
  cd "$REPO" || return 1
  apt-ftparchive packages pool/ > "dists/$CODENAME/main/binary-amd64/Packages"
  gzip -kf9 "dists/$CODENAME/main/binary-amd64/Packages"
  apt-ftparchive \
    -o APT::FTPArchive::Release::Origin=Nikita-Repo \
    -o APT::FTPArchive::Release::Label=Nikita-Repo \
    -o APT::FTPArchive::Release::Suite="$CODENAME" \
    -o APT::FTPArchive::Release::Codename="$CODENAME" \
    -o APT::FTPArchive::Release::Components=main \
    release "dists/$CODENAME" > "dists/$CODENAME/Release"

  python3 - "$JSON" > "$REPO/manifest.txt" <<'PY'
import json, sys
for r in json.load(open(sys.argv[1])):
    if r['method'] in ('apt', 'dpkg'):
        print(f"{r['seq']}\t{r['package']}\t{r['version']}\t{r['source']}")
PY

  do_publish_repo
}

# ===================== 4) OFFLINE SNAP/FLATPAK =====================
do_offline() {
  echo "=== [💾] Офлайн-копии snap/flatpak (дедуп по версии) ==="
  local O="$REPO/offline"
  mkdir -p "$O/snaps" "$O/flats"
  : > "$O/missing-offline.txt"
  echo "-- snaps --"
  snap list 2>/dev/null | tail -n +2 | while read -r s ver rest; do
    [ -z "$s" ] && continue
    if ls "$O/snaps/" 2>/dev/null | grep -q "^${s}_${ver}_"; then echo "= $s $ver (уже есть)"; continue; fi
    echo "⬇ $s $ver"
    ( cd "$O/snaps" && snap download "$s" ) 2>/dev/null || echo "$s" >> "$O/missing-offline.txt"
  done
  echo "-- flatpaks --"
  flatpak list --app --columns=application,version 2>/dev/null | while IFS=$'\t' read -r app ver; do
    [ -z "$app" ] && continue
    if [ -f "$O/flats/${app}-${ver}.flatpak" ]; then echo "= $app $ver (уже есть)"; continue; fi
    echo "⬇ $app $ver"
    flatpak build-bundle /var/lib/flatpak "$O/flats/${app}-${ver}.flatpak" "$app" || echo "$app" >> "$O/missing-offline.txt"
  done
  local rpath; rpath=$(nas_remote_rsync)
  if [ -n "$rpath" ]; then
    rsync -av -e "$RSH" --rsync-path="$rpath" "$O/" "$NAS_TARGET/offline/" 2>/dev/null \
      || echo "⚠️ offline не ушёл на NAS — повтори публикацию пунктом 5"
  else
    echo "⚠️ NAS недоступен — offline останется локально, опубликуй позже пунктом 5"
  fi
  echo "✅ Офлайн-копии готовы: $O"
}

# ===================== 6) ПРОВЕРКА =====================
do_check() {
  echo "=== [🌐] Проверка доступности репозитория ==="
  local code
  code=$(curl -s -o /dev/null -w '%{http_code}' "${REPO_URL}dists/$CODENAME/Release")
  echo "GET ${REPO_URL}dists/$CODENAME/Release → HTTP $code"
  if [ "$code" = "200" ]; then echo "✅ Репозиторий доступен"; else echo "❌ Недоступен: проверь публикацию (п.5) и nginx на NAS"; fi
}

# ===================== 7) СТАТИСТИКА =====================
do_stats() {
  echo "=== [📈] Статистика ==="
  [ -f "$OUT/install-history.csv" ] && echo "Записей в отчёте:      $(( $(wc -l < "$OUT/install-history.csv") - 1 ))"
  [ -d "$REPO/pool" ] && echo ".deb в pool:           $(ls -1 "$REPO/pool"/*.deb 2>/dev/null | wc -l)"
  [ -f "$REPO/closure.txt" ] && echo "Замыкание:             $(wc -l < "$REPO/closure.txt") пакетов"
  [ -f "$REPO/missing.txt" ] && echo "Недоступно для скачки: $(wc -l < "$REPO/missing.txt") (см. $REPO/missing.txt)"
  [ -d "$REPO/offline/snaps" ] && echo "Копий snap:            $(ls -1 "$REPO/offline/snaps" 2>/dev/null | grep -c '.snap$')"
  [ -d "$REPO/offline/flats" ] && echo "Копий flatpak:         $(ls -1 "$REPO/offline/flats" 2>/dev/null | grep -c '.flatpak$')"
  [ -d "$REPO" ] && echo "Размер репозитория:    $(du -sh "$REPO" 2>/dev/null | cut -f1)"
}

# ===================== 8/9) ПОДКЛЮЧЕНИЕ =====================
do_attach() {
  echo "=== [🔌] Подключение репозитория на этой машине ==="
  echo "deb [trusted=yes] $REPO_URL $CODENAME main" | sudo tee /etc/apt/sources.list.d/nikita-repo.list >/dev/null
  sudo apt update
  echo "✅ Подключено: /etc/apt/sources.list.d/nikita-repo.list"
}
do_detach() {
  echo "=== [🔌] Отключение репозитория ==="
  sudo rm -f /etc/apt/sources.list.d/nikita-repo.list
  sudo apt update
  echo "✅ Отключено"
}

# ===================== 11) УСТАНОВКА ПО СПИСКУ =====================
do_install_list() {
  echo "=== [📥] Установка софта по списку ==="
  if [ ! -f "$LIST" ]; then
    if [ -f "$REPO/mysoft.txt" ] && [ -s "$REPO/mysoft.txt" ]; then
      cp "$REPO/mysoft.txt" "$LIST"
      echo "Список сгенерирован из $REPO/mysoft.txt (твои manual-пакеты). Правка — пункт 12."
    else
      echo "⚠️ Нет списка $LIST и нет mysoft.txt — создай список пунктом 12"
      return 1
    fi
  fi
  if [ ! -f /etc/apt/sources.list.d/nikita-repo.list ]; then
    echo "Репозиторий не подключён — подключаю (пункт 8)..."
    do_attach || return 1
  fi
  local rpath; rpath=$(nas_remote_rsync)
  if [ -n "$rpath" ]; then
    rsync -a -e "$RSH" --rsync-path="$rpath" "$NAS_TARGET/offline/" "$REPO/offline/" 2>/dev/null \
      || echo "⚠️ Не удалось подтянуть offline с NAS — snap/flatpak пойдут из store"
  fi
  local fail=()
  echo "-- apt-пакеты --"
  while read -r p; do
    case "$p" in ''|'#'*|snap:*|flatpak:*) continue;; esac
    echo "⬇ apt: $p"
    sudo apt install -y "$p" >/dev/null 2>&1 || { echo "  ⚠️ не встало: $p"; fail+=("$p"); }
  done < "$LIST"
  echo "-- snap --"
  while read -r line; do
    case "$line" in snap:*) ;; *) continue;; esac
    local s="${line#snap:}"
    local b; b=$(ls "$REPO/offline/snaps/" 2>/dev/null | grep "^${s}_" | head -1)
    if [ -n "$b" ]; then
      echo "⬇ snap (офлайн-бандл): $s"
      sudo snap install --dangerous "$REPO/offline/snaps/$b" >/dev/null 2>&1 || fail+=("$s")
    else
      echo "⬇ snap (store): $s"
      sudo snap install "$s" >/dev/null 2>&1 || fail+=("$s")
    fi
  done < "$LIST"
  echo "-- flatpak --"
  while read -r line; do
    case "$line" in flatpak:*) ;; *) continue;; esac
    local app="${line#flatpak:}"
    local b; b=$(ls "$REPO/offline/flats/" 2>/dev/null | grep "^${app}-" | head -1)
    if [ -n "$b" ]; then
      echo "⬇ flatpak (бандл): $app"
      flatpak install --bundle -y "$REPO/offline/flats/$b" >/dev/null 2>&1 || fail+=("$app")
    else
      echo "⬇ flatpak (flathub): $app"
      flatpak install -y flathub "$app" >/dev/null 2>&1 || fail+=("$app")
    fi
  done < "$LIST"
  echo
  if [ ${#fail[@]} -eq 0 ]; then echo "✅ Всё из списка установлено"; else echo "⚠️ Не установилось: ${fail[*]}"; fi
}

# ===================== 12) РЕДАКТОР СПИСКА =====================
do_edit_list() {
  echo "=== [✏️] Список установки: $LIST ==="
  if [ ! -f "$LIST" ]; then
    if [ -f "$REPO/mysoft.txt" ] && [ -s "$REPO/mysoft.txt" ]; then cp "$REPO/mysoft.txt" "$LIST"; else : > "$LIST"; fi
    echo "Создан. Формат: по одному apt-пакету в строке; snap:имя; flatpak:id; # — комментарий"
  fi
  "${EDITOR:-nano}" "$LIST"
}

# ===================== 10) ДИАГНОСТИКА NAS =====================
do_diag() {
  local LOG="$TRACKER_DIR/nas-diag.log"
  echo "Пароль понадобится ОДИН раз (соединение переиспользуется)."
  {
    echo "===== DIAG $(date '+%F %T') | $(hostname) ====="
    echo "--- 1. Локальные версии ---"
    ssh -V 2>&1
    rsync --version 2>/dev/null | head -1 || echo "локально нет rsync"
    echo "--- 2. Цель ---"
    echo "NAS_TARGET=$NAS_TARGET"
    echo "USER=$NAS_USER HOST=$NAS_HOST PATH=$NAS_PATH"
    echo "--- 3. TCP порт 22 ---"
    if timeout 5 bash -c "echo > /dev/tcp/$NAS_HOST/22" 2>/dev/null; then echo "port 22: OPEN"; else echo "port 22: CLOSED/FILTERED"; fi
    echo "--- 4. Эффективный конфиг ssh ---"
    ssh -G "$NAS_USER@$NAS_HOST" 2>/dev/null | grep -Ei '^(user|hostname|port|identityfile|preferredauthentications|pubkeyauthentication) '
    echo "--- 5. Вход по ключу (BatchMode, без пароля) ---"
    ssh -o ControlMaster=auto -o ControlPath="$CTRL_SOCK" -o ControlPersist=600 -o BatchMode=yes -o ConnectTimeout=8 \
        "$NAS_USER@$NAS_HOST" 'echo KEY_AUTH_OK' 2>&1 | head -3
    echo "--- 6. Вход по паролю + окружение NAS (введи пароль один раз) ---"
    ssh -o ControlMaster=auto -o ControlPath="$CTRL_SOCK" -o ControlPersist=600 -o ConnectTimeout=8 \
        "$NAS_USER@$NAS_HOST" "echo REMOTE_SHELL_OK; id; echo SHELL=\$0; echo PATH=\$PATH; \
        echo 'rsync in PATH:' \$(command -v rsync || echo NO); \
        echo '/usr/syno/bin/rsync:' \$([ -x /usr/syno/bin/rsync ] && echo YES || echo NO); \
        echo '/usr/bin/rsync:' \$([ -x /usr/bin/rsync ] && echo YES || echo NO); \
        echo 'DSM:' \$(cat /etc.defaults/VERSION 2>/dev/null | head -2 | tr '\n' ' '); \
        ls -ld $NAS_PATH 2>&1; \
        mkdir -p $NAS_PATH && touch $NAS_PATH/.writetest && echo WRITE_OK && rm -f $NAS_PATH/.writetest; \
        df -h $NAS_PATH 2>/dev/null | tail -1" 2>&1 | head -25
    echo "--- 7. sshd_config и баннеры (могут ломать протокол rsync) ---"
    ssh -o ControlMaster=auto -o ControlPath="$CTRL_SOCK" -o ControlPersist=600 \
        "$NAS_USER@$NAS_HOST" "grep -Ei '^(PermitRootLogin|PasswordAuthentication|PubkeyAuthentication|AllowUsers|AllowGroups|DenyUsers|Banner|PrintMotd)' /etc/ssh/sshd_config 2>/dev/null || echo CANNOT_READ_SSHD_CONFIG; \
        ls -l /etc/motd 2>/dev/null || echo 'no /etc/motd'" 2>&1 | head -12
    echo "--- 8. Тест rsync dry-run ---"
    local rp; rp=$(nas_remote_rsync)
    echo "remote rsync: ${rp:-NOT_FOUND}"
    if [ -n "$rp" ]; then
      rsync -avn -e "$RSH" --rsync-path="$rp" "$REPO/manifest.txt" "$NAS_TARGET/" 2>&1 | head -5
    fi
    ssh -O exit -o ControlPath="$CTRL_SOCK" "$NAS_USER@$NAS_HOST" 2>/dev/null
    echo "===== END ====="
  } 2>&1 | tee "$LOG"
  echo
  echo "📄 Лог сохранён: $LOG — пришли его содержимое мне"
}

# ===================== 1) ПОЛНЫЙ ЦИКЛ =====================
do_full() {
  do_tracker
  echo
  do_builder
  echo
  do_offline
  echo
  echo "🎉 ПОЛНЫЙ ЦИКЛ ЗАВЕРШЁН"
}

# ===================== МЕНЮ =====================
while true; do
  clear
  echo "=================================================="
  echo "  🧰 SOFT-REPO MANAGER v1.3 | $(hostname) | $CODENAME"
  echo "  NAS:  $NAS_TARGET"
  echo "  URL:  $REPO_URL"
  echo "=================================================="
  echo " 1) 🔄 Полный цикл: отчёт → репо → offline → публикация"
  echo " 2) 📊 Только отчёт (трекер)"
  echo " 3) 📦 Обновить apt-репо (дедуп + индексы + публикация)"
  echo " 4) 💾 Офлайн-копии snap/flatpak (+ публикация)"
  echo " 5) 📤 Только опубликовать репо на NAS"
  echo " 6) 🌐 Проверить доступность репо"
  echo " 7) 📈 Статистика"
  echo " 8) 🔌 Подключить репо на этой машине"
  echo " 9) 🔌 Отключить репо на этой машине"
  echo "10) 🩺 Диагностика NAS (лог для анализа)"
  echo "11) 📥 Установить софт по списку (мой набор)"
  echo "12) ✏️ Создать/править список установки"
  echo " 0) 🚪 Выход"
  echo "--------------------------------------------------"
  read -r -p " Выбор: " choice
  case "$choice" in
    1) do_full ;;
    2) do_tracker ;;
    3) do_builder ;;
    4) do_offline ;;
    5) do_publish_repo ;;
    6) do_check ;;
    7) do_stats ;;
    8) do_attach ;;
    9) do_detach ;;
    10) do_diag ;;
    11) do_install_list ;;
    12) do_edit_list ;;
    0) echo "Пока!"; exit 0 ;;
    *) echo "Неверный выбор"; sleep 1 ;;
  esac
  echo
  read -r -p "Нажми Enter для возврата в меню..." _
done