#!/usr/bin/env bash

# ============================================
# Lifecycle
# ============================================

function cmd::block::on_load() {
  flag::register --name
  flag::register --type
  flag::register --force
  flag::register --quiet
  flag::register --ip
  flag::register --port
  flag::register --proto
  flag::register --subnet
  flag::register --block-name

  # System - NET Services
  flag::register --service
}

# ============================================
# Help
# ============================================

function cmd::block::help() {
  cat <<EOF
Usage: wgctl block --name <name> [options]

Block a client entirely or restrict access to specific IPs/ports/subnets/services.
All block rules are persisted and restored on WireGuard restart.
Clients blocked via --ip/--port/--subnet/--service remain in WireGuard
but have specific traffic restricted (shown as 'restricted' in list).

Options:
  --name <name>              Client name (e.g. phone-nuno)
  --type <type>              Device type (optional, combines with --name)
  --ip <ip>                  Block access to specific IP (repeatable)
  --subnet <cidr>            Block access to subnet (repeatable)
  --port <ip:port:proto>     Block specific port, e.g. 10.0.0.210:9000:tcp (repeatable)
  --service <name>           Block a named service (e.g. proxmox, truenas:web-ui) (repeatable)
  --block-name <name>        Optional name for this block rule
  --quiet                    Suppress output (used by group block)

Examples:
  wgctl block --name phone-nuno
  wgctl block --name nuno --type phone
  wgctl block --name phone-nuno --ip 10.0.0.210
  wgctl block --name phone-nuno --subnet 10.0.0.0/24
  wgctl block --name phone-nuno --port 10.0.0.210:9000:tcp
  wgctl block --name phone-nuno --service proxmox
  wgctl block --name phone-nuno --service truenas:web-ui --block-name "no truenas ui"
  wgctl ban --name phone-nuno
EOF
}

# ============================================
# Block Run
# ============================================

function cmd::block::run() {
  local name="" type="" block_name=""
  local ips=() subnets=() ports=() services=()
  local quiet=false force=false
  local changed=false

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --name)       name="$2";           shift 2 ;;
      --type)       type="$2";           shift 2 ;;
      --ip)         ips+=("$2");         shift 2 ;;
      --block-name) block_name="$2";     shift 2 ;;
      --service)    services+=("$2");    shift 2 ;;
      --force)      force=true;          shift   ;;
      --quiet)      quiet=true;          shift   ;;
      --subnet)     subnets+=("$2");     shift 2 ;;
      --port)       ports+=("$2");       shift 2 ;;
      --help)       cmd::block::help; return ;;
      *)
        log::error "Unknown flag: $1"
        cmd::block::help
        return 1 ;;
    esac
  done

  [[ -z "$name" ]] && log::error "Missing required flag: --name" && \
    cmd::block::help && return 1

  name=$(peers::resolve_and_require "$name" "$type") || return 1

  local client_ip
  client_ip=$(peers::get_ip "$name") || return 1

  # Full block if no specific targets
  if [[ ${#ips[@]} -eq 0 && ${#ports[@]} -eq 0 && \
        ${#subnets[@]} -eq 0 && ${#services[@]} -eq 0 ]]; then
    if peers::is_blocked "$name"; then
      log::wg_warning "Client is already blocked: ${name}"
      return 0
    fi
    monitor::update_endpoint_cache
    cmd::block::_block_all "$name" "$client_ip" "$quiet"
    return 0
  fi

  # Specific rules — check if already fully blocked
  if block::has_file "$name"; then
    local direct
    direct=$(block::is_blocked_direct "$name")
    if [[ "$direct" == "true" ]]; then
      log::wg_warning "${name} is fully blocked — unblock first to add specific rules"
      return 1
    fi
  fi

  # Block specific IPs
  for ip in "${ips[@]}"; do
    ip::require_valid "$ip"
    fw::block_ip "$client_ip" "$ip"
    block::add_rule "$name" "$client_ip" "ip" "${block_name:-}" "$ip"
    $quiet || log::wg_success "${ip} has been blocked for ${name}"
  done

  # Block specific subnets
  for subnet in "${subnets[@]}"; do
    ip::require_valid "$subnet"
    fw::block_subnet "$client_ip" "$subnet"
    block::add_rule "$name" "$client_ip" "subnet" "${block_name:-}" "$subnet"
    $quiet || log::wg_success "${subnet} has been blocked for ${name}"
  done

  # Block specific ports
  for entry in "${ports[@]}"; do
    local b_target b_port b_proto
    IFS=":" read -r b_target b_port b_proto <<< "$entry"
    ip::require_valid "$b_target"
    fw::block_port "$client_ip" "$b_target" "$b_port" "${b_proto:-tcp}"
    block::add_rule "$name" "$client_ip" "port" "${block_name:-}" \
      "$b_target" "$b_port" "${b_proto:-tcp}"
      $quiet || log::wg_success "${client_ip}:${b_port}:${b_proto:-tcp} has been blocked for ${name}"
  done

  # Block services
  for svc in "${services[@]}"; do
    local resolved_lines=()
    mapfile -t resolved_lines < <(net::resolve "$svc" 2>/dev/null)
    if [[ ${#resolved_lines[@]} -eq 0 ]]; then
      log::error "Service not found or has no ports: ${svc}"
      return 1
    fi

    # Check if already blocked
    local already_blocked=true
    for resolved in "${resolved_lines[@]}"; do
      if [[ "$resolved" == *:*:* ]]; then
        local b_ip b_port b_proto
        IFS=":" read -r b_ip b_port b_proto <<< "$resolved"
        fw::has_block_rule "$client_ip" "$b_ip" "$b_port" "$b_proto"  2>/dev/null || \
          { already_blocked=false; break; }
      else
        fw::has_block_rule "$client_ip" "$resolved" 2>/dev/null || \
          { already_blocked=false; break; }
      fi
    done

    if $already_blocked; then
      $quiet || log::wg_warning "${svc} is already blocked for ${name}"
      continue
    fi
    
    for resolved in "${resolved_lines[@]}"; do
      if [[ "$resolved" == *:*:* ]]; then
        local b_ip b_port b_proto
        IFS=":" read -r b_ip b_port b_proto <<< "$resolved"
        fw::block_port "$client_ip" "$b_ip" "$b_port" "$b_proto"
        block::add_rule "$name" "$client_ip" "port" "$svc" \
          "$b_ip" "$b_port" "$b_proto"
      else
        fw::block_ip "$client_ip" "$resolved"
        block::add_rule "$name" "$client_ip" "ip" "$svc" "$resolved"
      fi
    done

    changed=true
    $quiet || log::wg_success "${svc} has been blocked for ${name}"
  done

  [[ ${#ips[@]} -gt 0 || ${#ports[@]} -gt 0 || \
      ${#subnets[@]} -gt 0 ]] && changed=true

  # Reapply in correct order: rule ACCEPT first, then peer DROP rules
  # Only reorder if rules were actually added
  if $changed; then
    local peer_rule
    peer_rule=$(peers::get_meta "$name" "rule")
    if [[ -n "$peer_rule" ]] && rule::exists "$peer_rule"; then
      fw::flush_peer "$client_ip"
      rule::apply "$peer_rule" "$client_ip" "$name"
      block::restore_rules_for "$name" "$client_ip"
    fi
  fi

  return 0
}

function cmd::block::_get_endpoint() {
  local name="$1" public_key="$2"
  local endpoint
  endpoint=$(monitor::endpoint_for_key "$public_key")
  if [[ -z "$endpoint" || "$endpoint" == "(none)" ]]; then
    endpoint=$(monitor::get_cached_endpoint "$name")
  fi
  echo "$endpoint"
}

function cmd::block::_block_all() {
  local name="${1:?name required}"
  local client_ip="${2:?client_ip required}"
  local quiet="${3:-false}"

  # Apply fw rules and remove from server
  block::apply_full "$name" "$client_ip"

  # Mark as directly blocked
  block::set_direct "$name" "$client_ip" "true"

  $quiet || log::wg_success "${name} has been blocked."
}