#!/usr/bin/env bash
# activity.command.sh — WireGuard activity snapshot

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

function cmd::activity::on_load() {
  command::mixin json_output

  load_module net

  flag::register --peer
  flag::register --service
  flag::register --ip
  flag::register --hours
  flag::register --type
  flag::register --accept
  flag::register --drop
  flag::register --external
  flag::register --ports
  flag::register --exclude-service
  flag::register --include-service
  
  flag::exclusive --accept --drop
}

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

function cmd::activity::help() {
  cat <<EOF
Usage: wgctl activity [options]

Show WireGuard activity — transfer totals and firewall drops per peer.
Data sources: wg show transfer, fw_events.log

Options:
  --peer <name>       Filter by peer name
  --service <name>    Filter by service (e.g. truenas, proxmox:web-ui)
  --ip <ip>           Filter by destination IP
  --hours <n>         Time window in hours (default: 24, 0 = all time)
  --type <type>       Filter by device type (combined with --peer)
  --accept            Show only accepted traffic (from conntrack)
  --drop              Show only firewall drops
  --external          Show only external traffic (full tunnel peers)

Examples:
  wgctl activity
  wgctl activity --peer phone-nuno
  wgctl activity --service truenas
  wgctl activity --hours 0
  wgctl activity --ip 10.0.0.101
EOF
}

# ============================================
# Run
# ============================================

function cmd::activity::run() {
  local filter_peer="" filter_service="" filter_ip="" filter_type=""
  local hours=24
  local accept_only=false drop_only=false external_only=false show_ports=false
  local -a exclude_services=() include_services=()

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --peer)     filter_peer="$2";    shift 2 ;;
      --service)  filter_service="$2"; shift 2 ;;
      --ip)       filter_ip="$2";      shift 2 ;;
      --type)     filter_type="$2";    shift 2 ;;
      --hours)    hours="$2";          shift 2 ;;
      --accept)   accept_only=true;    shift   ;;
      --drop)     drop_only=true;      shift   ;;
      --external) external_only=true;  shift   ;;
      --ports)    show_ports=true;     shift   ;;
      --exclude-service) exclude_services+=("$2"); shift 2 ;;
      --include-service) include_services+=("$2"); shift 2 ;;
      --help)     cmd::activity::help; return  ;;
      *)
        log::error "Unknown flag: $1"
        cmd::activity::help
        return 1
        ;;
    esac
  done

  if command::json; then
    cmd::activity::_output_json "$hours"
    return 0
  fi

  if [[ -n "$filter_peer" && -n "$filter_type" ]]; then
    filter_peer=$(peers::resolve_and_require "$filter_peer" "$filter_type") || return 1
  fi

  local service_ip=""
  if [[ -n "$filter_service" ]]; then
    service_ip=$(net::resolve "$filter_service" 2>/dev/null | head -1 | cut -d: -f1) || true
    [[ -z "$service_ip" ]] && log::error "Service not found: ${filter_service}" && return 1
  fi
  [[ -n "$filter_ip" ]] && service_ip="$filter_ip"

  # Build final exclusion list — remove any --include-service entries
  local -a final_excludes=()
  for svc in "${exclude_services[@]:-}"; do
    local included=false
    for inc in "${include_services[@]:-}"; do
      [[ "$svc" == "$inc" ]] && included=true && break
    done
    $included || final_excludes+=("$svc")
  done
 
  # Build exclude string for Python (space-separated)
  local exclude_str=""
  [[ ${#final_excludes[@]} -gt 0 ]] && \
    exclude_str=$(IFS=' '; echo "${final_excludes[*]}")

  # ── Fetch data ──
  local data=""
  if ! $accept_only; then
    data=$(json::activity_aggregate \
      "$(ctx::fw_events_log)" "$(ctx::events_log)" \
      "$(config::interface)" "$(ctx::net)" \
      "$(ctx::clients)" "$(ctx::meta)" \
      "$hours" "$filter_peer" "$service_ip" "$exclude_str" 2>/dev/null)
  fi

  local accept_data=""
  if ! $drop_only; then
    local since_arg="" ext_flag="0"
    [[ "$hours" -gt 0 ]] && since_arg="${hours}h"
    $external_only && ext_flag="1"
    [[ -f "$(ctx::accept_events_log)" ]] && \
      accept_data=$(json::accept_aggregate \
        "$(ctx::accept_events_log)" "$(ctx::net)" "$(ctx::clients)" \
        "$since_arg" "$filter_peer" "$ext_flag" "$exclude_str" 2>/dev/null)
  fi

  [[ -z "$data" && -z "$accept_data" ]] && \
    log::wg_warning "No activity data found" && return 0

  # ── Build accept maps ──
  declare -gA _ACCEPT_PEER=()
  declare -gA _ACCEPT_DEST_KEYS=()
  declare -gA _ACCEPT_DEST=()

  while IFS='|' read -r type rest; do
    [[ -z "$type" ]] && continue
    case "$type" in
      peer)
        local a_name a_bi a_bo a_pi a_po a_conns
        IFS='|' read -r a_name a_bi a_bo a_pi a_po a_conns <<< "$rest"
        _ACCEPT_PEER["$a_name"]="${a_bi}|${a_bo}|${a_pi}|${a_po}|${a_conns}"
        ;;
      dest)
        local d_peer d_ip d_port d_proto d_bytes_orig d_bytes_reply d_count
        IFS='|' read -r d_peer d_ip d_port d_proto d_bytes_orig d_bytes_reply d_count <<< "$rest"
        local d_key="${d_peer}:${d_ip}:${d_port}:${d_proto}"
        _ACCEPT_DEST["$d_key"]="${d_bytes_orig}|${d_bytes_reply}|${d_count}"
        _ACCEPT_DEST_KEYS["$d_peer"]+="${d_key} "
        ;;
    esac
  done <<< "$accept_data"

  # ── Measure column widths ──
  local w_peer=16 w_count=1

  while IFS='|' read -r type rest; do
    case "$type" in
      peer)
        local name drops
        name=$(echo "$rest"  | cut -d'|' -f1)
        drops=$(echo "$rest" | cut -d'|' -f4)
        (( ${#name}  > w_peer  )) && w_peer=${#name}
        (( ${#drops} > w_count )) && w_count=${#drops}
        ;;
      service)
        local svc_count
        svc_count=$(echo "$rest" | cut -d'|' -f3)
        (( ${#svc_count} > w_count )) && w_count=${#svc_count}
        ;;
    esac
  done <<< "$data"

  for a_name in "${!_ACCEPT_PEER[@]}"; do
    (( ${#a_name} > w_peer )) && w_peer=${#a_name}
    local a_conns_val="${_ACCEPT_PEER[$a_name]##*|}"
    (( ${#a_conns_val} > w_count )) && w_count=${#a_conns_val}
  done
  for key in "${!_ACCEPT_DEST[@]}"; do
    local d_val="${_ACCEPT_DEST[$key]}"
    local d_count_val="${d_val##*|}"
    (( ${#d_count_val} > w_count )) && w_count=${#d_count_val}
  done

  (( w_peer += 2 ))
  local drops_col=$(( w_peer + 30 ))

  local hours_display="${hours}h"
  [[ "$hours" == "0" ]] && hours_display="all time"

  log::section "Activity Monitor (last ${hours_display})"
  echo ""

  if display::is_table "activity"; then
    cmd::activity::_render_table "$data"
    return 0
  fi

  # ── Accept dest inline renderer ──
  _render_peer_accept_dests() {
      local peer_name="$1"
      local keys="${_ACCEPT_DEST_KEYS[$peer_name]:-}"
      [[ -z "$keys" ]] && return 0
      for d_key in $keys; do
        local dest_stats="${_ACCEPT_DEST[$d_key]:-}"
        [[ -z "$dest_stats" ]] && continue
        local d_bytes_orig d_bytes_reply d_count
        IFS='|' read -r d_bytes_orig d_bytes_reply d_count <<< "$dest_stats"
        local rest_key="${d_key#${peer_name}:}"
        local d_ip="${rest_key%%:*}"
        local pp="${rest_key#*:}"
        local d_port="${pp%%:*}"
        local d_proto="${pp##*:}"
        local spec="${d_ip}:${d_port}:${d_proto}"
        local dest_display
        local raw_suffix=""
        local resolved="${_DEST_RESOLVE_CACHE[$spec]:-${d_ip}:${d_port}/${d_proto}}"
        local dest_display="$resolved"
        if [[ "$show_ports" == "true" && "$resolved" != "${d_ip}:"* && "$resolved" != "${d_ip} "* ]]; then
          if [[ -n "$d_port" && "$d_port" != "0" ]]; then
            dest_display=$(printf "%s \033[2m(%s:%s)\033[0m" "$resolved" "$d_ip" "$d_port")
          else
            dest_display=$(printf "%s \033[2m(%s)\033[0m" "$resolved" "$d_ip")
          fi
        fi
        ui::activity::accept_dest_row \
          "$dest_display" "$d_bytes_orig" "$d_bytes_reply" \
          "$d_count" "$drops_col" "$w_count"
      done
    }

  declare -gA _DEST_RESOLVE_CACHE=()
  local -a _dest_specs=()
  for _dk in "${!_ACCEPT_DEST[@]}"; do
    # key format: peer:ip:port:proto — strip peer prefix
    local _rest="${_dk#*:}"
    local _dip="${_rest%%:*}"
    local _pp="${_rest#*:}"
    local _dport="${_pp%%:*}"
    local _dproto="${_pp##*:}"
    local _spec="${_dip}:${_dport}:${_dproto}"
    # Deduplicate
    local _found=false
    for _s in "${_dest_specs[@]:-}"; do
      [[ "$_s" == "$_spec" ]] && _found=true && break
    done
    $_found || _dest_specs+=("$_spec")
  done

  if [[ ${#_dest_specs[@]} -gt 0 ]]; then
    while IFS='|' read -r _spec _display; do
      [[ -n "$_spec" ]] && _DEST_RESOLVE_CACHE["$_spec"]="$_display"
    done < <(json::batch_resolve_dest "${_dest_specs[@]}" 2>/dev/null)
  fi

  local first_peer=true skip_peer=false current_name=""
  local -a rendered_peers=()

  # ── Main render loop (drop data) ──
  while IFS='|' read -r record_type rest; do
    case "$record_type" in
      peer)
        local name rx tx drops
        IFS='|' read -r name rx tx drops <<< "$rest"

        # Flush previous peer's accept dests
        [[ -n "$current_name" ]] && ! $drop_only && \
          _render_peer_accept_dests "$current_name"

        skip_peer=false
        current_name="$name"
        local has_accept="${_ACCEPT_PEER[$name]:-}"

        $first_peer || echo ""
        first_peer=false
        rendered_peers+=("$name")

        local rx_fmt tx_fmt
        rx_fmt=$(fmt::bytes "$rx")
        tx_fmt=$(fmt::bytes "$tx")
        local name_pad rx_pad tx_pad
        name_pad=$(printf "%-${w_peer}s" "$name")
        rx_pad=$(printf   "%-10s"        "$rx_fmt")
        tx_pad=$(printf   "%-10s"        "$tx_fmt")

        local drop_word="drops"
        [[ "$drops" -eq 1 ]] && drop_word="drop"

        # Always show peer name — either full row or name-only for accept_only
        if $accept_only; then
          printf "  \033[1m%s\033[0m\n" "$name_pad"
        else
          ui::activity::peer_row \
            "$name_pad" "$rx_pad" "$tx_pad" "$drops" "$drop_word" "$w_count"
        fi

        # Accept summary row
        if [[ -n "$has_accept" ]] && ! $drop_only; then
          local a_bi a_bo a_pi a_po a_conns
          IFS='|' read -r a_bi a_bo a_pi a_po a_conns <<< "$has_accept"
          ui::activity::accept_row \
            "$name_pad" \
            "$(printf '%-10s' "$(fmt::bytes "$a_bi")")" \
            "$(printf '%-10s' "$(fmt::bytes "$a_bo")")" \
            "$a_conns" "$w_count"
        fi
        ;;

      service)
        local peer dest_display dst_ip dst_port proto drop_count
        IFS='|' read -r peer dest_display dst_ip dst_port proto drop_count <<< "$rest"
        # Build dim suffix if --ports
        local svc_display="$dest_display"
        if [[ "$show_ports" == "true" && -n "$dst_ip" ]]; then
          if [[ -n "$dst_port" ]]; then
            svc_display=$(printf "%s \033[2m(%s:%s)\033[0m" \
              "$dest_display" "$dst_ip" "$dst_port")
          else
            svc_display=$(printf "%s \033[2m(%s)\033[0m" \
              "$dest_display" "$dst_ip")
          fi
        fi
        local svc_drop_word="drops"
        [[ "$drop_count" -eq 1 ]] && svc_drop_word="drop"
        $accept_only || ui::activity::service_row \
          "$svc_display" "$drop_count" "$svc_drop_word" "$drops_col" "$w_count"
        ;;
    esac
  done <<< "$data"

  # Flush last peer's accept dests
  [[ -n "$current_name" ]] && ! $drop_only && \
    _render_peer_accept_dests "$current_name"

  # ── Accept-only peers (not in drop data) ──
  if ! $drop_only; then
    for a_name in $(echo "${!_ACCEPT_PEER[@]}" | tr ' ' '\n' | sort); do
      # Skip already rendered
      local already=false
      for rp in "${rendered_peers[@]:-}"; do
        [[ "$rp" == "$a_name" ]] && already=true && break
      done
      $already && continue

      $first_peer || echo ""
      first_peer=false

      local a_stats="${_ACCEPT_PEER[$a_name]}"
      local a_bi a_bo a_pi a_po a_conns
      IFS='|' read -r a_bi a_bo a_pi a_po a_conns <<< "$a_stats"
      local name_pad
      name_pad=$(printf "%-${w_peer}s" "$a_name")

      # Always show peer name
      printf "  \033[1m%s\033[0m\n" "$name_pad"

      ui::activity::accept_row \
        "$name_pad" \
        "$(printf '%-10s' "$(fmt::bytes "$a_bi")")" \
        "$(printf '%-10s' "$(fmt::bytes "$a_bo")")" \
        "$a_conns" "$w_count"
      _render_peer_accept_dests "$a_name"
    done
  fi

  echo ""
}

function cmd::activity::_render_table() {
  local data="${1:-}"
  [[ -z "$data" ]] && return 0
 
  ui::activity::header_table
  local skip_peer=false
  while IFS='|' read -r record_type rest; do
    case "$record_type" in
      peer)
        local name rx tx drops
        IFS='|' read -r name rx tx drops <<< "$rest"
        skip_peer=false
        local rx_fmt tx_fmt
        rx_fmt=$(fmt::bytes "$rx")
        tx_fmt=$(fmt::bytes "$tx")
        ui::activity::peer_row_table "$name" "$rx_fmt" "$tx_fmt" "$drops" ""
        ;;
      service)
        $skip_peer && continue
        local peer dest count
        IFS='|' read -r peer dest count <<< "$rest"
        ui::activity::service_row_table "$dest" "$count" "drops"
        ;;
    esac
  done <<< "$data"
}
 

function cmd::activity::_output_json() {
  local hours="${1:-24}"
  local data
  data=$(json::activity_aggregate \
    "$(ctx::fw_events_log)" "$(ctx::events_log)" \
    "$(config::interface)" "$(ctx::net)" \
    "$(ctx::clients)" "$(ctx::meta)" \
    "$hours" "" "" 2>/dev/null)
 
  local -a peers=()
  local current_peer="" current_services=""
  local -a current_svc_list=()
 
  while IFS='|' read -r record_type rest; do
    case "$record_type" in
      peer)
        # Flush previous peer
        if [[ -n "$current_peer" ]]; then
          local svc_array
          svc_array=$(printf '%s\n' "${current_svc_list[@]:-}" | paste -sd ',' -)
          peers+=("${current_peer},\"services\":[${svc_array:-}]}")
          current_svc_list=()
        fi
        local name rx tx drops
        IFS='|' read -r name rx tx drops <<< "$rest"
        current_peer=$(printf '{"name":"%s","rx":%s,"tx":%s,"drops":%s' \
          "$name" "$rx" "$tx" "$drops")
        ;;
      service)
        local peer dest count
        IFS='|' read -r peer dest count <<< "$rest"
        current_svc_list+=("$(printf '{"dest":"%s","drops":%s}' "$dest" "$count")")
        ;;
    esac
  done <<< "$data"
 
  # Flush last peer
  if [[ -n "$current_peer" ]]; then
    local svc_array
    svc_array=$(printf '%s\n' "${current_svc_list[@]:-}" | paste -sd ',' -)
    peers+=("${current_peer},\"services\":[${svc_array:-}]}")
  fi
 
  local count=${#peers[@]}
  local array
  array=$(printf '%s\n' "${peers[@]:-}" | paste -sd ',' -)
  printf '{"peers":[%s]}' "${array:-}" | json::envelope "activity" "$count"
}

function cmd::activity::_fetch_accept_data() {
  local hours="${1:-24}" filter_peer="${2:-}" external_only="${3:-false}"
 
  [[ ! -f "$(ctx::accept_events_log)" ]] && return 0
 
  local since_arg=""
  [[ "$hours" -gt 0 ]] && since_arg="${hours}h"
 
  local ext_flag="0"
  $external_only && ext_flag="1"
 
  json::accept_aggregate \
    "$(ctx::accept_events_log)" \
    "$(ctx::net)" \
    "$(ctx::clients)" \
    "$since_arg" \
    "$filter_peer" \
    2>/dev/null
}

function cmd::activity::_build_accept_maps() {
  local accept_data="${1:-}"
  # Outputs to stdout as bash declare statements — use eval
  # Sets: _ACCEPT_PEER[name]="bytes_in|bytes_out|packets_in|packets_out|conn_count"
  #       _ACCEPT_DEST[name:ip:port:proto]="bytes|count"
  declare -gA _ACCEPT_PEER=()
  declare -gA _ACCEPT_DEST=()
 
  while IFS='|' read -r type rest; do
    [[ -z "$type" ]] && continue
    case "$type" in
      peer)
        local name bytes_in bytes_out packets_in packets_out conn_count
        IFS='|' read -r name bytes_in bytes_out packets_in packets_out conn_count <<< "$rest"
        _ACCEPT_PEER["$name"]="${bytes_in}|${bytes_out}|${packets_in}|${packets_out}|${conn_count}"
        ;;
      dest)
        local peer dst_ip dst_port proto bytes count
        IFS='|' read -r peer dst_ip dst_port proto bytes count <<< "$rest"
        _ACCEPT_DEST["${peer}:${dst_ip}:${dst_port}:${proto}"]="${bytes}|${count}"
        ;;
    esac
  done <<< "$accept_data"
}