#!/usr/bin/env bash

readonly SCRIPT_NAME=$(basename "$0")
readonly DEPENDENCIES=("curl" "dig" "jq" "column")

readonly COLOR_WHITE="\033[97m"
readonly COLOR_RED="\033[31m"
readonly COLOR_GREEN="\033[32m"
readonly COLOR_BLUE="\033[36m"
readonly COLOR_ORANGE="\033[33m"
readonly COLOR_RESET="\033[0m"
readonly CURL_SEPARATOR="--UNIQUE-SEPARATOR--"

readonly DNS_SERVERS=("1.1.1.1" "1.0.0.1" "8.8.8.8" "8.8.4.4" "9.9.9.9" "9.9.9.10" "77.88.8.8" "77.88.8.1")
readonly ENCRYPTED_DNS_SERVERS=(
  "Cloudflare|1.1.1.1"
  "Cloudflare|1.0.0.1"
  "Google|8.8.8.8"
  "Google|8.8.4.4"
  "Quad9|9.9.9.9"
  "Quad9|9.9.9.10"
  "Yandex|77.88.8.8"
  "Yandex|77.88.8.1"
)

AVAILABLE_DOH=()
AVAILABLE_DOT=()

# Default values
TIMEOUT=5
RETRIES=2
MODE="both"
USER_AGENT="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0"
DOMAINS_FILE=""
IP_VERSION="4"
PROXY=""
SINGLE_DOMAIN=""
PROTOCOL="both"
JSON_OUTPUT=false
DNS_CHECK=true
SHOW_HEADER=true

readonly DPI_BLOCKED_SITES=(
  "youtube.com"
  "redirector.googlevideo.com/report_mapping?di=no"
  "discord.com"
  "instagram.com"
  "facebook.com"
  "x.com"
  "linkedin.com"
  "rutracker.org"
  "digitalocean.com"
  # "ntc.party"
  "amnezia.org"
  "getoutline.org"
  "mailfence.com"
  "flibusta.is"
  "rezka.ag"
  "api.telegram.org"
  "play.google.com"
)

readonly GEO_BLOCKED_SITES=(
  "spotify.com"
  "netflix.com"
  "patreon.com"
  "swagger.io"
  "snyk.io"
  "mongodb.com"
  "autodesk.com"
  "graylog.org"
  "redis.io"
  "copilot.microsoft.com"
)

readonly MSG_AVAILABLE="Available"
readonly MSG_BLOCKED="Blocked"
readonly MSG_TIMEOUT="$MSG_BLOCKED / Timeout"
readonly MSG_BLOCKED_BY_IP="$MSG_BLOCKED by IP"
readonly MSG_BLOCKED_BY_PORT="$MSG_BLOCKED by port"
readonly MSG_REDIRECT="Redirected"
readonly MSG_ACCESS_DENIED="Denied"
readonly MSG_OTHER="Responded with status code"

declare -a TEXT_RESULTS=()

error_exit() {
  local message="$1"
  local exit_code="${2:-1}"
  printf "[%b%s%b] %b%s%b\n" "$COLOR_RED" "ERROR" "$COLOR_RESET" "$COLOR_WHITE" "$message" "$COLOR_RESET" >&2
  exit "$exit_code"
}

show_progress() {
  local current=$1
  local total=$2
  local domain=$3
  local width=24
  local filled=0 pct=0 empty=0
  local label="$domain"
  local bar

  if [ "$total" -gt 0 ]; then
    filled=$((current * width / total))
    pct=$((current * 100 / total))
  fi
  [ "$filled" -gt "$width" ] && filled=$width
  empty=$((width - filled))

  bar="$(printf '%*s' "$filled" '' | tr ' ' '=')"
  if [ "$empty" -gt 0 ] && [ "$filled" -lt "$width" ]; then
    bar="${bar}>"
    empty=$((empty - 1))
  fi
  bar="${bar}$(printf '%*s' "$empty" '')"

  if [ "${#label}" -gt 42 ]; then
    label="${label:0:39}..."
  fi

  # stderr: JSON на stdout не ломаем даже в --json
  printf "\r\033[K%b[%s]%b %3d%%  %d/%d  %b%s%b" \
    "$COLOR_BLUE" \
    "$bar" \
    "$COLOR_RESET" \
    "$pct" \
    "$current" \
    "$total" \
    "$COLOR_WHITE" \
    "$label" \
    "$COLOR_RESET" >&2
}

clear_progress() {
  printf "\n" >&2
}

cleanup() {
  clear_progress
  exit 130
}

display_help() {
  cat <<EOF

Usage: $SCRIPT_NAME [OPTIONS]

Checks accessibility of websites that might be blocked by DPI or geolocation restrictions

Options:
  -h, --help         Display this help message and exit
  -m, --mode         Set checking mode: 'dpi', 'geoblock', 'both' (dpi + geoblock), or 'dns' only (default: $MODE)
  -t, --timeout      Set connection timeout in seconds (default: $TIMEOUT)
  -r, --retries      Set number of connection retries (default: $RETRIES)
  -u, --user-agent   Set custom User-Agent string (default: $USER_AGENT)
  -f, --file         Read domains from specified file instead of using built-in lists
  -6, --ipv6         Use IPv6 (default: IPv$IP_VERSION)
  -p, --proxy        Use SOCKS5 proxy (format: host:port)
  -d, --domain       Specify a single domain to check
  --http-only        Test only HTTP
  --https-only       Test only HTTPS
  --no-dns           Skip DNS checks (DoH/DoT availability and hijacking detection)
  --no-header        Do not print the script header with current settings
  -j, --json         Output results in JSON format

Examples:
  $SCRIPT_NAME                               # Check all predefined domains with default settings
  $SCRIPT_NAME --mode dpi                    # Check only DPI-blocked sites
  $SCRIPT_NAME --timeout 10 --retries 3      # Use longer timeout and more retries
  $SCRIPT_NAME --user-agent "MyAgent/1.0"    # Use custom User-Agent
  $SCRIPT_NAME --file my-domains.txt         # Check domains from custom file
  $SCRIPT_NAME --ipv6                        # Use IPv6 instead of IPv4
  $SCRIPT_NAME --proxy 127.0.0.1:1080        # Use SOCKS5 proxy
  $SCRIPT_NAME --domain example.com          # Check a single domain
  $SCRIPT_NAME --http-only                   # Test only HTTP
  $SCRIPT_NAME --https-only                  # Test only HTTPS
  $SCRIPT_NAME --no-dns                      # Skip DNS checks
  $SCRIPT_NAME --no-header                   # Do not print the script header

The domain file should contain one domain per line. Lines starting with # are ignored
EOF
}

is_installed() {
  local cmd="$1"
  command -v "$cmd" >/dev/null 2>&1
}

check_missing_dependencies() {
  local missing_pkgs=()

  for pkg in "${DEPENDENCIES[@]}"; do
    if ! is_installed "$pkg"; then
      missing_pkgs+=("$pkg")
    fi
  done

  echo "${missing_pkgs[@]}"
}

prompt_for_installation() {
  local missing_pkgs=("$@")

  echo "Missing dependencies: ${missing_pkgs[*]}"
  read -r -p "Do you want to install them? [y/N]: " answer
  answer=${answer,,}

  case "${answer,,}" in
    y | yes)
      return 0
      ;;
    *)
      exit 0
      ;;
  esac
}

get_package_manager() {
  # Check if the script is running in Termux
  if [[ -d /data/data/com.termux ]]; then
    echo "termux"
    return
  fi

  if [[ -f /etc/os-release ]]; then
    . /etc/os-release
    case "$ID" in
      debian | ubuntu)
        echo "apt"
        ;;
      arch)
        echo "pacman"
        ;;
      fedora)
        echo "dnf"
        ;;
      centos | rhel | rocky | almalinux)
        if command -v dnf >/dev/null 2>&1; then echo "dnf"; else echo "yum"; fi
        ;;
      *)
        error_exit "Unknown distribution: $ID. Please install dependencies manually."
        ;;
    esac
  else
    error_exit "File /etc/os-release not found, unable to determine distribution. Please install dependencies manually."
  fi
}

install_with_package_manager() {
  local pkg_manager="$1"
  local use_sudo=""
  local packages=()
  shift

  for dep in "$@"; do
    case "$pkg_manager" in
      apt | termux)
        case "$dep" in
          dig) packages+=("dnsutils") ;;
          column) packages+=("bsdextrautils") ;;
          *) packages+=("$dep") ;;
        esac
        ;;
      pacman)
        case "$dep" in
          dig) packages+=("bind") ;;
          column) packages+=("util-linux") ;;
          *) packages+=("$dep") ;;
        esac
        ;;
      dnf | yum)
        case "$dep" in
          dig) packages+=("bind-utils") ;;
          column) packages+=("util-linux") ;;
          *) packages+=("$dep") ;;
        esac
        ;;
      *)
        packages+=("$dep")
        ;;
    esac
  done

  if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
    use_sudo="sudo"
  fi

  case "$pkg_manager" in
    apt)
      $use_sudo apt update
      $use_sudo env NEEDRESTART_MODE=a apt install -y "${packages[@]}"
      ;;
    pacman)
      $use_sudo pacman -Syy --noconfirm "${packages[@]}"
      ;;
    dnf)
      $use_sudo dnf install -y "${packages[@]}"
      ;;
    yum)
      $use_sudo yum install -y "${packages[@]}"
      ;;
    termux)
      apt update
      apt install -y "${packages[@]}"
      ;;
    *)
      error_exit "Unknown package manager: $pkg_manager"
      ;;
  esac
}

install_dependencies() {
  local missing_packages
  local pkg_manager

  read -r -a missing_packages <<<"$(check_missing_dependencies)"

  if [[ ${#missing_packages[@]} -eq 0 ]]; then
    return 0
  fi

  if $JSON_OUTPUT; then
    echo "Missing dependencies: ${missing_packages[*]}" >&2
    exit 1
  fi

  prompt_for_installation "${missing_packages[@]}"

  pkg_manager=$(get_package_manager)
  install_with_package_manager "$pkg_manager" "${missing_packages[@]}"
}

check_ipv6_support() {
  if [[ -n $(ip -6 addr show scope global 2>/dev/null) ]]; then
    return 0
  fi

  return 1
}

parse_arguments() {
  while [[ $# -gt 0 ]]; do
    case $1 in
      -h | --help)
        display_help
        exit 0
        ;;
      -m | --mode)
        if [[ $2 == "dpi" || $2 == "geoblock" || $2 == "dns" || $2 == "both" ]]; then
          MODE=$2
        else
          error_exit "Invalid mode: $2. Valid modes are: dpi, geoblock, dns, both"
        fi
        shift 2
        ;;
      -t | --timeout)
        if [[ "$2" =~ ^[0-9]+$ ]]; then
          TIMEOUT=$2
        else
          error_exit "Invalid timeout value: $2. Timeout must be a positive integer"
        fi
        shift 2
        ;;
      -r | --retries)
        if [[ "$2" =~ ^[0-9]+$ ]]; then
          RETRIES=$2
        else
          error_exit "Invalid retries value: $2. Retry count must be a positive integer"
        fi
        shift 2
        ;;
      -u | --user-agent)
        if [[ -n "$2" ]]; then
          USER_AGENT=$2
        else
          error_exit "User-Agent cannot be empty"
        fi
        shift 2
        ;;
      -f | --file)
        if [[ -n "${2:-}" ]]; then
          if [[ -f "$2" ]]; then
            DOMAINS_FILE="$2"
          else
            error_exit "File '$2' does not exist"
          fi
        else
          error_exit "File path cannot be empty"
        fi
        shift 2
        ;;
      -6 | --ipv6)
        if ! check_ipv6_support; then
          error_exit "IPv6 is not supported on this system"
        fi

        IP_VERSION="6"
        shift
        ;;
      -p | --proxy)
        if [[ -n "${2:-}" ]]; then
          PROXY="$2"
        else
          error_exit "Proxy address cannot be empty"
        fi
        shift 2
        ;;
      -d | --domain)
        if [[ -n "$2" ]]; then
          SINGLE_DOMAIN="$2"
        else
          error_exit "Domain cannot be empty"
        fi
        shift 2
        ;;
      --http-only)
        PROTOCOL="http"
        shift
        ;;
      --https-only)
        PROTOCOL="https"
        shift
        ;;
      --no-dns)
        DNS_CHECK=false
        shift
        ;;
      --no-header)
        SHOW_HEADER=false
        shift
        ;;
      -j | --json)
        JSON_OUTPUT=true
        shift
        ;;
      *)
        error_exit "Unknown option: $1"
        ;;
    esac
  done

  if [[ "$MODE" == "dns" ]] && ! $DNS_CHECK; then
    error_exit "--no-dns cannot be used with --mode dns"
  fi
}

print_header() {
  local mode

  printf "\nTimeout set to: %b%ss%b\n" "$COLOR_WHITE" "$TIMEOUT" "$COLOR_RESET"

  case $MODE in
    dpi)
      mode="DPI"
      ;;
    geoblock)
      mode="Geoblock"
      ;;
    dns)
      mode="DNS"
      ;;
    both)
      mode="DPI and Geoblock"
      ;;
  esac

  if [[ -z "$DOMAINS_FILE" ]] && [[ -z "$SINGLE_DOMAIN" ]]; then
    printf "Mode set to: %b%s%b\n" "$COLOR_WHITE" "$mode" "$COLOR_RESET"
  fi

  if [[ "$MODE" == "dns" ]]; then
    return
  fi

  printf "Retries set to: %b%s%b\n" "$COLOR_WHITE" "$RETRIES" "$COLOR_RESET"
  printf "User-Agent set to: %b%s%b\n" "$COLOR_WHITE" "$USER_AGENT" "$COLOR_RESET"

  if [[ -n "$DOMAINS_FILE" ]]; then
    printf "Domain mode set to: %buser domains from %s%b\n" "$COLOR_WHITE" "$DOMAINS_FILE" "$COLOR_RESET"
  elif [[ -n "$SINGLE_DOMAIN" ]]; then
    printf "Checking single domain: %b%s%b\n" "$COLOR_WHITE" "$SINGLE_DOMAIN" "$COLOR_RESET"
  else
    printf "Domain mode set to: %bpredefined domains%b\n" "$COLOR_WHITE" "$COLOR_RESET"
  fi

  printf "IP version set to: %bIPv%s%b\n" "$COLOR_WHITE" "$IP_VERSION" "$COLOR_RESET"

  if [ -n "$PROXY" ]; then
    printf "SOCKS5 proxy set to: %b%s%b\n" "$COLOR_WHITE" "$PROXY" "$COLOR_RESET"
  fi

  case $PROTOCOL in
    http)
      printf "Protocol set to: %bHTTP only%b\n" "$COLOR_WHITE" "$COLOR_RESET"
      ;;
    https)
      printf "Protocol set to: %bHTTPS only%b\n" "$COLOR_WHITE" "$COLOR_RESET"
      ;;
    both)
      printf "Protocol set to: %bHTTP and HTTPS%b\n" "$COLOR_WHITE" "$COLOR_RESET"
      ;;
  esac
}

run_dns_checks() {
  check_encrypted_dns_servers
  check_dns_hijacking
}

read_domains_from_file() {
  local file=$1
  local domains=()

  while IFS= read -r line || [[ -n "$line" ]]; do
    if [[ -n "$line" && ! "$line" =~ ^[[:space:]]*# ]]; then
      line=$(echo "$line" | xargs)
      if [[ -n "$line" ]]; then
        domains+=("$line")
      fi
    fi
  done <"$file"

  echo "${domains[@]}"
}

execute_curl() {
  local url=$1
  local protocol=$2
  local follow_redirects=$3
  local ip_version_to_use=${4:-$IP_VERSION}
  local curl_output
  local curl_opts=(
    -s
    --compressed
    -o /dev/null
    -w "%{http_code}${CURL_SEPARATOR}%{redirect_url}"
    --retry-connrefused
    --retry-all-errors
    --retry "$RETRIES"
    --connect-timeout "$TIMEOUT"
    --max-time "$((TIMEOUT * (RETRIES + 1)))"
    -"$ip_version_to_use"
    -A "$USER_AGENT"
    -H "Sec-Fetch-Site: none"
    -H "Accept-Language: en-US,en;q=0.5"
  )

  if [ -n "$PROXY" ]; then
    curl_opts+=(--proxy "socks5h://$PROXY")
  fi

  if [ "$follow_redirects" = true ]; then
    curl_opts+=(-L)
  fi

  if curl_output=$(curl "${curl_opts[@]}" "${protocol}://${url}"); then
    echo "$curl_output"
  else
    echo "000${CURL_SEPARATOR}"
  fi
}

get_domains_to_check() {
  if [[ -n "$SINGLE_DOMAIN" ]]; then
    echo "$SINGLE_DOMAIN"
  elif [[ -z "$DOMAINS_FILE" ]]; then
    case $MODE in
      # TODO: Replace echo with function
      dpi) echo "${DPI_BLOCKED_SITES[@]}" ;;
      geoblock) echo "${GEO_BLOCKED_SITES[@]}" ;;
      both) echo "${DPI_BLOCKED_SITES[@]}" "${GEO_BLOCKED_SITES[@]}" ;;
    esac
  else
    read_domains_from_file "$DOMAINS_FILE"
  fi
}

get_single_check_result() {
  local domain=$1
  local protocol=$2
  local follow_redirects=$3
  local ip_version=$4
  local response status_code redirect_url

  local port
  [[ "${protocol,,}" == "https" ]] && port=443 || port=80

  if [[ -z "$PROXY" ]]; then
    local ip
    ip=$(get_domain_ip "$domain" "$ip_version")

    if [[ -n "$ip" ]] && ! is_port_open "$ip" "$port"; then
      jq -n '{"status": -1, "redirect_url": null}'
      return
    fi
  fi

  response=$(execute_curl "$domain" "$protocol" "$follow_redirects" "$ip_version")
  status_code="${response%%$CURL_SEPARATOR*}"
  redirect_url="${response#*$CURL_SEPARATOR}"

  jq -n \
    --argjson status "${status_code:-0}" \
    --arg redirect_url "${redirect_url:-}" \
    '
    {
      "status": ($status|tonumber),
      "redirect_url": (if $redirect_url == "" then null else $redirect_url end)
    }
    '
}

gather_single_domain_result() {
  local domain=$1
  local ipv6_supported
  local http_ipv4=null http_ipv6=null https_ipv4=null https_ipv6=null

  check_ipv6_support && ipv6_supported=true || ipv6_supported=false

  if [[ "$PROTOCOL" == "both" || "$PROTOCOL" == "http" ]]; then
    http_ipv4=$(get_single_check_result "$domain" "HTTP" false 4)
    if $ipv6_supported; then
      http_ipv6=$(get_single_check_result "$domain" "HTTP" false 6)
    fi
  fi
  if [[ "$PROTOCOL" == "both" || "$PROTOCOL" == "https" ]]; then
    https_ipv4=$(get_single_check_result "$domain" "HTTPS" true 4)
    if $ipv6_supported; then
      https_ipv6=$(get_single_check_result "$domain" "HTTPS" true 6)
    fi
  fi

  jq -n \
    --arg service "$(get_domain_host "$domain")" \
    --argjson http_ipv4 "$http_ipv4" \
    --argjson http_ipv6 "$http_ipv6" \
    --argjson https_ipv4 "$https_ipv4" \
    --argjson https_ipv6 "$https_ipv6" \
    '
        {
          "service": $service,
          "http": {
            "ipv4": $http_ipv4,
            "ipv6": $http_ipv6
          },
          "https": {
            "ipv4": $https_ipv4,
            "ipv6": $https_ipv6
          }
        }
        '
}

get_domain_host() {
  printf "%s" "${1%%/*}"
}

get_record_type() {
  local ip_version=${1:-$IP_VERSION}
  [[ "$ip_version" == "6" ]] && echo "AAAA" || echo "A"
}

get_domain_ip() {
  local domain
  domain=$(get_domain_host "$1")
  local ip_version=${2:-$IP_VERSION}

  dig +short "$domain" "$(get_record_type "$ip_version")" 2>/dev/null |
    awk '/^[0-9a-fA-F.:]+$/ {print; exit}' || true
}

domain_exists() {
  local domain
  domain=$(get_domain_host "$1")
  local rcode

  rcode=$(dig +noall +comments "$domain" A 2>/dev/null |
    awk -F'status: ' '/status:/ {split($2, parts, ","); print parts[1]; exit}')

  [[ "$rcode" != "NXDOMAIN" ]]
}

get_domain_ips_via_dns() {
  local domain=$1
  local server=$2

  dig +short @"$server" "$domain" "$(get_record_type)" \
    +timeout="$TIMEOUT" +tries=1 2>/dev/null |
    awk '/^[0-9a-fA-F.:]+$/' || true
}

resolve_via_dig() {
  local domain=$1
  local server_ip=$2
  local transport=$3

  dig +short +"$transport" @"$server_ip" "$domain" "$(get_record_type)" \
    +timeout="$TIMEOUT" +tries=1 2>/dev/null |
    awk '/^[0-9a-fA-F.:]+$/'
}

have_ip_intersection() {
  local -n first_ips=$1
  local -n second_ips=$2

  declare -A ip_set=()
  local ip

  for ip in "${first_ips[@]}"; do
    ip_set["$ip"]=1
  done

  for ip in "${second_ips[@]}"; do
    [[ -n ${ip_set["$ip"]+1} ]] && return 0
  done

  return 1
}

probe_resolver_transport() {
  if [[ -n "$(resolve_via_dig "$1" "$2" "$3")" ]]; then
    echo "available"
  else
    echo "blocked"
  fi
}

check_encrypted_dns_servers() {
  local test_domain="rutracker.org"
  local entry name ip doh_status dot_status
  local table_rows=()
  local i tmp_dir

  AVAILABLE_DOH=()
  AVAILABLE_DOT=()

  tmp_dir=$(mktemp -d)

  for i in "${!ENCRYPTED_DNS_SERVERS[@]}"; do
    ip="${ENCRYPTED_DNS_SERVERS[$i]##*|}"
    probe_resolver_transport "$test_domain" "$ip" "https" >"$tmp_dir/${i}_https" &
    probe_resolver_transport "$test_domain" "$ip" "tls" >"$tmp_dir/${i}_tls" &
  done
  wait

  for i in "${!ENCRYPTED_DNS_SERVERS[@]}"; do
    entry="${ENCRYPTED_DNS_SERVERS[$i]}"
    name="${entry%%|*}"
    ip="${entry##*|}"

    if [[ "$(<"$tmp_dir/${i}_https")" == "available" ]]; then
      AVAILABLE_DOH+=("$entry")
      doh_status="$MSG_AVAILABLE"
    else
      doh_status="$MSG_BLOCKED"
    fi

    if [[ "$(<"$tmp_dir/${i}_tls")" == "available" ]]; then
      AVAILABLE_DOT+=("$entry")
      dot_status="$MSG_AVAILABLE"
    else
      dot_status="$MSG_BLOCKED"
    fi

    table_rows+=("$(printf "%s%b\t%s\t%s\t%s" \
      "$name" "$COLOR_RESET" "$ip" \
      "$(colorize_summary "$doh_status")" \
      "$(colorize_summary "$dot_status")")")
  done

  rm -rf "$tmp_dir"

  printf "\n%bEncrypted DNS (DoH/DoT) availability:%b\n\n" "$COLOR_WHITE" "$COLOR_RESET"

  {
    printf "\033[1m%b%s\t%s\t%s\t%s%b\033[0m\n" \
      "$COLOR_WHITE" "Resolver" "IP" "DoH" "DoT" "$COLOR_RESET"
    printf "%s\n" "${table_rows[@]}"
  } | column -t -s $'\t'
}

get_reference_resolver() {
  if [[ ${#AVAILABLE_DOH[@]} -gt 0 ]]; then
    printf "%s\thttps" "${AVAILABLE_DOH[0]}"
  elif [[ ${#AVAILABLE_DOT[@]} -gt 0 ]]; then
    printf "%s\ttls" "${AVAILABLE_DOT[0]}"
  fi
}

check_dns_hijacking() {
  local test_domains=("rutracker.org" "linkedin.com" "flibusta.is")
  local regular_dns_ips=()
  local encrypted_ips=()
  local hijacked_domain=""
  local hijacked_ip=""

  local reference reference_entry reference_transport reference_name reference_ip
  reference=$(get_reference_resolver)

  if [[ -z "$reference" ]]; then
    printf "\n%b%s%b\n" \
      "$COLOR_ORANGE" \
      "Cannot verify DNS spoofing: no DoH/DoT server reachable, skipping check" \
      "$COLOR_RESET"
    return 0
  fi

  reference_entry="${reference%%$'\t'*}"
  reference_transport="${reference##*$'\t'}"
  reference_name="${reference_entry%%|*}"
  reference_ip="${reference_entry##*|}"

  for test_domain in "${test_domains[@]}"; do
    regular_dns_ips=()
    encrypted_ips=()

    for dns_server in "${DNS_SERVERS[@]}"; do
      mapfile -t regular_dns_ips < <(get_domain_ips_via_dns "$test_domain" "$dns_server")
      [[ ${#regular_dns_ips[@]} -gt 0 ]] && break
    done

    mapfile -t encrypted_ips < <(resolve_via_dig "$test_domain" "$reference_ip" "$reference_transport")

    [[ ${#regular_dns_ips[@]} -eq 0 ]] || [[ ${#encrypted_ips[@]} -eq 0 ]] && continue

    if ! have_ip_intersection regular_dns_ips encrypted_ips; then
      hijacked_domain="$test_domain"
      hijacked_ip="${regular_dns_ips[0]}"
      break
    fi
  done

  if [[ -n "$hijacked_domain" ]]; then
    printf "\n%b%s%b %s %b%s%b %s %b%s%b\n" \
      "$COLOR_RED" "DNS hijacking detected!" "$COLOR_RESET" \
      "ISP redirects" "$COLOR_WHITE" "$hijacked_domain" "$COLOR_RESET" "to" \
      "$COLOR_RED" "$hijacked_ip" "$COLOR_RESET"

    printf "%bReference resolver: %s (%s) over %s%b\n\n" \
      "$COLOR_WHITE" "$reference_name" "$reference_ip" "${reference_transport^^}" "$COLOR_RESET"

    printf "%b%s%b\n%b%s%b\n" \
      "$COLOR_ORANGE" "DNS hijacking may affect the accuracy of this check" "$COLOR_RESET" \
      "$COLOR_ORANGE" "Configure encrypted DNS (DoH/DoT) on your system" "$COLOR_RESET"
  else
    printf "\n%b%s%b %b(reference: %s over %s)%b\n" \
      "$COLOR_GREEN" "Good news, no DNS hijacking detected!" "$COLOR_RESET" \
      "$COLOR_WHITE" "$reference_name" "${reference_transport^^}" "$COLOR_RESET"
  fi
}

is_port_open() {
  local ip="$1"
  local port="$2"
  timeout "$TIMEOUT" bash -c "(echo >/dev/tcp/$ip/$port)" 2>/dev/null
}

is_ip_reachable() {
  local ip="$1"
  is_port_open "$ip" 80 || is_port_open "$ip" 443
}

make_json_error() {
  local domain="$1"
  local error_code="$2"
  local error_message="$3"

  jq -n \
    --arg service "$domain" \
    --arg code "$error_code" \
    --arg error "$error_message" \
    '
    {
      "service": $service,
      "error": $error,
      "error_code": $code,
      "http": null,
      "https": null
    }
    '
}

summarize_status_description() {
  local status_code=$1
  local redirect_url=$2
  local msg

  if [[ "$status_code" -eq -1 ]]; then
    msg="$MSG_BLOCKED_BY_PORT"
  elif [[ -z "$status_code" || "$status_code" = "000" || "$status_code" -eq 0 ]]; then
    msg="$MSG_TIMEOUT"
  elif [[ "$status_code" -ge 300 && "$status_code" -lt 400 ]]; then
    [[ -z "$redirect_url" ]] && redirect_url="<empty>"
    msg=$(printf "%s (%s) -> %s" "$MSG_REDIRECT" "$status_code" "$redirect_url")
  elif [[ "$status_code" -eq 200 ]]; then
    msg="$MSG_AVAILABLE ($status_code)"
  elif [[ "$status_code" -eq 403 ]]; then
    msg="$MSG_ACCESS_DENIED ($status_code)"
  else
    msg="$MSG_OTHER $status_code"
  fi

  echo "$msg"
}

colorize_summary() {
  local message="$1"
  local first_word rest first_word_color

  first_word="${message%% *}"
  if [[ "$first_word" == "$message" ]]; then
    rest=""
  else
    rest="${message#* }"
  fi

  case "$first_word" in
    Blocked)
      first_word_color=$COLOR_RED
      ;;
    Available)
      first_word_color=$COLOR_GREEN
      ;;
    Redirected)
      first_word_color=$COLOR_BLUE
      ;;
    Denied)
      first_word_color=$COLOR_RED
      ;;
    *)
      first_word_color=$COLOR_ORANGE
      ;;
  esac

  if [[ -z "$rest" ]]; then
    printf "%b%s%b" "$first_word_color" "$first_word" "$COLOR_RESET"
  else
    printf "%b%s%b %s" "$first_word_color" "$first_word" "$COLOR_RESET" "$rest"
  fi
}

summarize_protocol_result() {
  local result_json=$1
  local protocol=$2

  if [[ "$PROTOCOL" != "both" && "$PROTOCOL" != "$protocol" ]]; then
    echo "Skipped"
    return
  fi

  local data
  data=$(jq -c --arg protocol "$protocol" --arg ip_version "$IP_VERSION" '
      if $ip_version == "6" then .[$protocol].ipv6
      else .[$protocol].ipv4
      end
    ' <<<"$result_json")

  if [[ "$data" == "null" || -z "$data" ]]; then
    echo "N/A"
    return
  fi

  local status redirect
  status=$(jq -r '.status' <<<"$data")
  redirect=$(jq -r '.redirect_url // ""' <<<"$data")
  summarize_status_description "$status" "$redirect"
}

add_text_result_row() {
  local service=$1
  local ip=$2
  local http_cell=$3
  local https_cell=$4

  TEXT_RESULTS+=("$(
    jq -n \
      --arg service "$service" \
      --arg ip "$ip" \
      --arg http "$http_cell" \
      --arg https "$https_cell" \
      '{service: $service, ip: $ip, http: $http, https: $https}'
  )")
}

add_text_result_from_json() {
  local result_json=$1
  local ip=$2
  local service
  service=$(echo "$result_json" | jq -r '.service')
  local http_cell https_cell

  http_cell=$(summarize_protocol_result "$result_json" "http")
  https_cell=$(summarize_protocol_result "$result_json" "https")

  add_text_result_row "$service" "$ip" "$http_cell" "$https_cell"
}

print_table_results() {
  printf "\n"
  {
    printf "\033[1m%b%s\t%s\t%s\t%s%b\033[0m\n" \
      "$COLOR_WHITE" \
      "Service" \
      "IP" \
      "HTTP" \
      "HTTPS" \
      "$COLOR_RESET"

    for row_json in "${TEXT_RESULTS[@]}"; do
      local service http https
      service=$(jq -r '.service' <<<"$row_json")
      ip=$(jq -r '.ip' <<<"$row_json")
      http=$(jq -r '.http' <<<"$row_json")
      https=$(jq -r '.https' <<<"$row_json")

      printf "%s%b\t%s\t%s\t%s\n" \
        "$service" \
        "$COLOR_RESET" \
        "$ip" \
        "$(colorize_summary "$http")" \
        "$(colorize_summary "$https")"
    done
  } | column -t -s $'\t'
}

run_checks_and_print() {
  local domains
  local all_results_json="[]"

  if [[ "$MODE" == "dns" ]]; then
    if $JSON_OUTPUT; then
      error_exit "JSON output is not supported for dns mode"
    fi

    if $SHOW_HEADER; then
      print_header
    fi

    run_dns_checks
    return
  fi

  read -r -a domains <<<"$(get_domains_to_check)"

  local total_domains=${#domains[@]}

  local current_index=0

  TEXT_RESULTS=()

  if ! $JSON_OUTPUT; then
    if $SHOW_HEADER; then
      print_header
    fi
    if $DNS_CHECK; then
      run_dns_checks
    fi
    if $SHOW_HEADER || $DNS_CHECK; then
      printf "\n"
    fi
  fi

  for domain in "${domains[@]}"; do
    ((++current_index))
    show_progress "$current_index" "$total_domains" "$domain"

    local host ip_address

    host=$(get_domain_host "$domain")
    ip_address=$(get_domain_ip "$domain")

    if [[ -z "$ip_address" ]]; then
      local error_code error_message

      if domain_exists "$domain"; then
        error_code="no_dns_record"
        error_message="No $(get_record_type) record"
      else
        error_code="nxdomain"
        error_message="Domain does not exist"
      fi

      if $JSON_OUTPUT; then
        all_results_json=$(echo "$all_results_json" | jq --argjson item "$(make_json_error "$host" "$error_code" "$error_message")" '. + [$item]')
      else
        add_text_result_row "$host" "N/A" "$error_message" "$error_message"
      fi
      continue
    fi

    if [[ -z "$PROXY" ]] && ! is_ip_reachable "$ip_address"; then
      if $JSON_OUTPUT; then
        all_results_json=$(echo "$all_results_json" | jq --argjson item "$(make_json_error "$host" blocked_by_ip "$MSG_BLOCKED_BY_IP")" '. + [$item]')
      else
        add_text_result_row "$host" "$ip_address" "$MSG_BLOCKED_BY_IP" "$MSG_BLOCKED_BY_IP"
      fi
      continue
    fi

    local domain_result_json
    domain_result_json=$(gather_single_domain_result "$domain")

    if $JSON_OUTPUT; then
      all_results_json=$(echo "$all_results_json" | jq --argjson item "$domain_result_json" '. + [$item]')
    else
      add_text_result_from_json "$domain_result_json" "$ip_address"
    fi
  done

  clear_progress

  if $JSON_OUTPUT; then
    local ipv6_supported
    check_ipv6_support && ipv6_supported=true || ipv6_supported=false

    local ip_version_param_val
    if $ipv6_supported; then
      ip_version_param_val="IPv4 & IPv6"
    else
      ip_version_param_val="IPv4"
    fi

    local params_json

    params_json=$(
      jq -n \
        --arg timeout "${TIMEOUT}s" \
        --arg retries "$RETRIES" \
        --arg mode "${MODE^^}" \
        --arg user_agent "$USER_AGENT" \
        --arg domain_mode "$(if [[ -n "$DOMAINS_FILE" ]]; then echo "user domains from $DOMAINS_FILE"; elif [[ -n "$SINGLE_DOMAIN" ]]; then echo "single domain"; else echo "predefined domains"; fi)" \
        --arg ip_version "$ip_version_param_val" \
        --arg protocol "$(if [[ "$PROTOCOL" == "both" ]]; then echo "HTTP and HTTPS"; elif [[ "$PROTOCOL" == "http" ]]; then echo "HTTP only"; else echo "HTTPS only"; fi)" \
        '[
          {"key":"timeout", "value":$timeout},

          {"key":"retries", "value":$retries},
          {"key":"mode", "value":$mode},
          {"key":"user_agent", "value":$user_agent},
          {"key":"domain_mode", "value":$domain_mode},
          {"key":"ip_version", "value":$ip_version},
          {"key":"protocol", "value":$protocol}
        ]'
    )

    jq -n \
      --argjson params "$params_json" \
      --argjson results "$all_results_json" \
      '{
				"version": 1,
				"params": $params,
				"results": $results
			}'

    return
  fi

  print_table_results
}

main() {
  set -euo pipefail

  trap cleanup INT TERM

  parse_arguments "$@"
  install_dependencies
  run_checks_and_print

  if ! $JSON_OUTPUT; then
    printf "\n"
  fi

  trap - INT TERM
}

main "$@"
