#!/usr/bin/env bash
# subnet.module.sh — subnet map lookups, resolution, validation, and CIDR utilities
# All subnet data lives in subnets.json; this module wraps json:: calls
# and provides hardcoded fallbacks for safety.

# ======================================================
# CIDR Utilities
# Pure functions — no external dependencies, no side effects.
# Suitable for unit testing.
# ======================================================

# subnet::prefix <cidr>
# Returns the first three octets of a CIDR (the allocation prefix).
# Example: 10.1.3.0/24 -> 10.1.3
function subnet::prefix() {
  echo "${1%%/*}" | cut -d'.' -f1-3
}

# subnet::base_ip <cidr>
# Returns the network address without the mask.
# Example: 10.1.3.0/24 -> 10.1.3.0
function subnet::base_ip() {
  echo "${1%%/*}"
}

# subnet::mask <cidr>
# Returns the prefix length.
# Example: 10.1.3.0/24 -> 24
function subnet::mask() {
  echo "${1##*/}"
}

# subnet::host_range <cidr>
# Returns the iterable host range for a subnet.
# Currently supports /24 (1-254). Mask-aware implementation deferred.
function subnet::host_range() {
  local cidr="${1:-}"
  local mask
  mask=$(subnet::mask "$cidr")
  case "$mask" in
    24) seq 1 254 ;;
    *)
      # Fallback for non-/24 — still seq 1 254, but noted for future upgrade
      seq 1 254
      ;;
  esac
}

# subnet::contains <cidr> <ip>
# Returns 0 if the IP falls within the CIDR, 1 otherwise.
# Example: subnet::contains 10.1.3.0/24 10.1.3.5 -> 0 (true)
function subnet::contains() {
  local cidr="${1:-}" ip="${2:-}"
  [[ -z "$cidr" || -z "$ip" ]] && return 1

  local prefix mask
  prefix=$(subnet::prefix "$cidr")
  mask=$(subnet::mask "$cidr")

  # For /24: check that the first three octets match
  case "$mask" in
    24)
      local ip_prefix
      ip_prefix=$(echo "$ip" | cut -d'.' -f1-3)
      [[ "$ip_prefix" == "$prefix" ]]
      ;;
    *)
      # Delegate to Python for non-/24 subnets
      python3 -c "
import ipaddress, sys
try:
    net = ipaddress.ip_network('${cidr}', strict=False)
    addr = ipaddress.ip_address('${ip}')
    sys.exit(0 if addr in net else 1)
except Exception:
    sys.exit(1)
"
      ;;
  esac
}

# subnet::is_valid_cidr <cidr>
# Returns 0 if the string is a valid CIDR notation.
function subnet::is_valid_cidr() {
  local cidr="${1:-}"
  [[ "$cidr" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]] || return 1
  # Also validate each octet is 0-255
  local ip
  ip=$(subnet::base_ip "$cidr")
  local IFS='.'
  read -ra octets <<< "$ip"
  for octet in "${octets[@]}"; do
    (( octet >= 0 && octet <= 255 )) || return 1
  done
  return 0
}

# subnet::ip_valid_for <cidr> <ip>
# Returns 0 if the IP is a valid host address within the given CIDR.
# Excludes network address (.0) and broadcast address (.255) for /24.
function subnet::ip_valid_for() {
  local cidr="${1:-}" ip="${2:-}"
  [[ -z "$cidr" || -z "$ip" ]] && return 1

  # Must be a valid IP first
  ip::is_valid "$ip" || return 1

  # Must be within the subnet
  subnet::contains "$cidr" "$ip" || return 1

  # Must not be network or broadcast address (for /24)
  local mask
  mask=$(subnet::mask "$cidr")
  if [[ "$mask" == "24" ]]; then
    local last_octet
    last_octet=$(echo "$ip" | cut -d'.' -f4)
    [[ "$last_octet" == "0" || "$last_octet" == "255" ]] && return 1
  fi

  return 0
}

# subnet::require_valid_cidr <cidr>
# Errors and exits if the CIDR is not valid.
function subnet::require_valid_cidr() {
  local cidr="${1:-}"
  if ! subnet::is_valid_cidr "$cidr"; then
    log::error "Invalid CIDR notation: '${cidr}'"
    return 1
  fi
}

# subnet::require_ip_valid_for <cidr> <ip>
# Errors and exits if the IP is not a valid host for the subnet.
function subnet::require_ip_valid_for() {
  local cidr="${1:-}" ip="${2:-}"
  if ! subnet::ip_valid_for "$cidr" "$ip"; then
    log::error "IP '${ip}' is not a valid host address in subnet '${cidr}'"
    return 1
  fi
}

# ======================================================
# Hardcoded Fallbacks
# Mirror of production subnets.json.
# Used only when subnets.json lookup fails.
# ======================================================

function subnet::_hardcoded_cidr() {
  local type="${1:-}" subnet_name="${2:-}"
  if [[ -n "$subnet_name" ]]; then
    case "$subnet_name" in
      guests)  echo "10.1.100.0/24" ;;
      servers) echo "10.1.200.0/24" ;;
      iot)     echo "10.1.210.0/24" ;;
      *)       echo "10.1.0.0/24"   ;;
    esac
    return 0
  fi
  case "$type" in
    desktop)       echo "10.1.1.0/24"   ;;
    laptop)        echo "10.1.2.0/24"   ;;
    phone)         echo "10.1.3.0/24"   ;;
    tablet)        echo "10.1.4.0/24"   ;;
    guest)         echo "10.1.100.0/24" ;;
    guest-desktop) echo "10.1.101.0/24" ;;
    guest-laptop)  echo "10.1.102.0/24" ;;
    guest-phone)   echo "10.1.103.0/24" ;;
    guest-tablet)  echo "10.1.104.0/24" ;;
    server)        echo "10.1.200.0/24" ;;
    iot)           echo "10.1.210.0/24" ;;
    *)             echo "10.1.0.0/24"   ;;
  esac
}

function subnet::_hardcoded_type() {
  local ip="${1:-}"
  case "$ip" in
    10.1.1.*)   echo "desktop"  ;;
    10.1.2.*)   echo "laptop"   ;;
    10.1.3.*)   echo "phone"    ;;
    10.1.4.*)   echo "tablet"   ;;
    10.1.100.*) echo "none"     ;;
    10.1.101.*) echo "desktop"  ;;
    10.1.102.*) echo "laptop"   ;;
    10.1.103.*) echo "phone"    ;;
    10.1.104.*) echo "tablet"   ;;
    10.1.200.*) echo "server"   ;;
    10.1.210.*) echo "iot"      ;;
    *)          echo "unknown"  ;;
  esac
}

function subnet::_hardcoded_tunnel_mode() {
  echo "split"
}

# ======================================================
# Core Resolution
# ======================================================

# subnet::lookup <subnet_name> [type_key]
# Returns the CIDR for a given subnet name and optional type.
# Falls back to hardcoded map on failure.
function subnet::lookup() {
  local subnet_name="${1:-}" type_key="${2:-}"
  local result
  result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" "$type_key" 2>/dev/null) || true
  if [[ -n "$result" ]]; then
    echo "$result"
    return 0
  fi
  subnet::_hardcoded_cidr "" "$subnet_name"
}

# subnet::resolve_for_add <type> [subnet_name]
# Main entry point for wgctl add — returns the CIDR to allocate from.
function subnet::resolve_for_add() {
  local peer_type="${1:-}" subnet_name="${2:-}"
  local result

  if [[ -n "$subnet_name" ]]; then
    # Group entry: try type-specific child first, then "none" slot
    if [[ -n "$peer_type" ]]; then
      result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" "$peer_type" 2>/dev/null) || true
      [[ -n "$result" ]] && { echo "$result"; return 0; }
    fi
    result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" 2>/dev/null) || true
    [[ -n "$result" ]] && { echo "$result"; return 0; }
    subnet::_hardcoded_cidr "" "$subnet_name"
    return 0
  fi

  # No subnet_name — resolve from type (native allocation)
  if [[ -n "$peer_type" ]]; then
    result=$(json::subnet_lookup "$(ctx::subnets)" "$peer_type" 2>/dev/null) || true
    [[ -n "$result" ]] && { echo "$result"; return 0; }
  fi

  subnet::_hardcoded_cidr "$peer_type"
}

# subnet::type_for_add <type_flag> [subnet_name]
# Returns the canonical type string to store in meta.
function subnet::type_for_add() {
  local type_flag="${1:-}" subnet_name="${2:-}"
  local result

  if [[ -n "$subnet_name" ]]; then
    result=$(json::subnet_type "$(ctx::subnets)" "$subnet_name" "$type_flag" 2>/dev/null) || true
    [[ -n "$result" ]] && { echo "$result"; return 0; }
  fi

  echo "${type_flag:-none}"
}

# subnet::tunnel_mode <subnet_name> [type_key]
# Returns "split" or "full" for the given subnet.
function subnet::tunnel_mode() {
  local subnet_name="${1:-}" type_key="${2:-}"
  local result
  result=$(json::subnet_tunnel_mode "$(ctx::subnets)" "$subnet_name" "$type_key" 2>/dev/null) || true
  [[ -n "$result" ]] && { echo "$result"; return 0; }
  subnet::_hardcoded_tunnel_mode
}

function subnet::type_from_ip() {
  local ip="${1:-}"
  [[ -z "$ip" ]] && echo "unknown" && return 0

  # Fast path: hardcoded map covers all production subnets — pure bash, no subshell
  local type
  type=$(subnet::_hardcoded_type "$ip")
  if [[ "$type" != "unknown" ]]; then
    echo "$type"
    return 0
  fi

  # Slow path: Python lookup for dynamically-added subnets not in hardcoded map
  local result
  result=$(json::subnet_for_ip "$(ctx::subnets)" "$ip" 2>/dev/null) || true
  if [[ -n "$result" ]]; then
    echo "${result##*|}"
    return 0
  fi

  echo "unknown"
}

# subnet::name_from_ip <ip>
# Returns the subnet name (e.g. "guests", "desktop") for an IP.
function subnet::name_from_ip() {
  local ip="${1:-}"
  local result
  result=$(json::subnet_for_ip "$(ctx::subnets)" "$ip" 2>/dev/null) || true
  [[ -n "$result" ]] && { echo "${result%%|*}"; return 0; }
  echo ""
}

# ======================================================
# Validation
# ======================================================

function subnet::exists() {
  local name="${1:-}"
  json::subnet_exists "$(ctx::subnets)" "$name" 2>/dev/null
}

function subnet::require_exists() {
  local name="${1:-}"
  if ! subnet::exists "$name"; then
    log::error "Subnet '${name}' not found. Use 'wgctl subnet list' to see available subnets."
    return 1
  fi
}

function subnet::peers_using() {
  local subnet_name="${1:-}"
  local peers
  peers=$(json::subnet_peers \
    "$(ctx::meta)" \
    "$(ctx::clients)" \
    "$subnet_name" \
    "$(ctx::subnets)" \
    2>/dev/null) || true
  echo "$peers" | tr '\n' ',' | sed 's/,$//'
}
 

# ======================================================
# Display Data
# ======================================================

function subnet::list_data() {
  json::subnet_list "$(ctx::subnets)" 2>/dev/null || true
}

function subnet::show_data() {
  local name="${1:-}"
  json::subnet_show "$(ctx::subnets)" "$name"
}