internal/dashboard/frontend/src/composables/useChartSelection.ts

import { useEventListener } from "@vueuse/core";
import { storeToRefs } from "pinia";
import Plotly from "plotly.js-dist-min";
import { useChartStore, useUIStore } from "src/store";
import {
  defaultMarkerSize,
  selectedColor,
  selectedMarkerSize,
  unselectedMarkerSize,
  unselectedOpacity,
} from "utils/chart";
import { watch, type Ref } from "vue";

export function useChartSelection(
  chartContainer: Ref<HTMLDivElement | undefined>,
  allColors: Ref<number[]>,
  allRemoteAddrs: Ref<any[]>,
  allPorts?: Ref<number[]>,
  allSizes?: Ref<number[]>,
  allCounts?: Ref<number[]>,
  allCountries?: Ref<string[]>,
) {
  const chartStore = useChartStore().state;
  const uiStore = useUIStore();
  const {
    selectedIP,
    selectedPort,
    selectedCountry,
    selectedCount,
    selectedIPCount,
  } = storeToRefs(uiStore);

  function applyHighlight() {
    if (!chartContainer.value || !(chartContainer.value as any).data?.length)
      return;

    if (!selectedIP.value && !selectedPort.value && !selectedCountry.value) {
      resetSelectionVisuals();
      return;
    }

    if (selectedIP.value) {
      highlightIPVisuals(selectedIP.value);
    } else if (selectedPort.value) {
      highlightPortVisuals(selectedPort.value);
    } else if (selectedCountry.value) {
      highlightCountryVisuals(selectedCountry.value);
    }
  }

  function resetSelectionVisuals() {
    if (!chartContainer.value || !(chartContainer.value as any).data?.length)
      return;
    const resetData: any = {
      "marker.color": [allColors.value.slice()],
      "marker.opacity": [1],
    };
    if (allSizes?.value) {
      resetData["marker.size"] = [allSizes.value.slice()];
    } else {
      resetData["marker.size"] = [defaultMarkerSize];
    }
    Plotly.restyle(chartContainer.value, resetData);
    selectedCount.value = 0;
    selectedIPCount.value = 0;
  }

  function resetSelection() {
    uiStore.resetSelection();
    resetSelectionVisuals();
  }

  function toggleIPExclusion(ip: string) {
    const excluded = chartStore.remote_addr.includes("!" + ip);
    if (excluded) {
      chartStore.remote_addr = chartStore.remote_addr.filter(
        (a) => a !== "!" + ip,
      );
    } else {
      chartStore.remote_addr = [...chartStore.remote_addr, "!" + ip];
    }
  }

  function deleteSelectedIP() {
    if (selectedIP.value) {
      toggleIPExclusion(selectedIP.value);
      resetSelection();
    }
  }

  function highlightIP(ip: string) {
    if (selectedIP.value === ip) {
      resetSelection();
      return;
    }
    selectedIP.value = ip;
    selectedPort.value = undefined;
    selectedCountry.value = undefined;
    highlightIPVisuals(ip);
  }

  function highlightIPVisuals(ip: string) {
    if (!chartContainer.value || !(chartContainer.value as any).data?.length)
      return;

    const isMatch = (val: any) => {
      if (Array.isArray(val)) return val.includes(ip);
      return val === ip;
    };

    const uniqueIPs = new Set<string>();
    const matchingCount = allRemoteAddrs.value.reduce((acc, val, i) => {
      if (isMatch(val)) {
        if (Array.isArray(val)) val.forEach((v) => uniqueIPs.add(v));
        else uniqueIPs.add(val);
        return acc + (allCounts?.value?.[i] || 1);
      }
      return acc;
    }, 0);

    const newColorArr = allColors.value.map((origColor, i) =>
      isMatch(allRemoteAddrs.value[i]) ? selectedColor : origColor,
    );
    const newOpacityArr = allRemoteAddrs.value.map((val) =>
      isMatch(val) ? 1 : unselectedOpacity,
    );

    const restyleData: any = {
      "marker.color": [newColorArr],
      "marker.opacity": [newOpacityArr],
      "marker.line.width": [0],
    };

    if (allSizes?.value) {
      // Keep original sizes for map or other dynamic charts
      restyleData["marker.size"] = [allSizes.value];
    } else {
      restyleData["marker.size"] = [
        allRemoteAddrs.value.map((val) =>
          isMatch(val) ? selectedMarkerSize : unselectedMarkerSize,
        ),
      ];
    }

    Plotly.restyle(chartContainer.value, restyleData);
    selectedCount.value = matchingCount;
    selectedIPCount.value = uniqueIPs.size;
  }

  function highlightPort(portValue: number | string) {
    const port = Number(portValue);
    if (selectedPort.value === port) {
      resetSelection();
      return;
    }
    selectedPort.value = port;
    selectedIP.value = undefined;
    selectedCountry.value = undefined;
    highlightPortVisuals(port);
  }

  function highlightPortVisuals(port: number) {
    if (
      !chartContainer.value ||
      !(chartContainer.value as any).data?.length ||
      !allPorts?.value
    )
      return;

    const uniqueIPs = new Set<string>();
    const matchingCount = allPorts.value.reduce((acc, p, i) => {
      if (p === port) {
        const addr = allRemoteAddrs.value[i];
        if (Array.isArray(addr)) addr.forEach((a) => uniqueIPs.add(a));
        else uniqueIPs.add(addr);
        return acc + (allCounts?.value?.[i] || 1);
      }
      return acc;
    }, 0);

    const newColorArr = allColors.value.map((origColor, i) =>
      allPorts.value![i] === port ? selectedColor : origColor,
    );
    const newOpacityArr = allPorts.value.map((p) =>
      p === port ? 1 : unselectedOpacity,
    );

    const restyleData: any = {
      "marker.color": [newColorArr],
      "marker.opacity": [newOpacityArr],
      "marker.line.width": [0],
    };

    if (allSizes?.value) {
      restyleData["marker.size"] = [allSizes.value];
    } else {
      restyleData["marker.size"] = [
        allPorts.value.map((p) =>
          p === port ? selectedMarkerSize : unselectedMarkerSize,
        ),
      ];
    }

    Plotly.restyle(chartContainer.value, restyleData);
    selectedCount.value = matchingCount;
    selectedIPCount.value = uniqueIPs.size;
  }

  function highlightCountry(country: string) {
    if (selectedCountry.value === country) {
      resetSelection();
      return;
    }
    selectedCountry.value = country;
    selectedIP.value = undefined;
    selectedPort.value = undefined;
    highlightCountryVisuals(country);
  }

  function highlightCountryVisuals(country: string) {
    if (
      !chartContainer.value ||
      !(chartContainer.value as any).data?.length ||
      !allCountries?.value
    )
      return;

    const uniqueIPs = new Set<string>();
    const matchingCount = allCountries.value.reduce((acc, c, i) => {
      if (c === country) {
        const addr = allRemoteAddrs.value[i];
        if (Array.isArray(addr)) addr.forEach((a) => uniqueIPs.add(a));
        else uniqueIPs.add(addr);
        return acc + (allCounts?.value?.[i] || 1);
      }
      return acc;
    }, 0);

    const newColorArr = allColors.value.map((origColor, i) =>
      allCountries.value[i] === country ? selectedColor : origColor,
    );
    const newOpacityArr = allCountries.value.map((c) =>
      c === country ? 1 : unselectedOpacity,
    );

    const restyleData: any = {
      "marker.color": [newColorArr],
      "marker.opacity": [newOpacityArr],
      "marker.line.width": [0],
    };

    if (allSizes?.value) {
      restyleData["marker.size"] = [allSizes.value];
    } else {
      restyleData["marker.size"] = [
        allCountries.value.map((c) =>
          c === country ? selectedMarkerSize : unselectedMarkerSize,
        ),
      ];
    }

    Plotly.restyle(chartContainer.value, restyleData);
    selectedCount.value = matchingCount;
    selectedIPCount.value = uniqueIPs.size;
  }

  function handleKeyDown(e: KeyboardEvent) {
    const active = document.activeElement;
    if (
      active &&
      (active.tagName === "INPUT" ||
        active.tagName === "TEXTAREA" ||
        (active as HTMLElement).isContentEditable)
    ) {
      return;
    }

    if (e.key === "Escape" || e.key === "Esc") {
      resetSelection();
    } else if (e.key === "Delete" || e.key === "Del") {
      deleteSelectedIP();
    }
  }

  useEventListener(document, "keydown", handleKeyDown);

  watch(
    [
      chartContainer,
      allColors,
      allRemoteAddrs,
      allPorts,
      allSizes,
      allCounts,
      allCountries,
    ].filter((s) => s !== undefined),
    () => {
      applyHighlight();
    },
    { flush: "post" },
  );

  return {
    selectedIP,
    selectedPort,
    selectedCountry,
    selectedCount,
    selectedIPCount,
    selectedColor,
    highlightIP,
    highlightPort,
    highlightCountry,
    resetSelection,
    deleteSelectedIP,
    toggleIPExclusion,
  };
}