internal/dashboard/frontend/src/utils/filters.ts

import { type FilterState } from "src/store";
import { type HoneypotEvent } from "src/types";
import { formatTimestamp, ipToInt, maskIp } from "./formatting";

export interface ChartDataPoint {
  id: number;
  time: Date;
  time_with_ms: string;
  dst_port: number;
  remote_addr: string;
  remote_addr_int: number;
  event: string;
  country?: string;
  city?: string;
  latitude?: number;
  longitude?: number;
}

export function isIpInSubnet(ip: string, subnet: string): boolean {
  if (!subnet.includes("/")) return ip === subnet;
  const [range, bits] = subnet.split("/");
  if (!range || !bits) return false;
  const maskBits = parseInt(bits);
  if (isNaN(maskBits)) return ip === range;
  return maskIp(ip, maskBits) === maskIp(range, maskBits);
}

export function transformToChartPoint(evt: HoneypotEvent): ChartDataPoint {
  const time = new Date(evt.time);
  const remoteAddr = evt.remote_addr!;
  let dstPort = evt.dst_port ?? -1;
  if (evt.event === "icmp_packet") {
    dstPort = -1;
  }
  const event = evt.type + " " + evt.event;
  return {
    id: evt.id,
    time: time,
    time_with_ms: formatTimestamp(evt.time),
    dst_port: dstPort,
    remote_addr: remoteAddr,
    remote_addr_int: ipToInt(remoteAddr),
    event: event,
    country: evt.country,
    city: evt.city,
    latitude: evt.latitude,
    longitude: evt.longitude,
  };
}

export function filterEvents(
  events: HoneypotEvent[],
  filterStore: FilterState,
): HoneypotEvent[] {
  return events.filter((event) => {
    // 1. Type filter
    if (
      filterStore.type &&
      filterStore.type.length > 0 &&
      !filterStore.type.includes(event.type)
    ) {
      return false;
    }

    // 2. Event filter
    if (
      filterStore.event &&
      filterStore.event.length > 0 &&
      !filterStore.event.includes(event.event)
    ) {
      return false;
    }

    // 3. Remote address filter (including ! exclusions)
    const remote_addr = filterStore.remote_addr;
    if (remote_addr && remote_addr.length > 0) {
      const inclusions = remote_addr.filter((ip) => !ip.startsWith("!"));
      const exclusions = remote_addr
        .filter((ip) => ip.startsWith("!"))
        .map((ip) => ip.substring(1));

      if (event.remote_addr) {
        if (exclusions.some((sub) => isIpInSubnet(event.remote_addr!, sub)))
          return false;
        if (
          inclusions.length > 0 &&
          !inclusions.some((sub) => isIpInSubnet(event.remote_addr!, sub))
        )
          return false;
      } else if (inclusions.length > 0) {
        return false;
      }
    }

    // 4. Destination port filter (including ! exclusions)
    if (filterStore.dst_port && filterStore.dst_port.length > 0) {
      const ports = filterStore.dst_port;
      const inclusions = ports.filter((p) => !p.startsWith("!"));
      const exclusions = ports
        .filter((p) => p.startsWith("!"))
        .map((p) => p.substring(1));

      const eventPort = event.dst_port ? String(event.dst_port) : undefined;
      if (eventPort) {
        if (exclusions.includes(eventPort)) return false;
        if (inclusions.length > 0 && !inclusions.includes(eventPort))
          return false;
      } else if (inclusions.length > 0) {
        return false;
      }
    }

    // 5. Standard Field filters
    const fieldFilters = [
      "method",
      "uri",
      "user_agent",
      "username",
      "password",
      "client_version",
      "asn",
      "country",
      "city",
      "domain",
      "fqdn",
    ] as const;

    for (const field of fieldFilters) {
      const filterValues = (filterStore as any)[field] as string[];
      if (filterValues && filterValues.length > 0) {
        let eventValue: string | undefined;
        if (
          field === "user_agent" &&
          event.type === "http" &&
          event.fields?.headers
        ) {
          const headers = event.fields.headers as Record<string, string>;
          eventValue = (
            headers["User-Agent"] || headers["user-agent"]
          )?.toString();
        } else {
          // Check for top-level properties and fields map
          eventValue = (
            event.fields?.[field] ?? (event as any)[field]
          )?.toString();
        }

        const inclusions = filterValues.filter((v) => !v.startsWith("!"));
        const exclusions = filterValues
          .filter((v) => v.startsWith("!"))
          .map((v) => v.substring(1));

        if (eventValue) {
          if (exclusions.some((ex) => eventValue?.includes(ex))) return false;
          if (
            inclusions.length > 0 &&
            !inclusions.some((inc) => eventValue?.includes(inc))
          )
            return false;
        } else if (inclusions.length > 0) {
          return false;
        }
      }
    }

    // 6. JSON fields filter (for custom filters)
    if (filterStore.json_fields) {
      for (const [field, filterValues] of Object.entries(
        filterStore.json_fields,
      )) {
        if (filterValues && filterValues.length > 0) {
          // Standardize value extraction
          const eventValue = (
            event.fields?.[field] ?? (event as any)[field]
          )?.toString();

          const inclusions = filterValues.filter((v) => !v.startsWith("!"));
          const exclusions = filterValues
            .filter((v) => v.startsWith("!"))
            .map((v) => v.substring(1));

          if (eventValue) {
            if (exclusions.some((ex) => eventValue?.includes(ex))) return false;
            if (
              inclusions.length > 0 &&
              !inclusions.some((inc) => eventValue?.includes(inc))
            )
              return false;
          } else if (inclusions.length > 0) {
            return false;
          }
        }
      }
    }

    return true;
  });
}

export function getSparkData(data: ChartDataPoint[]): {
  x: number[];
  y: number[];
} {
  if (data.length === 0) return { x: [], y: [] };

  const firstTime = data[0]!.time.getTime();
  const lastTime = data[data.length - 1]!.time.getTime();

  const points = 50;
  const duration = lastTime - firstTime;
  if (duration === 0) {
    return { x: [firstTime], y: [data.length] };
  }

  const slotMs = duration / (points - 1);

  const binEdges: number[] = [];
  for (let i = 0; i < points; i++) {
    binEdges.push(firstTime + i * slotMs);
  }

  const slotCounts = new Array(points).fill(0);

  data.forEach((d) => {
    const t = d.time.getTime();
    let idx = Math.round((t - firstTime) / slotMs);
    if (idx < 0) idx = 0;
    if (idx >= points) idx = points - 1;
    slotCounts[idx]++;
  });

  return { x: binEdges, y: slotCounts };
}