#!/usr/bin/env bash

# ============================================
# Rule File Parsing
# ============================================

function rule::is_base() {
  local name="${1:-}"
  [[ -f "$(ctx::rules::base)/${name}.rule" ]]
}

function rule::exists() {
  local name="${1:-}"
  local path
  path=$(json::find_rule_file "$(ctx::rules)" "$name")
  [[ -n "$path" ]]
}

function rule::require_assignable() {
  local name="${1:-}"
  if rule::is_base "$name"; then
    log::error "Cannot assign base rule '${name}' — base rules cannot be assigned directly"
    return 1
  fi
}

function rule::require_exists() {
  local name="${1:-}"
  if ! rule::exists "$name"; then
    log::error "Rule not found: ${name}"
    return 1
  fi
}

function rule::get() {
  local name="${1:-}" key="${2:-}"
  json::rule_resolve_field "$(ctx::rules)" "$name" "$key"
}

function rule::get_own() {
  local name="${1:-}" key="${2:-}"
  local file
  file=$(rule::path "$name") || return 0
  json::get_raw "$file" "$key"
}

function rule::get_resolved() {
  local name="${1:-}"
  json::rule_resolve "$(ctx::rules)" "$name"
}

function rule::path() {
  local name="${1:-}"
  local path
  path=$(json::find_rule_file "$(ctx::rules)" "$name")
  [[ -n "$path" ]] && echo "$path" || return 1
}

function rule::get_all() {
  local name="${1:-}"
  rule::get_resolved "$name"
}

function rule::is_applied() {
  local rule_name="${1:-}" client_ip="${2:-}"

  local first_port
  first_port=$(rule::get "$rule_name" "block_ports" | head -1)
  if [[ -n "$first_port" ]]; then
    local target port proto
    IFS=":" read -r target port proto <<< "$first_port"
    proto="${proto:-tcp}"
    fw::has_block_rule "$client_ip" "$target" "$proto" "$port" 
    return $?
  fi

  local first_ip
  first_ip=$(rule::get "$rule_name" "block_ips" | head -1)
  if [[ -n "$first_ip" ]]; then
    fw::has_block_rule "$client_ip" "$first_ip"
    return $?
  fi

  local first_allow
  first_allow=$(rule::get "$rule_name" "allow_ports" | head -1)
  if [[ -n "$first_allow" ]]; then
    local target port proto
    IFS=":" read -r target port proto <<< "$first_allow"
    proto="${proto:-tcp}"
    fw::_forward_exists -s "$client_ip" -d "$target" \
      -p "$proto" --dport "$port" -j ACCEPT
    return $?
  fi

  return 1
}

# ============================================
# Rule Application
# ============================================

function rule::apply() {
  local rule_name="${1:?rule_name required}"
  local client_ip="${2:?client_ip required}"
  local peer_name="${3:-}"

  rule::require_exists "$rule_name" || return 1

  if [[ -z "$peer_name" ]]; then
    peer_name=$(peers::find_by_ip "$client_ip")
  fi

  log::debug "rule::apply: peer_name=$peer_name ip=$client_ip"

  # Check if already applied
  if rule::is_applied "$rule_name" "$client_ip"; then
    log::wg "Rule '${rule_name}' already applied to: ${client_ip}"
    [[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
    return 0
  fi

  # Process block_ips
  while IFS= read -r block_ip; do
    [[ -z "$block_ip" ]] && continue
    fw::block_ip "$client_ip" "$block_ip"
  done < <(rule::get "$rule_name" "block_ips")

  # Process block_ports
  while IFS= read -r entry; do
    [[ -z "$entry" ]] && continue
    local target port proto
    IFS=":" read -r target port proto <<< "$entry"
    proto="${proto:-tcp}"
    fw::block_port "$client_ip" "$target" "$port" "$proto"
  done < <(rule::get "$rule_name" "block_ports")

  # Process allow_ips (inserted before blocks)
  while IFS= read -r allow_ip; do
    [[ -z "$allow_ip" ]] && continue
    fw::allow_ip "$client_ip" "$allow_ip"
  done < <(rule::get "$rule_name" "allow_ips")

  # Process allow_ports (highest priority)
  while IFS= read -r entry; do
    [[ -z "$entry" ]] && continue
    local target port proto
    IFS=":" read -r target port proto <<< "$entry"
    proto="${proto:-tcp}"
    fw::allow_port "$client_ip" "$target" "$port" "$proto"
  done < <(rule::get "$rule_name" "allow_ports")

  # Persist rule assignment
  [[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"

  # DNS redirect
  local dns_redirect
  dns_redirect=$(rule::get "$rule_name" "dns_redirect")
  if [[ "$dns_redirect" == "true" ]]; then
    local peer_subnet
    peer_subnet=$(peers::get_ip "$peer_name" | cut -d'.' -f1-3)
    if ! fw::_nat_exists -i wg0 -s "${peer_subnet}.0/24" \
      -p udp --dport 53 -j DNAT \
      --to-destination "$(config::dns):53" 2>/dev/null; then
      rule::apply_dns_redirect "${peer_subnet}.0/24"
      log::debug "dns_redirect: applied for ${peer_subnet}.0/24"
    else
      log::debug "dns_redirect: already applied for ${peer_subnet}.0/24"
    fi
  fi

  log::debug "Applied rule '${rule_name}' to: ${client_ip}"
}

function rule::unapply() {
  local rule_name="${1:-}" client_ip="${2:-}"

  rule::require_exists "$rule_name" || return 1

  local peer_name
  peer_name=$(peers::find_by_ip "$client_ip")

  # Remove allow_ports first (reverse order of apply)
  while IFS= read -r entry; do
    [[ -z "$entry" ]] && continue
    local target port proto
    IFS=":" read -r target port proto <<< "$entry"
    proto="${proto:-tcp}"
    fw::unallow_port "$client_ip" "$target" "$port" "$proto"
  done < <(rule::get "$rule_name" "allow_ports")

  # Remove allow_ips
  while IFS= read -r allow_ip; do
    [[ -z "$allow_ip" ]] && continue
    if [[ "$allow_ip" == *"/"* ]]; then
      fw::unallow_subnet "$client_ip" "$allow_ip"
    else
      fw::unallow_ip "$client_ip" "$allow_ip"
    fi
  done < <(rule::get "$rule_name" "allow_ips")

  # Remove block_ports
  while IFS= read -r entry; do
    [[ -z "$entry" ]] && continue
    local target port proto
    IFS=":" read -r target port proto <<< "$entry"
    proto="${proto:-tcp}"
    fw::unblock_port "$client_ip" "$target" "$port" "$proto"
  done < <(rule::get "$rule_name" "block_ports")

  # Remove block_ips
  while IFS= read -r block_ip; do
    [[ -z "$block_ip" ]] && continue
    if [[ "$block_ip" == *"/"* ]]; then
      fw::unblock_subnet "$client_ip" "$block_ip"
    else
      fw::unblock_ip "$client_ip" "$block_ip"
    fi
  done < <(rule::get "$rule_name" "block_ips")

  # Remove DNS redirect if applicable
  local dns_redirect
  dns_redirect=$(rule::get "$rule_name" "dns_redirect")
  if [[ "$dns_redirect" == "true" ]]; then
    local subtype
    subtype=$(peers::get_meta "$peer_name" "subtype")
    local subnet
    if [[ -n "$subtype" ]]; then
      subnet=$(config::subnet_for "$subtype")
    else
      local peer_type
      peer_type=$(peers::get_type "$peer_name") || true
      [[ -z "$peer_type" ]] && peer_type="phone"
      subnet=$(config::subnet_for "$peer_type")
    fi
    rule::remove_dns_redirect "${subnet}.0/24"
  fi

  # Clear rule from meta
  [[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" ""

  log::debug "Removed rule '${rule_name}' from: ${client_ip}"
}

function rule::reapply_all() {
  local rule_name="${1:-}"
  rule::require_exists "$rule_name" || return 1

  local peers=()
  mapfile -t peers < <(peers::with_rule "$rule_name")
  [[ ${#peers[@]} -eq 0 ]] && return 0

  local count=0
  for peer_name in "${peers[@]}"; do
    local client_ip
    client_ip=$(peers::get_ip "$peer_name")
    [[ -z "$client_ip" ]] && continue
    # FLUSH first to ensure clean ordering
    fw::flush_peer "$client_ip"
    rule::apply "$rule_name" "$client_ip" "$peer_name"
    (( count++ )) || true
  done

  log::wg_success "Rule '${rule_name}' re-applied to ${count} peers"
}

function rule::restore_all() {
  while IFS= read -r peer_name; do
    # Skip blocked peers - no fw rules needed when blocked
    block::is_blocked "$peer_name" && continue
    
    local rule_name
    rule_name=$(peers::get_meta "$peer_name" "rule")

    [[ -z "$rule_name" ]] && continue

    if ! rule::exists "$rule_name"; then
      log::wg_warning "Rule '${rule_name}' not found for peer '${peer_name}', skipping"
      continue
    fi

    local client_ip
    client_ip=$(peers::get_ip "$peer_name")
    [[ -z "$client_ip" ]] && continue

    rule::apply "$rule_name" "$client_ip"
  done < <(peers::all)
  log::wg "Rules restored for all peers"
}

# ============================================
# Rendering
# ============================================

function rule::render_flat() {
  local rule_name="${1:-}"

  local allow_ports allow_ips block_ips block_ports dns
  allow_ports=$(rule::get "$rule_name" "allow_ports")
  allow_ips=$(rule::get "$rule_name" "allow_ips")
  block_ips=$(rule::get "$rule_name" "block_ips")
  block_ports=$(rule::get "$rule_name" "block_ports")
  dns=$(rule::get_own "$rule_name" "dns_redirect")

  local has_content=false
  [[ -n "${allow_ports}${allow_ips}${block_ips}${block_ports}" ]] && \
    has_content=true

  if ! $has_content; then
    printf "\n    full access (no restrictions)\n"
    return 0
  fi

  if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then
    printf "\n"
    while IFS= read -r e; do
      [[ -z "$e" ]] && continue
      net::print_entry "+" "$e" 2
    done <<< "$allow_ports"$'\n'"$allow_ips"
  fi

  if [[ -n "$block_ips" || -n "$block_ports" ]]; then
    printf "\n"
    while IFS= read -r e; do
      [[ -z "$e" ]] && continue
      net::print_entry "-" "$e" 2
    done <<< "$block_ips"$'\n'"$block_ports"
  fi

  [[ "${dns,,}" == "true" ]] && \
    net::print_dns_redirect "$(config::dns)" 6 "DNS"

  return 0
}

function rule::render_entries() {
  # Renders allow/block entries for a rule name with annotations and DNS
  # Usage: rule::render_entries <rule_name> <indent>
  # indent: 4 for rule show, 4 for inspect (same)
  local rule_name="${1:-}" indent="${2:-4}"
  local rule_file
  rule_file="$(rule::path "$rule_name")"

  local allow_ports allow_ips block_ips block_ports dns
  allow_ports=$(rule::get "$rule_name" "allow_ports" 2>/dev/null || true)
  allow_ips=$(rule::get "$rule_name" "allow_ips"    2>/dev/null || true)
  block_ips=$(rule::get "$rule_name" "block_ips"    2>/dev/null || true)
  block_ports=$(rule::get "$rule_name" "block_ports" 2>/dev/null || true)
  dns=$(rule::get_own "$rule_name" "dns_redirect")

  while IFS= read -r e; do
    [[ -z "$e" ]] && continue
    net::print_entry "+" "$e"
  done <<< "$allow_ports"$'\n'"$allow_ips"

  while IFS= read -r e; do
    [[ -z "$e" ]] && continue
    net::print_entry "-" "$e"
  done <<< "$block_ips"$'\n'"$block_ports"

  [[ "${dns,,}" == "true" ]] && \
    net::print_dns_redirect "$(config::dns)" 6 "DNS"
}

function rule::render_own_entries() {
  # Renders own (non-inherited) entries for a rule
  local rule_name="${1:-}"
  local rule_file
  rule_file="$(rule::path "$rule_name")"

  local allow_ports allow_ips block_ips block_ports dns
  allow_ports=$(json::get "$rule_file" "allow_ports" 2>/dev/null || true)
  allow_ips=$(json::get "$rule_file" "allow_ips"     2>/dev/null || true)
  block_ips=$(json::get "$rule_file" "block_ips"     2>/dev/null || true)
  block_ports=$(json::get "$rule_file" "block_ports"  2>/dev/null || true)
  dns=$(json::get "$rule_file" "dns_redirect"         2>/dev/null || true)

  local has_own=false
  local combined="${allow_ports}${allow_ips}${block_ips}${block_ports}"
  [[ -n "${combined//[$'\n']/}" ]] && has_own=true

  $has_own || return 0

  while IFS= read -r e; do
    [[ -z "$e" ]] && continue
    net::print_entry "+" "$e"
  done <<< "$allow_ports"$'\n'"$allow_ips"

  while IFS= read -r e; do
    [[ -z "$e" ]] && continue
    net::print_entry "-" "$e"
  done <<< "$block_ips"$'\n'"$block_ports"

  [[ "${dns,,}" == "true" ]] && \
    net::print_dns_redirect "$(config::dns)" 6 "DNS"

  return 0
}

function rule::render_extends_tree() {
  # Renders full inheritance tree for a rule
  local rule_name="${1:-}"
  local rule_file
  rule_file="$(rule::path "$rule_name")"

  local extends_raw=()
  mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true)

  [[ ${#extends_raw[@]} -eq 0 || -z "${extends_raw[0]:-}" ]] && return 1

  for base_name in "${extends_raw[@]}"; do
    [[ -z "$base_name" ]] && continue
    printf "\n    \033[0;37m↳ %s\033[0m\n" "$base_name"
    rule::render_entries "$base_name"
  done

  # Own rules after inherited
  local own_output
  own_output=$(rule::render_own_entries "$rule_name")
  if [[ -n "$own_output" ]]; then
    printf "\n    \033[0;37mOwn:\033[0m\n"
    printf "%s\n" "$own_output"
  fi

  return 0
}

# ============================================
# DNS Redirect
# ============================================

function rule::apply_dns_redirect() {
  local client_subnet="${1:-}"
  fw::nat_add_dns_redirect "$client_subnet" "$(config::dns)" "$(config::interface)"
}

function rule::remove_dns_redirect() {
  local client_subnet="${1:-}"
  fw::nat_remove_dns_redirect "$client_subnet" "$(config::dns)" "$(config::interface)"
}
