#!/bin/bash
#
# Split-horizon ("local") DNS.
#
# Points every configured domain at the LibrePortal server's LAN IP inside the
# self-hosted resolver, so app subdomains resolve to the box on the local
# network and hit Traefik directly — with valid Let's Encrypt certs — instead
# of bouncing out to the public IP (which most home routers can't hairpin).
#
# Domain-level, single-host model: every domain -> the one server IP. Traefik
# sorts out which app by Host header. Adding apps/domains later is covered
# automatically (AdGuard wildcard) or on the next run (Pi-hole host list).
#
# SAFE BY CONSTRUCTION: every action is idempotent, guarded by an "installed"
# check, and cannot corrupt the resolver if a detail is off —
#   * AdGuard goes through its REST API; a bad URL/cred just fails harmlessly.
#   * Pi-hole writes ONLY to the supported, mounted /etc/pihole/custom.list,
#     inside a clearly-marked managed block (never touches /etc/dnsmasq.d).
# NOTE: the AdGuard API call and the Pi-hole reload need a live smoke-test —
# they can't be exercised without the running containers.

# The server's LAN IP (the source address used to reach the network).
localDnsServerIp() {
    local ip
    ip=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')
    [[ -z "$ip" ]] && ip="${public_ip_v4:-}"     # last-resort fallback
    printf '%s' "$ip"
}

# Configured (non-empty) domains, one per line.
localDnsDomains() {
    local i d
    for i in 1 2 3 4 5 6 7 8 9; do
        d="CFG_DOMAIN_${i}"; d="${!d:-}"
        [[ -n "$d" ]] && printf '%s\n' "$d"
    done
}

# Every app host (subdomain.domain) across all apps' Traefik-managed ports —
# for resolvers without wildcard support (Pi-hole custom.list). Mirrors the
# per-port host rule: subdomain ""/@/root -> apex, else <subdomain>.<domain>.
localDnsAppHosts() {
    local cfg app up d_idx dom portv sub host
    local -a parts
    for cfg in "${containers_dir}"*/*.config; do
        [[ -f "$cfg" ]] || continue
        app=$(basename "$cfg" .config); up=${app^^}
        d_idx=$(grep -oE "CFG_${up}_DOMAIN=[0-9]+" "$cfg" | head -1 | cut -d= -f2)
        [[ -z "$d_idx" ]] && continue
        dom="CFG_DOMAIN_${d_idx}"; dom="${!dom:-}"
        [[ -z "$dom" ]] && continue
        while IFS= read -r portv; do
            IFS='|' read -ra parts <<< "$portv"
            [[ "${parts[6]:-}" == "true" ]] || continue          # Traefik-managed only
            sub="${parts[10]:-}"
            if [[ "$sub" == "@" || "$sub" == "root" ]]; then
                host="$dom"                          # apex (explicit only)
            elif [[ -n "$sub" ]]; then
                host="${sub}.${dom}"
            else
                host="${app}.${dom}"                 # no subdomain set -> app-name default
            fi
            printf '%s\n' "$host"
        done < <(grep -oE "CFG_${up}_PORT_[0-9]+=\"[^\"]*\"" "$cfg" | sed -E 's/^[^"]*"//; s/"$//')
    done | sort -u
}

# AdGuard Home: wildcard rewrite per domain (covers every subdomain) + apex,
# via the REST API. Idempotent (delete-then-add). Admin port discovered at
# runtime via `docker port`, creds from the saved CFG_ADGUARD_* values.
localDnsApplyAdguard() {
    local ip="$1"; shift
    local user="${CFG_ADGUARD_ADMIN_USER:-admin}"
    local pass="${CFG_ADGUARD_ADMIN_PASSWORD:-${CFG_ADGUARD_PASSWORD:-}}"
    local hostport base d entry payload
    hostport=$(dockerCommandRun "docker port adguard-service 3000/tcp" 2>/dev/null | head -1 | sed 's/.*://')
    if [[ -z "$hostport" ]]; then
        isNotice "AdGuard admin port not found (running?). Skipping AdGuard rewrites."
        return 0
    fi
    base="http://127.0.0.1:${hostport}"
    for d in "$@"; do
        for entry in "*.${d}" "${d}"; do
            payload="{\"domain\":\"${entry}\",\"answer\":\"${ip}\"}"
            curl -fsS -u "${user}:${pass}" -H 'Content-Type: application/json' \
                 -X POST "${base}/control/rewrite/delete" -d "$payload" >/dev/null 2>&1
            if curl -fsS -u "${user}:${pass}" -H 'Content-Type: application/json' \
                 -X POST "${base}/control/rewrite/add" -d "$payload" >/dev/null 2>&1; then
                isSuccessful "AdGuard rewrite: ${entry} -> ${ip}"
            else
                isNotice "AdGuard rewrite for ${entry} not applied (check API URL/creds) — safe to retry."
            fi
        done
    done
}

# Pi-hole: per-host A records in the mounted, supported custom.list, inside a
# managed block (no wildcards on Pi-hole; no touching /etc/dnsmasq.d). Reloads
# via `pihole restartdns`, falling back to a container restart.
localDnsApplyPihole() {
    local ip="$1"
    local list="${containers_dir}pihole/pihole-dnsmasq-unbound/custom.list"   # mounts to /etc/pihole
    local b="# >>> libreportal-local >>>" e="# <<< libreportal-local <<<"
    local hosts tmp h n
    hosts=$(localDnsAppHosts)
    if [[ -z "$hosts" ]]; then isNotice "No app hosts for Pi-hole — skipping."; return 0; fi
    # $list lives under containers/pihole (docker-install-owned): read/write it
    # via the container-owner helpers; build the new content in a manager /tmp
    # scratch in between.
    tmp=$(mktemp)
    if runFileOp test -f "$list"; then               # preserve anything outside our block
        runFileOp cat "$list" | awk -v b="$b" -v e="$e" '$0==b{skip=1} !skip{print} $0==e{skip=0}' > "$tmp"
    fi
    {
        echo "$b"
        while IFS= read -r h; do [[ -n "$h" ]] && echo "${ip} ${h}"; done <<< "$hosts"
        echo "$e"
    } >> "$tmp"
    runFileWrite "$list" < "$tmp"
    rm -f "$tmp"
    n=$(printf '%s\n' "$hosts" | grep -c .)
    dockerCommandRun "docker exec pihole-service pihole restartdns" >/dev/null 2>&1 || dockerComposeRestart pihole
    isSuccessful "Pi-hole custom.list updated: ${n} hosts -> ${ip}"
}

# Orchestrator — call after a domain change or app install.
setupLocalDnsRewrites() {
    isHeader "Local (split-horizon) DNS"
    local ip domains
    ip=$(localDnsServerIp)
    [[ -z "$ip" ]] && { isNotice "Could not determine the server LAN IP — skipping local DNS."; return 0; }
    domains=$(localDnsDomains)
    [[ -z "$domains" ]] && { isNotice "No domains configured (CFG_DOMAIN_*) — skipping local DNS."; return 0; }
    isNotice "Server IP ${ip} · domains: $(printf '%s ' $domains)"

    local did=0
    if [[ "$(dockerCheckAppInstalled "adguard" "docker")" == "installed" ]]; then
        localDnsApplyAdguard "$ip" $domains; did=1
    fi
    if [[ "$(dockerCheckAppInstalled "pihole" "docker")" == "installed" ]]; then
        localDnsApplyPihole "$ip"; did=1
    fi
    [[ "$did" -eq 0 ]] && isNotice "Neither AdGuard nor Pi-hole installed — local DNS skipped."
}
