#!/usr/bin/env bash
#
# DNS Shield ISP 1.3.4 - Instalador automatico para Debian 11/12/13
# Gestao de servidor DNS recursivo com bloqueio judicial (RPZ / DNS Firewall)
#
# Desenvolvedor: Euquias Junior <junior@jbinfo.com.br>
#
set -Eeuo pipefail

APP_NAME="DNS Shield ISP"
APP_VERSION="1.3.4"
GITHUB_REPO="juniorgnv-svg/secure-dns-guardian"
LICENCA_URL="https://licenca.jbinfo.com.br"


REPO_DIR="/opt/dnsshield"
APP_DIR="${REPO_DIR}/dns-shield-isp"
VENV_DIR="${REPO_DIR}/venv"
CONF_DIR="/etc/dnsshield"
CONF_FILE="${CONF_DIR}/config.ini"
DATA_DIR="/var/lib/dnsshield"
DB_FILE="${DATA_DIR}/dnsshield.db"
WWW_DIR="/var/www/dnsshield"
RPZ_ZONE="bloqueados.fndns.lan"
RPZ_DIR="/etc/bind/rpz"
RPZ_FILE="${RPZ_DIR}/db.${RPZ_ZONE%%.*}"
TSIG_FILE="/etc/bind/dnsshield-transfer.key"
LOG_FILE="/var/log/dnsshield-install.log"

SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

C_RESET="\033[0m"; C_BOLD="\033[1m"; C_BLUE="\033[34m"
C_GREEN="\033[32m"; C_YELLOW="\033[33m"; C_RED="\033[31m"

log()  { printf "%s %s\n" "$(date '+%F %T')" "$*" >>"${LOG_FILE}" 2>/dev/null || true; }
info() { printf "${C_BLUE}[info]${C_RESET} %s\n" "$*"; log "[info] $*"; }
ok()   { printf "${C_GREEN}[ ok ]${C_RESET} %s\n" "$*"; log "[ ok ] $*"; }
warn() { printf "${C_YELLOW}[warn]${C_RESET} %s\n" "$*"; log "[warn] $*"; }
die()  { printf "${C_RED}[erro]${C_RESET} %s\n" "$*" >&2; log "[erro] $*"; exit 1; }

trap 'die "Instalacao interrompida na linha ${LINENO}. Consulte ${LOG_FILE}."' ERR

banner() {
  printf "\n${C_BOLD}==============================================================${C_RESET}\n"
  printf "  ${C_BOLD}%s %s${C_RESET}\n" "${APP_NAME}" "${APP_VERSION}"
  printf "  DNS recursivo + bloqueio judicial (RPZ) para provedores\n"
  printf "  Desenvolvedor: Euquias Junior <junior@jbinfo.com.br>\n"
  printf "${C_BOLD}==============================================================${C_RESET}\n\n"
}

# ------------------------------------------------------------------ validacoes

require_root() {
  [[ "${EUID}" -eq 0 ]] || die "Execute como root: sudo ./install.sh"
}

require_debian() {
  [[ -f /etc/os-release ]] || die "Sistema operacional nao identificado."
  # shellcheck disable=SC1091
  . /etc/os-release
  [[ "${ID:-}" == "debian" ]] || warn "Distribuicao '${ID:-?}' nao homologada. Homologado: Debian 11/12/13."
  case "${VERSION_ID:-}" in
    11|12|13) ok "Debian ${VERSION_ID} detectado (homologado)." ;;
    *) warn "Debian ${VERSION_ID:-?} nao homologado. Prosseguindo por sua conta e risco." ;;
  esac
}

check_connectivity() {
  info "Verificando acesso a internet..."
  if ! getent hosts deb.debian.org >/dev/null 2>&1; then
    die "Sem resolucao DNS/internet. Configure a rede antes de instalar."
  fi
  ok "Conectividade confirmada."
}

# ------------------------------------------------------------------ validadores

valid_cidr() {
  local v="$1"
  [[ "$v" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}(/[0-9]{1,2})?$ ]] && return 0
  [[ "$v" =~ ^[0-9A-Fa-f:]+(/[0-9]{1,3})?$ && "$v" == *:* ]] && return 0
  return 1
}

valid_ip() {
  local v="$1"
  [[ "$v" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] && return 0
  [[ "$v" =~ ^[0-9A-Fa-f:]+$ && "$v" == *:* ]] && return 0
  return 1
}

valid_url() { [[ "$1" =~ ^https?://[A-Za-z0-9._~:/?#@!$\&\'\(\)\*\+,\;=%-]+$ ]]; }

ask() { # ask <var> <pergunta> [default]
  local __var="$1" __q="$2" __def="${3:-}" __ans=""
  while :; do
    if [[ -n "${__def}" ]]; then
      read -r -p "$(printf "${C_BOLD}?${C_RESET} %s [%s]: " "${__q}" "${__def}")" __ans
      __ans="${__ans:-${__def}}"
    else
      read -r -p "$(printf "${C_BOLD}?${C_RESET} %s: " "${__q}")" __ans
    fi
    [[ -n "${__ans}" ]] && break
    warn "Campo obrigatorio."
  done
  printf -v "${__var}" '%s' "${__ans}"
}

# ---------------------------------------------------------------- questionario

ACL_LIST=()
SERVER_ROLE=""
PEER_IP=""
ADMIN_USER=""; ADMIN_PASS=""
SINKHOLE_IP=""
SINKHOLE_IP6="::1"

# Redes privadas sempre autorizadas (RFC1918 + CGNAT + loopback/link-local IPv6).
# O operador informa apenas os blocos publicos/roteaveis do provedor.
PRIVATE_NETS=(
  "127.0.0.0/8"
  "10.0.0.0/8"
  "172.16.0.0/12"
  "192.168.0.0/16"
  "100.64.0.0/10"
  "::1/128"
  "fc00::/7"
  "fe80::/10"
)

ask_networks() {
  printf "\n${C_BOLD}1) Blocos IP do provedor autorizados a usar este DNS${C_RESET}\n"
  printf "   As redes privadas (10/8, 172.16/12, 192.168/16, CGNAT 100.64/10 e IPv6 local)\n"
  printf "   ja vem liberadas automaticamente - nao precisa digita-las.\n"
  printf "   Informe apenas os blocos publicos/roteaveis do provedor, um por linha.\n"
  printf "   Ex: 203.0.113.0/24, 2001:db8::/32. Linha vazia encerra a lista.\n\n"
  local entry
  while :; do
    read -r -p "   Bloco (ENTER para finalizar): " entry || true
    entry="$(printf '%s' "${entry}" | tr -d '[:space:]')"
    if [[ -z "${entry}" ]]; then
      if [[ ${#ACL_LIST[@]} -eq 0 ]]; then
        warn "Nenhum bloco publico informado - apenas as redes privadas serao atendidas."
      fi
      break
    fi
    if valid_cidr "${entry}"; then
      ACL_LIST+=("${entry}")
      ok "Adicionado: ${entry}"
    else
      warn "Formato invalido: ${entry}"
    fi
  done
}

ask_role() {
  printf "\n${C_BOLD}2) Tipo de servidor${C_RESET}\n"
  printf "   [1] Primario  - Master da zona RPZ + Painel Web (porta 8080)\n"
  printf "   [2] Secundario - Slave, apenas sincronizacao via NOTIFY/AXFR\n\n"
  local choice
  while :; do
    read -r -p "   Opcao [1/2]: " choice || true
    case "${choice}" in
      1) SERVER_ROLE="primary"; ok "Servidor PRIMARIO selecionado."; break ;;
      2) SERVER_ROLE="secondary"; ok "Servidor SECUNDARIO selecionado."; break ;;
      *) warn "Escolha 1 ou 2." ;;
    esac
  done
}

ask_peer() {
  printf "\n${C_BOLD}3) IP do outro servidor DNS (sincronizacao)${C_RESET}\n"
  if [[ "${SERVER_ROLE}" == "primary" ]]; then
    printf "   Informe o IP do SECUNDARIO (destino de NOTIFY/AXFR).\n"
  else
    printf "   Informe o IP do PRIMARIO (origem da zona RPZ).\n"
  fi
  while :; do
    ask PEER_IP "   IP do par DNS"
    valid_ip "${PEER_IP}" && break
    warn "IP invalido."
  done
}

ask_admin() {
  printf "\n${C_BOLD}4) Credenciais do painel administrativo${C_RESET}\n"
  while :; do
    ask ADMIN_USER "   Usuario administrador" "admin"
    [[ "${ADMIN_USER}" =~ ^[A-Za-z0-9._-]{3,32}$ ]] && break
    warn "Use de 3 a 32 caracteres: letras, numeros, ponto, hifen ou underline."
  done
  local p1 p2
  while :; do
    read -r -s -p "   Senha (min. 8 caracteres): " p1; printf "\n"
    read -r -s -p "   Confirme a senha: " p2; printf "\n"
    if [[ ${#p1} -lt 8 ]]; then warn "Senha muito curta."; continue; fi
    if [[ "${p1}" != "${p2}" ]]; then warn "As senhas nao conferem."; continue; fi
    ADMIN_PASS="${p1}"; break
  done
  ok "Credenciais definidas para '${ADMIN_USER}'."
}




detect_sinkhole_ip() {
  # O IP da pagina de bloqueio e o proprio IP deste servidor Debian.
  local ip
  ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '/src/ {for(i=1;i<=NF;i++) if($i=="src") print $(i+1); exit}')"
  if [[ -z "${ip}" ]]; then
    ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
  fi
  if [[ -n "${ip}" ]] && valid_ip "${ip}"; then
    SINKHOLE_IP="${ip}"
    ok "IP da pagina de bloqueio detectado automaticamente: ${SINKHOLE_IP}"
    return 0
  fi
  warn "Nao foi possivel detectar o IP deste servidor automaticamente."
  while :; do
    ask SINKHOLE_IP "   IPv4 deste servidor (pagina de bloqueio)"
    valid_ip "${SINKHOLE_IP}" && break
    warn "IP invalido."
  done
}

review() {
  printf "\n${C_BOLD}Resumo da configuracao${C_RESET}\n"
  printf "  Blocos do provedor : %s\n" "${ACL_LIST[*]:-(somente redes privadas)}"
  printf "  Redes privadas     : liberadas automaticamente\n"
  printf "  Papel              : %s\n" "${SERVER_ROLE}"
  printf "  Par DNS            : %s\n" "${PEER_IP}"
  printf "  Usuario do painel  : %s\n" "${ADMIN_USER}"
  printf "  Atualizacoes       : servico de licencas (%s)\n" "${LICENCA_URL}"
  printf "  Sinkhole (este IP) : %s\n" "${SINKHOLE_IP}"
  printf "\n  Dados do provedor (nome fantasia, razao social, CNPJ, e-mail de suporte\n"
  printf "  e URL de retorno) sao definidos depois no Painel > Configuracoes.\n\n"
  local c
  read -r -p "Confirmar e instalar? [S/n]: " c || true
  [[ -z "${c}" || "${c}" =~ ^[SsYy]$ ]] || die "Instalacao cancelada pelo operador."
}

# -------------------------------------------------------------------- pacotes

install_packages() {
  info "Instalando pacotes do sistema (pode levar alguns minutos)..."
  export DEBIAN_FRONTEND=noninteractive
  apt-get update -qq >>"${LOG_FILE}" 2>&1
  apt-get install -y -qq \
    bind9 bind9utils bind9-dnsutils dnsutils \
    python3 python3-venv python3-pip \
    python3-requests python3-pypdf \
    sqlite3 nginx git curl ca-certificates rsync \
    >>"${LOG_FILE}" 2>&1
  ok "Pacotes instalados."
}

# ----------------------------------------------------------- caminhos do BIND9
# Debian 11 mantem utilitarios em /usr/sbin; Debian 12/13 os movem para /usr/bin.
# Detectamos em tempo de instalacao para evitar 'No such file or directory'.
RNDC_BIN=""
NAMED_CHECKZONE_BIN=""
NAMED_BIN=""

detect_bind_bins() {
  local name path
  for name in rndc named-checkzone named; do
    path="$(command -v "${name}" 2>/dev/null || true)"
    if [[ -z "${path}" ]]; then
      for d in /usr/sbin /usr/bin /sbin /bin; do
        [[ -x "${d}/${name}" ]] && path="${d}/${name}" && break
      done
    fi
    case "${name}" in
      rndc) RNDC_BIN="${path:-/usr/sbin/rndc}" ;;
      named-checkzone) NAMED_CHECKZONE_BIN="${path:-/usr/sbin/named-checkzone}" ;;
      named) NAMED_BIN="${path:-/usr/sbin/named}" ;;
    esac
  done
  ok "Utilitarios BIND detectados: rndc=${RNDC_BIN} named-checkzone=${NAMED_CHECKZONE_BIN} named=${NAMED_BIN}"
}


# ------------------------------------------------------------------- aplicacao

deploy_app() {
  info "Publicando a aplicacao em ${APP_DIR}..."
  mkdir -p "${REPO_DIR}" "${DATA_DIR}" "${CONF_DIR}" "${WWW_DIR}"

  if [[ "${SRC_DIR}" != "${APP_DIR}" ]]; then
    mkdir -p "${APP_DIR}"
    rsync -a --delete \
      --exclude 'venv' --exclude '__pycache__' --exclude '*.pyc' \
      "${SRC_DIR}/" "${APP_DIR}/"
    # mantem o repositorio git no diretorio raiz para o updater
    if [[ -d "${SRC_DIR}/../.git" && ! -d "${REPO_DIR}/.git" ]]; then
      rsync -a "${SRC_DIR}/../.git" "${REPO_DIR}/"
      cp -f "${SRC_DIR}/../CHANGELOG.md" "${REPO_DIR}/" 2>/dev/null || true
      cp -f "${SRC_DIR}/../README.md" "${REPO_DIR}/" 2>/dev/null || true
      cp -f "${SRC_DIR}/../requirements.txt" "${REPO_DIR}/" 2>/dev/null || true
    fi
  fi
  cp -f "${APP_DIR}/requirements.txt" "${REPO_DIR}/requirements.txt"
  ok "Arquivos publicados."

  info "Criando ambiente virtual Python..."
  [[ -d "${VENV_DIR}" ]] || python3 -m venv "${VENV_DIR}"
  "${VENV_DIR}/bin/pip" install --quiet --upgrade pip >>"${LOG_FILE}" 2>&1
  "${VENV_DIR}/bin/pip" install --quiet -r "${APP_DIR}/requirements.txt" >>"${LOG_FILE}" 2>&1
  ok "Dependencias Python instaladas."
}

write_config() {
  info "Gravando ${CONF_FILE}..."
  local secret
  secret="$(python3 -c 'import secrets;print(secrets.token_hex(32))')"
  if [[ -f "${CONF_FILE}" ]]; then
    local old
    old="$(awk -F' = ' '/^secret_key/ {print $2}' "${CONF_FILE}" | tail -n1)"
    [[ -n "${old}" ]] && secret="${old}"
  fi

  cat >"${CONF_FILE}" <<EOF
; DNS Shield ISP ${APP_VERSION} - configuracao gerada pelo install.sh
; Desenvolvedor: Euquias Junior <junior@jbinfo.com.br>
[app]
secret_key = ${secret}
install_dir = ${REPO_DIR}
github_repo = ${GITHUB_REPO}
licenca_url = ${LICENCA_URL}

[database]
db_path = ${DB_FILE}

[dns]
server_role = ${SERVER_ROLE}
peer_ip = ${PEER_IP}
rpz_zone = ${RPZ_ZONE}
rpz_file = ${RPZ_FILE}
sinkhole_ip = ${SINKHOLE_IP}
sinkhole_ipv6 = ${SINKHOLE_IP6}
sinkhole_root = ${WWW_DIR}
rndc_bin = ${RNDC_BIN}
named_checkzone_bin = ${NAMED_CHECKZONE_BIN}
bind_service = $(bind_unit)
panel_service = dnsshield
EOF
  chmod 640 "${CONF_FILE}"
  ok "Configuracao gravada."
}


init_database() {
  info "Preparando banco SQLite em ${DB_FILE}..."
  install -d -m 750 /var/log/dnsshield
  local existed="no"
  [[ -f "${DB_FILE}" ]] && existed="yes"
  sqlite3 "${DB_FILE}" < "${APP_DIR}/db_schema.sql"
  chmod 640 "${DB_FILE}"

  # Auto-reparo/migracao de bancos criados por versoes anteriores (1.0.x-1.3.x):
  # garante colunas e tabelas novas mesmo que uma migracao antiga tenha abortado.
  DNSSHIELD_DB="${DB_FILE}" DNSSHIELD_CONFIG="${CONF_FILE}" DNSSHIELD_APP="${APP_DIR}" \
    "${VENV_DIR}/bin/python" - <<'PY' >>"${LOG_FILE}" 2>&1 || \
    warn "Migracao do banco reportou avisos. Consulte ${LOG_FILE}."
import os, sys
sys.path.insert(0, os.environ["DNSSHIELD_APP"])
from dnsshield.db import connect, migrate
conn = connect()
print("schema versao final:", migrate(conn))
conn.close()
PY



  local hash
  hash="$("${VENV_DIR}/bin/python" - "${ADMIN_PASS}" <<'PY'
import bcrypt, sys
print(bcrypt.hashpw(sys.argv[1].encode(), bcrypt.gensalt()).decode())
PY
)"

  "${VENV_DIR}/bin/python" - "${DB_FILE}" "${ADMIN_USER}" "${hash}" <<'PY'
import sqlite3, sys
db, user, pwd_hash = sys.argv[1:4]
conn = sqlite3.connect(db)
conn.execute(
    "INSERT INTO users (username, password_hash, role) VALUES (?, ?, 'admin') "
    "ON CONFLICT(username) DO UPDATE SET password_hash = excluded.password_hash, active = 1",
    (user, pwd_hash),
)
conn.execute(
    "INSERT INTO audit_log (username, action, detail, ip_address) VALUES (?, ?, ?, 'localhost')",
    (user, "instalacao", "Instalacao/reconfiguracao concluida pelo install.sh"),
)
conn.commit()
conn.close()
PY
  if [[ "${existed}" == "yes" ]]; then
    ok "Banco existente preservado (dados de bloqueios mantidos)."
  else
    ok "Banco criado e populado."
  fi
}

# ----------------------------------------------------------------------- BIND9

acl_block() {
  local out=""
  local net
  # redes privadas embutidas + blocos publicos informados pelo operador
  for net in "${PRIVATE_NETS[@]}" ${ACL_LIST[@]+"${ACL_LIST[@]}"}; do
    out+="    ${net};"$'\n'
  done
  printf '%s' "${out}"
}

setup_tsig() {
  if [[ ! -f "${TSIG_FILE}" ]]; then
    info "Gerando chave TSIG para transferencia de zona..."
    local secret
    secret="$(openssl rand -base64 32)"
    cat >"${TSIG_FILE}" <<EOF
key "dnsshield-transfer" {
    algorithm hmac-sha256;
    secret "${secret}";
};
EOF
    chown root:bind "${TSIG_FILE}"
    chmod 640 "${TSIG_FILE}"
    warn "Copie ${TSIG_FILE} para o outro servidor (mesma chave nos dois lados)."
  else
    ok "Chave TSIG existente reutilizada."
  fi
}

# Unidade systemd do BIND: Debian 12/13 usa named.service (bind9.service e alias).
bind_unit() {
  if systemctl list-unit-files 2>/dev/null | grep -q '^named\.service'; then
    printf 'named'
  else
    printf 'bind9'
  fi
}

bind_diag() {
  local unit; unit="$(bind_unit)"
  local named_bin="${NAMED_BIN:-/usr/sbin/named}"
  printf "\n${C_BOLD}Diagnostico do BIND9${C_RESET}\n"
  {
    echo "--- journalctl -u ${unit} (ultimas 30 linhas) ---"
    journalctl -u "${unit}" -n 30 --no-pager 2>&1 || true
    echo "--- ${named_bin} -g (foreground, 5s) ---"
    timeout 5 "${named_bin}" -g -u bind 2>&1 | tail -n 30 || true
  } | tee -a "${LOG_FILE}"
}


setup_bind_logdir() {
  # O named roda como usuario 'bind' e nao escreve em /var/log; o AppArmor tambem
  # so permite /var/log/named/. Sem isso o canal de log e fatal e o servico morre.
  install -d -o bind -g bind -m 750 /var/log/named
  if [[ -d /etc/apparmor.d ]]; then
    mkdir -p /etc/apparmor.d/local
    local local_profile=/etc/apparmor.d/local/usr.sbin.named
    touch "${local_profile}"
    if ! grep -q '/var/log/named/' "${local_profile}" 2>/dev/null; then
      printf '# DNS Shield ISP\n  /var/log/named/** rw,\n  /var/log/named/ rw,\n' \
        >>"${local_profile}"
    fi
    if command -v apparmor_parser >/dev/null 2>&1 && [[ -f /etc/apparmor.d/usr.sbin.named ]]; then
      apparmor_parser -r /etc/apparmor.d/usr.sbin.named >>"${LOG_FILE}" 2>&1 || true
    fi
  fi
}

configure_bind() {
  info "Configurando o BIND9..."
  mkdir -p "${RPZ_DIR}"
  setup_bind_logdir
  local ts
  ts="$(date +%Y%m%d%H%M%S)"
  for f in /etc/bind/named.conf.options /etc/bind/named.conf.local; do
    [[ -f "${f}" && ! -f "${f}.dnsshield.bak" ]] && cp -a "${f}" "${f}.dnsshield.bak.${ts}"
  done

  setup_tsig

  local transfer_acl="key \"dnsshield-transfer\";"
  local notify_block="    notify no;"
  if [[ -n "${PEER_IP}" ]]; then
    transfer_acl="key \"dnsshield-transfer\"; ${PEER_IP};"
    notify_block="    also-notify { ${PEER_IP}; };
    notify yes;"
  fi

  cat >/etc/bind/named.conf.options <<EOF
// DNS Shield ISP ${APP_VERSION} - resolver recursivo seguro
// Gerado por install.sh. Desenvolvedor: Euquias Junior <junior@jbinfo.com.br>

// A chave TSIG e incluida aqui (antes das options) porque 'allow-transfer'
// referencia a chave e o named.conf.local e carregado depois.
include "${TSIG_FILE}";

acl "clientes-autorizados" {
    localhost;
    localnets;
$(acl_block)};

options {
    directory "/var/cache/bind";

    recursion yes;
    allow-query { clientes-autorizados; };
    allow-recursion { clientes-autorizados; };
    allow-query-cache { clientes-autorizados; };
    allow-transfer { ${transfer_acl} };
    allow-update { none; };

    version none;
    hostname none;
    server-id none;

    dnssec-validation auto;
    auth-nxdomain no;
    minimal-responses yes;
    max-cache-size 60%;
    max-cache-ttl 86400;
    max-ncache-ttl 3600;

    rate-limit {
        responses-per-second 25;
        window 5;
        exempt-clients { clientes-autorizados; };
    };

    listen-on { any; };
    listen-on-v6 { any; };

    // Politica RPZ: bloqueio judicial
    response-policy {
        zone "${RPZ_ZONE}";
    } qname-wait-recurse no break-dnssec yes;
};

logging {
    channel dnsshield_rpz {
        file "/var/log/named/rpz.log" versions 5 size 20m;
        severity info;
        print-time yes;
        print-category yes;
    };
    category rpz { dnsshield_rpz; };
};
EOF

  if [[ "${SERVER_ROLE}" == "primary" ]]; then
    if [[ ! -f "${RPZ_FILE}" ]]; then
      cat >"${RPZ_FILE}" <<EOF
; DNS Shield ISP - zona RPZ gerada automaticamente. NAO EDITE MANUALMENTE.
; Desenvolvedor: Euquias Junior <junior@jbinfo.com.br>
\$TTL 60
@   IN  SOA localhost. admin.${RPZ_ZONE}. (
        $(date +%s) ; serial
        300        ; refresh
        60         ; retry
        604800     ; expire
        60 )       ; minimum
    IN  NS  localhost.
EOF
    fi
    chown root:bind "${RPZ_FILE}"
    chmod 644 "${RPZ_FILE}"

    cat >/etc/bind/named.conf.local <<EOF
// DNS Shield ISP ${APP_VERSION} - zona RPZ (PRIMARIO/MASTER)
// A chave TSIG ja e incluida em named.conf.options.

zone "${RPZ_ZONE}" {
    type master;
    file "${RPZ_FILE}";
    allow-query { localhost; };
    allow-transfer { ${transfer_acl} };
${notify_block}
};
EOF
  else
    cat >/etc/bind/named.conf.local <<EOF
// DNS Shield ISP ${APP_VERSION} - zona RPZ (SECUNDARIO/SLAVE)
// A chave TSIG ja e incluida em named.conf.options.


zone "${RPZ_ZONE}" {
    type slave;
    file "/var/cache/bind/db.${RPZ_ZONE}";
    masters { ${PEER_IP} key "dnsshield-transfer"; };
    allow-query { localhost; };
    allow-transfer { none; };
    notify no;
};
EOF
  fi

  info "Validando configuracao do BIND9..."
  named-checkconf || die "named-checkconf falhou. Backups em /etc/bind/*.dnsshield.bak.*"
  if [[ "${SERVER_ROLE}" == "primary" ]]; then
    named-checkzone "${RPZ_ZONE}" "${RPZ_FILE}" >>"${LOG_FILE}" 2>&1 \
      || die "named-checkzone falhou para ${RPZ_FILE}."
  fi

  local unit; unit="$(bind_unit)"
  systemctl enable "${unit}" >>"${LOG_FILE}" 2>&1 || true
  if ! systemctl restart "${unit}" >>"${LOG_FILE}" 2>&1; then
    bind_diag
    die "O servico ${unit} nao subiu. Veja o diagnostico acima e ${LOG_FILE}. \
Para voltar ao estado anterior, restaure os backups em /etc/bind/*.dnsshield.bak.*"
  fi
  ok "BIND9 configurado e ativo."
}

# ------------------------------------------------------------ sinkhole (Nginx)

configure_nginx() {
  info "Configurando a pagina de bloqueio no Nginx (porta 80)..."
  mkdir -p "${WWW_DIR}"

  # A pagina inicial e generica: os dados do provedor (nome fantasia, razao social,
  # CNPJ, e-mail de suporte e URL de retorno) sao definidos no Painel > Configuracoes,
  # que regrava este arquivo automaticamente ao salvar.
  ( cd "${APP_DIR}" && DNSSHIELD_CONFIG="${CONF_FILE}" "${VENV_DIR}/bin/python" - <<'PY'
from dnsshield import create_app
from dnsshield.db import get_provider
from dnsshield.routes.settings import write_sinkhole

app = create_app()
with app.test_request_context():
    error = write_sinkhole(get_provider())
    print(f"aviso ao gravar a pagina de bloqueio: {error}" if error else "pagina de bloqueio gravada")
PY
  ) >>"${LOG_FILE}" 2>&1 || warn "Nao foi possivel gerar a pagina de bloqueio (veja ${LOG_FILE})."

  install -m 644 "${APP_DIR}/nginx/dnsshield-sinkhole.conf" \
    /etc/nginx/sites-available/dnsshield-sinkhole.conf
  ln -sf /etc/nginx/sites-available/dnsshield-sinkhole.conf \
    /etc/nginx/sites-enabled/dnsshield-sinkhole.conf
  rm -f /etc/nginx/sites-enabled/default

  nginx -t >>"${LOG_FILE}" 2>&1 || die "nginx -t falhou. Consulte ${LOG_FILE}."
  systemctl enable nginx >>"${LOG_FILE}" 2>&1 || true
  systemctl restart nginx
  ok "Pagina de bloqueio publicada em http://${SINKHOLE_IP}/"
}

# ------------------------------------------------------------- painel (systemd)

configure_panel() {
  if [[ "${SERVER_ROLE}" != "primary" ]]; then
    info "Servidor secundario: painel web nao instalado (apenas sincronizacao)."
    systemctl disable --now dnsshield >/dev/null 2>&1 || true
    return 0
  fi
  info "Instalando servico systemd do painel..."
  install -m 644 "${APP_DIR}/systemd/dnsshield.service" /etc/systemd/system/dnsshield.service
  systemctl daemon-reload
  systemctl enable dnsshield >>"${LOG_FILE}" 2>&1
  systemctl restart dnsshield
  sleep 3
  if systemctl is-active --quiet dnsshield; then
    ok "Painel administrativo ativo na porta 8080."
  else
    warn "O painel nao subiu. Verifique: journalctl -u dnsshield -n 50"
  fi
}

configure_sync_cron() {
  [[ "${SERVER_ROLE}" == "primary" ]] || return 0
  info "Instalando agendamentos de sincronizacao (AnaBlock e Federacao)..."
  install -d -m 750 /var/log/dnsshield
  cat >/etc/cron.d/dnsshield-anablock <<EOF
# DNS Shield ISP ${APP_VERSION} - sincronizacao automatica com o AnaBlock.
# Executa a cada 15 min; o script respeita a frequencia definida no painel.
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
*/15 * * * * root ${VENV_DIR}/bin/python ${APP_DIR}/scripts/anablock-sync.py >>/var/log/dnsshield/anablock.log 2>&1
EOF
  chmod 644 /etc/cron.d/dnsshield-anablock

  cat >/etc/cron.d/dnsshield-federacao <<EOF
# DNS Shield ISP ${APP_VERSION} - sincronizacao com provedores parceiros (federacao).
# Executa a cada 15 min; o script respeita a frequencia definida no painel.
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
*/15 * * * * root ${VENV_DIR}/bin/python ${APP_DIR}/scripts/federacao-sync.py >>/var/log/dnsshield/federacao.log 2>&1
EOF
  chmod 644 /etc/cron.d/dnsshield-federacao

  cat >/etc/cron.d/dnsshield-licenca <<EOF
# DNS Shield ISP ${APP_VERSION} - revalidacao diaria da licenca (contrato ativo).
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
27 4 * * * root ${VENV_DIR}/bin/python ${APP_DIR}/scripts/licenca-check.py >>/var/log/dnsshield/licenca.log 2>&1
EOF
  chmod 644 /etc/cron.d/dnsshield-licenca
  ok "Agendamentos instalados (configure licenca, token do AnaBlock e parceiros no painel)."

}

sync_initial_zone() {
  [[ "${SERVER_ROLE}" == "primary" ]] || return 0
  info "Sincronizando a zona RPZ com o banco..."
  ( cd "${APP_DIR}" && DNSSHIELD_CONFIG="${CONF_FILE}" "${VENV_DIR}/bin/python" - <<'PY'
from dnsshield import create_app
from dnsshield import rpz

app = create_app()
with app.test_request_context():
    try:
        rpz.sync_zone()
        print("zona sincronizada")
    except rpz.RPZError as exc:
        print(f"aviso: {exc}")
PY
  ) >>"${LOG_FILE}" 2>&1 || warn "Sincronizacao inicial da zona reportou aviso (veja ${LOG_FILE})."
  ok "Zona RPZ sincronizada."
}

summary() {
  local host_ip="${SINKHOLE_IP}"
  printf "\n${C_GREEN}${C_BOLD}Instalacao concluida com sucesso!${C_RESET}\n\n"
  printf "  Papel do servidor  : %s\n" "$([[ "${SERVER_ROLE}" == primary ]] && echo 'Primario (master + painel)' || echo 'Secundario (slave)')"
  if [[ "${SERVER_ROLE}" == "primary" ]]; then
    printf "  Painel web         : http://%s:8080\n" "${host_ip}"
    printf "  Usuario            : %s\n" "${ADMIN_USER}"
  fi
  printf "  Pagina de bloqueio : http://%s/\n" "${host_ip}"
  printf "  Zona RPZ           : %s (%s)\n" "${RPZ_ZONE}" "${RPZ_FILE}"
  printf "  Configuracao       : %s\n" "${CONF_FILE}"
  printf "  Banco de dados     : %s\n" "${DB_FILE}"
  printf "  Chave TSIG         : %s (replique no outro servidor)\n" "${TSIG_FILE}"
  printf "  Log da instalacao  : %s\n\n" "${LOG_FILE}"
  if [[ "${SERVER_ROLE}" == "primary" ]]; then
    printf "  ${C_BOLD}Proximos passos no painel (http://%s:8080):${C_RESET}\n" "${host_ip}"
    printf "    1. Configuracoes > Dados do provedor (nome fantasia, CNPJ, e-mail de\n"
    printf "       suporte ao assinante e URL de retorno da pagina de bloqueio).\n"
    printf "    2. Configuracoes > Integracao AnaBlock (token e frequencia) - requer licenca.\n"
    printf "    3. Federacao entre provedores (publicar/consumir listas de parceiros) - requer licenca.\n"
    printf "    4. Atualizacoes do sistema sao gratuitas em Configuracoes > Atualizacoes.\n\n"
  fi

  printf "  Teste rapido:\n"
  printf "    dig @%s google.com +short\n" "${host_ip}"
  printf "    dig @%s <dominio-bloqueado> +short   # deve retornar %s\n\n" "${host_ip}" "${host_ip}"
  printf "  %s %s - Euquias Junior <junior@jbinfo.com.br>\n\n" "${APP_NAME}" "${APP_VERSION}"
}

main() {
  : >"${LOG_FILE}" 2>/dev/null || true
  banner
  require_root
  require_debian
  check_connectivity

  if [[ -f "${CONF_FILE}" ]]; then
    warn "Instalacao existente detectada em ${CONF_DIR}."
    warn "Prosseguir vai reconfigurar o sistema PRESERVANDO o banco ${DB_FILE}."
    local c
    read -r -p "Continuar? [S/n]: " c || true
    [[ -z "${c}" || "${c}" =~ ^[SsYy]$ ]] || die "Instalacao cancelada."
  fi

  ask_networks
  ask_role
  ask_peer
  ask_admin
  detect_sinkhole_ip
  review

  install_packages
  detect_bind_bins
  deploy_app
  write_config
  init_database
  configure_bind
  configure_nginx
  configure_panel
  configure_sync_cron
  sync_initial_zone
  summary

}

main "$@"
