#!/bin/bash
# ========== install_dns.sh ==========
# Установка и настройка DNS-сервера Unbound в Docker
# Создаёт каталоги, конфиги, скрипт обновления adblock,
# отключает systemd-resolved, настраивает resolv.conf,
# запускает extract_geosite.sh для подготовки списков,
# затем запускает контейнер через docker compose.
# После запуска удаляет резервный nameserver и проверяет порты.

# ---------- ЛОГИРОВАНИЕ ----------
log_info() {
    echo "ℹ️  [INFO] $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
log_success() {
    echo "✅ [SUCCESS] $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
log_error() {
    echo "❌ [ERROR] $(date '+%Y-%m-%d %H:%M:%S') - $1" >&2
}
log_step() {
    echo "📋 [STEP] $(date '+%Y-%m-%d %H:%M:%S') - $1"
    echo "=================================================="
}

# ---------- ПРОВЕРКА ROOT ----------
if [[ $EUID -ne 0 ]]; then
    log_error "Модуль install_dns.sh должен запускаться с правами root"
    exit 1
fi

# ---------- ПРОВЕРКА ПЕРЕМЕННОЙ FULL_DOMAIN ----------
if [[ -z "$FULL_DOMAIN" ]]; then
    log_error "Переменная FULL_DOMAIN не установлена. Запустите главный скрипт и введите локацию."
    exit 1
fi

log_info "Настройка DNS для домена: $FULL_DOMAIN"

# ---------- ОСТАНОВКА И ОТКЛЮЧЕНИЕ SYSTEMD-RESOLVED ----------
log_step "Отключение systemd-resolved"
if systemctl is-active --quiet systemd-resolved; then
    systemctl stop systemd-resolved
    systemctl disable systemd-resolved
    log_success "systemd-resolved остановлен и отключён"
else
    log_info "systemd-resolved не активен, пропускаем"
fi

# ---------- НАСТРОЙКА /ETC/RESOLV.CONF (ВРЕМЕННАЯ) ----------
log_step "Настройка /etc/resolv.conf"
rm -f /etc/resolv.conf
cat > /etc/resolv.conf << EOF
nameserver 127.0.0.1
nameserver 1.1.1.1
EOF
log_success "/etc/resolv.conf временно содержит локальный Unbound и 1.1.1.1"

# ---------- ПРОВЕРКА НАЛИЧИЯ DOCKER И ПЛАГИНА COMPOSE ----------
log_step "Проверка наличия Docker и плагина compose"
if ! command -v docker &> /dev/null; then
    log_error "Docker не установлен. Сначала выполните модуль prepare_vm.sh (пункт 1)."
    exit 1
fi

if ! docker compose version &> /dev/null; then
    log_error "Плагин 'docker compose' не доступен. Убедитесь, что Docker установлен корректно и содержит compose."
    exit 1
fi
log_success "Docker и плагин compose доступны: $(docker compose version)"

# ---------- СОЗДАНИЕ КАТАЛОГОВ ----------
log_step "Создание необходимых каталогов"
mkdir -p /opt/dns /opt/log /opt/log/dns /opt/dns/adblock /opt/dns/import
# Создаём файл лога с правами на запись для всех
touch /opt/log/dns/unbound.log
chmod 777 /opt/log/dns/unbound.log
log_success "Каталоги созданы, файл /opt/log/dns/unbound.log доступен для записи (777)"

# ---------- ФАЙЛ ADBLOCK.CONF ----------
cat > /opt/dns/adblock.conf << 'EOF'
include: "/opt/unbound/adblock/custom_sites.txt"
EOF
log_success "Файл /opt/dns/adblock.conf создан"

# ---------- ПУСТОЙ ФАЙЛ CUSTOM_SITES.TXT ----------
touch /opt/dns/adblock/custom_sites.txt
log_success "Пустой файл /opt/dns/adblock/custom_sites.txt создан"

# ---------- СКРИПТ EXTRACT_GEOSITE.SH ----------
cat > /opt/dns/import/extract_geosite.sh << 'EOF'
#!/usr/bin/env bash
set -e

# Конфигурация
URL="https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release/geosite.dat"
GEOSITE_DAT="/opt/dns/import/geosite.dat"
ADBLOCK_DIR="/opt/dns/adblock"
CONF_FILE="/opt/dns/adblock.conf"
CATEGORIES_FILE="/opt/dns/import/categories.txt"
CUSTOM_SITES="custom_sites.txt"
CONTAINER_NAME="unbound"

# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }

# Проверка прав доступа
if [[ $EUID -ne 0 ]]; then
   error "Этот скрипт должен запускаться от root (sudo)."
fi

# --- Функции для установки зависимостей ---
detect_package_manager() {
    if command -v apt &>/dev/null; then echo "apt"
    elif command -v dnf &>/dev/null; then echo "dnf"
    elif command -v yum &>/dev/null; then echo "yum"
    elif command -v pacman &>/dev/null; then echo "pacman"
    elif command -v zypper &>/dev/null; then echo "zypper"
    else echo "unknown"; fi
}

get_package_name() {
    local cmd=$1
    local pkg_manager=$2
    case "$pkg_manager" in
        apt)
            case "$cmd" in
                go) echo "golang-go" ;;
                curl) echo "curl" ;;
                wget) echo "wget" ;;
                *) echo "$cmd" ;;
            esac ;;
        yum|dnf)
            case "$cmd" in
                go) echo "golang" ;;
                *) echo "$cmd" ;;
            esac ;;
        pacman)
            case "$cmd" in
                go) echo "go" ;;
                curl) echo "curl" ;;
                wget) echo "wget" ;;
                *) echo "$cmd" ;;
            esac ;;
        zypper)
            case "$cmd" in
                go) echo "go" ;;
                curl) echo "curl" ;;
                wget) echo "wget" ;;
                *) echo "$cmd" ;;
            esac ;;
        *) echo "$cmd" ;;
    esac
}

install_dependencies() {
    local pkgs=("$@")
    local pkg_manager=$(detect_package_manager)
    if [[ "$pkg_manager" == "unknown" ]]; then
        error "Не удалось определить менеджер пакетов. Установите вручную: ${pkgs[*]}"
    fi

    for cmd in "${pkgs[@]}"; do
        local pkg=$(get_package_name "$cmd" "$pkg_manager")
        info "Устанавливаю $cmd (пакет $pkg) через $pkg_manager..."
        case "$pkg_manager" in
            apt) apt update && apt install -y "$pkg" ;;
            dnf) dnf install -y "$pkg" ;;
            yum) yum install -y "$pkg" ;;
            pacman) pacman -S --noconfirm "$pkg" ;;
            zypper) zypper install -y "$pkg" ;;
        esac
        if [[ $? -ne 0 ]]; then
            error "Не удалось установить $pkg. Установите вручную."
        fi
    done
}

check_downloader() {
    if command -v wget &>/dev/null; then
        echo "wget"
    elif command -v curl &>/dev/null; then
        echo "curl"
    else
        warn "Ни wget, ни curl не найдены. Пытаюсь установить curl..."
        install_dependencies "curl"
        echo "curl"
    fi
}

ensure_v2dat() {
    if ! command -v v2dat &>/dev/null; then
        info "v2dat не найден. Устанавливаю через go install..."
        if ! command -v go &>/dev/null; then
            install_dependencies "go"
        fi
        export GOPROXY=https://proxy.golang.org,direct
        go install github.com/urlesistiana/v2dat@latest
        local go_bin="$(go env GOPATH)/bin/v2dat"
        if [[ ! -f "$go_bin" ]]; then
            error "Не удалось установить v2dat."
        fi
        cp "$go_bin" /usr/local/bin/v2dat
        info "v2dat установлен в /usr/local/bin/v2dat"
    else
        info "v2dat уже установлен."
    fi
}

# --- Основные функции ---
download_geosite() {
    local downloader=$(check_downloader)
    info "Скачиваю geosite.dat с GitHub..."
    if [[ "$downloader" == "wget" ]]; then
        wget -q --show-progress -O "$GEOSITE_DAT" "$URL"
    else
        curl -L -o "$GEOSITE_DAT" "$URL"
    fi
    if [[ ! -s "$GEOSITE_DAT" ]]; then
        error "Скачанный файл пуст. Проверьте соединение."
    fi
    info "Скачивание завершено: $GEOSITE_DAT ($(du -h "$GEOSITE_DAT" | cut -f1))"
}

read_categories() {
    if [[ ! -f "$CATEGORIES_FILE" ]]; then
        error "Файл с категориями не найден: $CATEGORIES_FILE"
    fi
    mapfile -t categories < <(grep -v '^[[:space:]]*#' "$CATEGORIES_FILE" | grep -v '^[[:space:]]*$')
    if [[ ${#categories[@]} -eq 0 ]]; then
        error "Файл категорий пуст (игнорируя комментарии)."
    fi
    info "Загружено категорий: ${#categories[@]}"
}

# Глобальные массивы
success=()
failed=()

extract_and_convert() {
    mkdir -p "$ADBLOCK_DIR"
    info "Извлекаю и конвертирую категории в правила блокировки..."
    success=()
    failed=()

    for cat in "${categories[@]}"; do
        cat_clean=$(echo "$cat" | xargs)
        [[ -z "$cat_clean" ]] && continue

        local raw_file="$ADBLOCK_DIR/geosite_${cat_clean}.txt"
        local conf_file="$ADBLOCK_DIR/geosite_${cat_clean}.conf"

        if v2dat unpack geosite -o "$ADBLOCK_DIR" -f "$cat_clean" "$GEOSITE_DAT" 2>/dev/null; then
            if [[ -f "$raw_file" ]]; then
                > "$conf_file"
                while IFS= read -r domain; do
                    [[ -z "$domain" || "$domain" =~ ^# ]] && continue
                    domain=$(echo "$domain" | sed -E 's/^(domain:|full:|regexp:)//')
                    echo "local-zone: \"$domain\" redirect" >> "$conf_file"
                    echo "local-data: \"$domain A 0.0.0.0\"" >> "$conf_file"
                done < "$raw_file"
                rm -f "$raw_file"
                success+=("$cat_clean")
                info "  + $cat_clean (преобразован)"
            else
                warn "  ? $cat_clean (файл не создан)"
                failed+=("$cat_clean")
            fi
        else
            warn "  ! $cat_clean (ошибка извлечения)"
            failed+=("$cat_clean")
        fi
    done

    if [[ ${#success[@]} -eq 0 ]]; then
        error "Ни одной категории не обработано."
    fi
    info "Успешно обработано: ${#success[@]}, пропущено: ${#failed[@]}"
}

generate_conf() {
    # Убедимся, что custom_sites.txt существует
    if [[ ! -f "$ADBLOCK_DIR/$CUSTOM_SITES" ]]; then
        touch "$ADBLOCK_DIR/$CUSTOM_SITES"
        info "Создан пустой файл $ADBLOCK_DIR/$CUSTOM_SITES"
    fi

    # Очищаем и записываем новый adblock.conf с путями ВНУТРИ КОНТЕЙНЕРА
    > "$CONF_FILE"
    # Первая строка — всегда custom_sites.txt
    echo "include: \"/opt/unbound/adblock/$CUSTOM_SITES\"" >> "$CONF_FILE"
    # Затем все успешно преобразованные .conf файлы
    for cat in "${success[@]}"; do
        echo "include: \"/opt/unbound/adblock/geosite_${cat}.conf\"" >> "$CONF_FILE"
    done
    info "Создан $CONF_FILE с ${#success[@]}+1 include-строк."
}

restart_unbound_container() {
    if command -v docker &>/dev/null; then
        if docker ps --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
            info "Перезагружаю конфигурацию Unbound в контейнере $CONTAINER_NAME..."
            if docker exec "$CONTAINER_NAME" unbound-control reload 2>/dev/null; then
                info "unbound-control reload выполнен успешно."
            else
                warn "unbound-control reload не удался, пробую перезапустить контейнер..."
                docker restart "$CONTAINER_NAME"
            fi
        else
            warn "Контейнер '$CONTAINER_NAME' не найден или не запущен. Перезапустите его вручную."
        fi
    else
        warn "Docker не найден. Перезапустите Unbound вручную."
    fi
}

main() {
    info "=== Обновление списков блокировки Unbound ==="
    ensure_v2dat
    download_geosite
    read_categories
    extract_and_convert
    generate_conf
    restart_unbound_container
    info "Готово. Новые списки загружены."
}

main "$@"
EOF
chmod +x /opt/dns/import/extract_geosite.sh
log_success "Скрипт /opt/dns/import/extract_geosite.sh создан и сделан исполняемым"

# ---------- ФАЙЛ DOCKER-COMPOSE.YML ----------
cat > /opt/dns/docker-compose.yml << 'EOF'
services:
  unbound:
    image: mvance/unbound:latest
    container_name: unbound
    restart: unless-stopped
    network_mode: host
    security_opt:
      - apparmor:unconfined
    command: ["unbound", "-d", "-c", "/opt/unbound/unbound.conf"]
    volumes:
      - /opt/dns/unbound.conf:/opt/unbound/unbound.conf:ro
      - /opt/dns/adblock.conf:/opt/unbound/adblock.conf:ro
      - /opt/dns/adblock:/opt/unbound/adblock:ro
      - /opt/dns/import:/opt/unbound/import:ro
      - /opt/cert:/opt/unbound/cert:ro
      - /opt/log/dns/unbound.log:/opt/unbound/unbound.log:rw
EOF
log_success "Файл /opt/dns/docker-compose.yml создан"

# ---------- ФАЙЛ UNBOUND.CONF (с подстановкой FULL_DOMAIN) ----------
PRIVKEY_FILE="/opt/unbound/cert/${FULL_DOMAIN}_privkey.key"
FULLCHAIN_FILE="/opt/unbound/cert/${FULL_DOMAIN}_fullchain.pem"

cat > /opt/dns/unbound.conf << EOF
server:
    # --- Отключаем chroot ---
    chroot: ""
    # --- Сеть (только IPv4) ---
    interface: 0.0.0.0@53
    interface: 0.0.0.0@853

    # --- Контроль доступа ---
    access-control: 127.0.0.0/8 allow
    access-control: 10.0.0.0/8 allow
    access-control: 172.16.0.0/12 allow
    access-control: 192.168.0.0/16 allow
    access-control: 0.0.0.0/0 allow
    access-control: 0.0.0.0/0 refuse

    # --- Отключить IPv6 ---
    do-ip6: no

    # --- TLS (DoT) ---
    tls-service-key: $PRIVKEY_FILE
    tls-service-pem: $FULLCHAIN_FILE
    tls-port: 853
    tls-session-ticket-keys: ""

    # --- Логирование ---
    logfile: "/opt/unbound/unbound.log"
    verbosity: 0
    use-syslog: no

    # --- Безопасность и производительность ---
    hide-identity: yes
    hide-version: yes
    msg-cache-size: 50m
    rrset-cache-size: 100m
    cache-min-ttl: 300
    cache-max-ttl: 86400
    aggressive-nsec: yes
#    auto-trust-anchor-file: /opt/unbound/root.key

    # --- Рекурсия ---
    num-threads: 2
    outgoing-range: 8192
    num-queries-per-thread: 512
EOF
log_success "Файл /opt/dns/unbound.conf создан с использованием сертификатов для $FULL_DOMAIN"

# ---------- ФАЙЛ CATEGORIES.TXT ----------
echo "win-spy" > /opt/dns/import/categories.txt
log_success "Файл /opt/dns/import/categories.txt создан"

# ---------- ПРОВЕРКА НАЛИЧИЯ СЕРТИФИКАТОВ ----------
if [[ ! -f "/opt/cert/${FULL_DOMAIN}_privkey.key" ]] || [[ ! -f "/opt/cert/${FULL_DOMAIN}_fullchain.pem" ]]; then
    log_error "Сертификаты для $FULL_DOMAIN не найдены в /opt/cert. Сначала выполните генерацию сертификатов (пункт 2 меню)."
    exit 1
else
    log_info "Сертификаты найдены, продолжаем."
fi

# ---------- ПЕРВОНАЧАЛЬНЫЙ ЗАПУСК СКРИПТА ADBLOCK ----------
log_step "Первоначальное создание adblock-списков"
if [[ -x /opt/dns/import/extract_geosite.sh ]]; then
    /opt/dns/import/extract_geosite.sh
    if [[ $? -eq 0 ]]; then
        log_success "Списки adblock успешно подготовлены"
    else
        log_warning "Скрипт extract_geosite.sh завершился с ошибкой, но контейнер будет запущен. Возможно, списки не загружены."
    fi
else
    log_warning "Скрипт extract_geosite.sh не найден или не исполняемый"
fi

# ---------- ЗАПУСК КОНТЕЙНЕРА ----------
log_step "Запуск Unbound через docker compose"
cd /opt/dns || exit 1
docker compose up -d

if [[ $? -eq 0 ]]; then
    log_success "Контейнер Unbound успешно запущен"
else
    log_error "Ошибка при запуске контейнера"
    exit 1
fi

# ---------- ПРОВЕРКА СТАТУСА ----------
sleep 3
if docker ps | grep -q unbound; then
    log_success "Unbound работает. Просмотр логов: docker logs unbound"
else
    log_error "Unbound не запустился. Проверьте конфигурацию: docker logs unbound"
    exit 1
fi

# ---------- КОРРЕКТИРОВКА /ETC/RESOLV.CONF (УДАЛЕНИЕ ЗАПАСНОГО NS) ----------
log_step "Обновление /etc/resolv.conf (оставляем только локальный Unbound)"
# Удаляем строку с 1.1.1.1, если она есть
sed -i '/^nameserver 1\.1\.1\.1$/d' /etc/resolv.conf
# Убедимся, что запись с 127.0.0.1 присутствует (должна быть)
if ! grep -q '^nameserver 127\.0\.0\.1$' /etc/resolv.conf; then
    echo "nameserver 127.0.0.1" >> /etc/resolv.conf
fi
log_success "/etc/resolv.conf теперь содержит только 127.0.0.1"

# ---------- УБЕДИМСЯ, ЧТО У НАС ЕСТЬ УТИЛИТА ДЛЯ ПРОВЕРКИ ПОРТОВ ----------
log_step "Проверка наличия утилит для проверки портов (ss или netstat)"
SS_AVAILABLE=false
NETSTAT_AVAILABLE=false

# Проверка ss
if command -v ss &> /dev/null; then
    SS_AVAILABLE=true
    log_success "ss доступен."
else
    log_warning "ss не найден. Пробую установить iproute2..."
    if command -v apt &> /dev/null; then
        apt update && apt install -y iproute2
    elif command -v dnf &> /dev/null; then
        dnf install -y iproute2
    elif command -v yum &> /dev/null; then
        yum install -y iproute2
    elif command -v pacman &> /dev/null; then
        pacman -S --noconfirm iproute2
    elif command -v zypper &> /dev/null; then
        zypper install -y iproute2
    else
        log_warning "Не удалось определить менеджер пакетов для установки iproute2."
    fi
    if command -v ss &> /dev/null; then
        SS_AVAILABLE=true
        log_success "ss успешно установлен."
    fi
fi

# Если ss не доступен, проверяем netstat
if ! $SS_AVAILABLE; then
    if command -v netstat &> /dev/null; then
        NETSTAT_AVAILABLE=true
        log_success "netstat доступен (будет использоваться как запасной вариант)."
    else
        log_warning "netstat не найден. Пробую установить net-tools..."
        if command -v apt &> /dev/null; then
            apt update && apt install -y net-tools
        elif command -v dnf &> /dev/null; then
            dnf install -y net-tools
        elif command -v yum &> /dev/null; then
            yum install -y net-tools
        elif command -v pacman &> /dev/null; then
            pacman -S --noconfirm net-tools
        elif command -v zypper &> /dev/null; then
            zypper install -y net-tools
        else
            log_warning "Не удалось определить менеджер пакетов для установки net-tools."
        fi
        if command -v netstat &> /dev/null; then
            NETSTAT_AVAILABLE=true
            log_success "netstat успешно установлен."
        fi
    fi
fi

# ---------- ПРОВЕРКА ДОСТУПНОСТИ ПОРТОВ 53 И 853 ----------
if $SS_AVAILABLE; then
    log_step "Проверка доступности портов 53 и 853 с помощью ss"
    check_port() {
        local port=$1
        local proto=$2
        if ss -lntu | grep -q ":$port.*$proto"; then
            log_success "Порт $port/$proto прослушивается"
            return 0
        else
            log_error "Порт $port/$proto не прослушивается"
            return 1
        fi
    }
    TCP53_OK=0; UDP53_OK=0; TCP853_OK=0
    check_port 53 tcp || TCP53_OK=1
    check_port 53 udp || UDP53_OK=1
    check_port 853 tcp || TCP853_OK=1
    if [[ $TCP53_OK -eq 0 && $UDP53_OK -eq 0 && $TCP853_OK -eq 0 ]]; then
        log_success "Все необходимые порты (53/tcp, 53/udp, 853/tcp) доступны."
    else
        log_warning "Некоторые порты не прослушиваются. Проверьте конфигурацию Unbound."
    fi
elif $NETSTAT_AVAILABLE; then
    log_step "Проверка доступности портов 53 и 853 с помощью netstat"
    check_port_netstat() {
        local port=$1
        local proto=$2
        if netstat -tulpn 2>/dev/null | grep -q ":$port.*$proto"; then
            log_success "Порт $port/$proto прослушивается"
            return 0
        else
            log_error "Порт $port/$proto не прослушивается"
            return 1
        fi
    }
    TCP53_OK=0; UDP53_OK=0; TCP853_OK=0
    check_port_netstat 53 tcp || TCP53_OK=1
    check_port_netstat 53 udp || UDP53_OK=1
    check_port_netstat 853 tcp || TCP853_OK=1
    if [[ $TCP53_OK -eq 0 && $UDP53_OK -eq 0 && $TCP853_OK -eq 0 ]]; then
        log_success "Все необходимые порты (53/tcp, 53/udp, 853/tcp) доступны."
    else
        log_warning "Некоторые порты не прослушиваются. Проверьте конфигурацию Unbound."
    fi
else
    log_warning "Ни ss, ни netstat не доступны. Пропускаем проверку портов."
fi

log_success "Модуль install_dns.sh выполнен"
exit 0
