// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {useEffect, useRef, useState} from "react";

import type {LogFilterPredicate} from "./useLogs.ts";

import styles from "./Filters.module.css";

/**
 * A generic setter type compatible with React's state setters.
 */
type Setter<T> = (value: T | ((prev: T) => T)) => void;

/**
 * Mapping of log level names to their corresponding numeric severity.
 * Used for comparison in log filtering predicates.
 */
const optionMapping: Map<string, number> = new Map([
  ["ALL", 0],
  ["DEBUG", 10],
  ["INFO", 20],
  ["WARNING", 30],
  ["ERROR", 40],
  ["CRITICAL", 50],
  ["NONE", 999_999_999_999], // It is technically possible to have a higher level, but this is fine
]);

/**
 * Renders a single log-level selector (dropdown) for a specific filter target.
 *
 * Used by both the global filter and agent-specific filters.
 *
 * @param name - The display name or identifier for the filter target.
 * @param level - The currently selected log level.
 * @param setLevel - Function to update the selected log level.
 * @param onDelete - Optional callback for deleting this filter element.
 * @returns A JSX element that renders a labeled dropdown for selecting log levels.
 */
function LevelPredicateElement({
  name,
  level,
  setLevel,
  onDelete,
}: {
  name: string;
  level: string;
  setLevel: (level: string) => void;
  onDelete?: () => void;
}) {
  const normalizedName = name.split(".").pop() || name;

  return <div className={"flex-row gap-sm align-center"}>
    <label
      htmlFor={`log_level_${name}`}
      className={"font-small"}
    >
      {onDelete
        ? <button
          className={`no-button ${styles.deletable}`}
          onClick={onDelete}
        >{normalizedName}:</button>
        : normalizedName + ':'
      }
    </label>
    <select
      id={`log_level_${name}`}
      value={level}
      onChange={(e) => setLevel(e.target.value)}
    >
      {Array.from(optionMapping.keys()).map((key) => (
        <option key={key} value={key}>{key}</option>
      ))}
    </select>
  </div>
}

/** Key used for the global log-level predicate in the filter map. */
const GLOBAL_LOG_LEVEL_PREDICATE_KEY = "global_log_level";

/**
 * Renders and manages the **global log-level filter**.
 *
 * This component defines a baseline log level that all logs must meet or exceed
 * to be displayed, unless overridden by per-agent filters.
 *
 * @param filterPredicates - Map of current log filter predicates.
 * @param setFilterPredicates - Setter function to update the filter predicates map.
 * @returns A JSX element rendering the global log-level selector.
 */
function GlobalLevelFilter({
  filterPredicates,
  setFilterPredicates,
}: {
  filterPredicates: Map<string, LogFilterPredicate>;
  setFilterPredicates: Setter<Map<string, LogFilterPredicate>>;
}) {
  const selected = filterPredicates.get(GLOBAL_LOG_LEVEL_PREDICATE_KEY)?.value ?? "ALL";
  const setSelected = (selected: string | null) => {
    if (!selected || !optionMapping.has(selected)) return;

    setFilterPredicates((curr) => {
      const next = new Map(curr);
      next.set(GLOBAL_LOG_LEVEL_PREDICATE_KEY, {
        predicate: (record) => record.levelno >= optionMapping.get(selected)!,
        priority: 0,
        value: selected,
      });
      return next;
    });
  }

  // Initialize default global level on mount.
  useEffect(() => {
    if (filterPredicates.has(GLOBAL_LOG_LEVEL_PREDICATE_KEY)) return;
    setSelected("INFO");
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []); // Run only once when the component mounts, not when anything changes

  return <LevelPredicateElement
    name={"Global"}
    level={selected}
    setLevel={setSelected}
  />;
}

/** Prefix for agent-specific log-level predicate keys in the filter map. */
const AGENT_LOG_LEVEL_PREDICATE_KEY_PREFIX = "agent_log_level_";

/**
 * Renders and manages **per-agent log-level filters**.
 *
 * Allows the user to set specific log levels for individual agents, overriding
 * the global filter for those agents. Includes functionality to add, edit,
 * or remove agent-level filters.
 *
 * @param filterPredicates - Map of current log filter predicates.
 * @param setFilterPredicates - Setter function to update the filter predicates map.
 * @param agentNames - Set of agent names available for filtering.
 * @returns A JSX element rendering agent-level filters and a dropdown to add new ones.
 */
function AgentLevelFilters({
  filterPredicates,
  setFilterPredicates,
  agentNames,
}: {
  filterPredicates: Map<string, LogFilterPredicate>;
  setFilterPredicates: Setter<Map<string, LogFilterPredicate>>;
  agentNames: Set<string>;
}) {
  const rootRef = useRef<HTMLDivElement>(null);
  const [open, setOpen] = useState(false);

  // Close dropdown or panels when clicking outside or pressing Escape.
  useEffect(() => {
    if (!open) return;
    const onDocClick = (e: MouseEvent) => {
      if (!rootRef.current?.contains(e.target as Node)) setOpen(false);
    };
    const onKey = (e: KeyboardEvent) => {
      if (e.key !== "Escape") return;
      setOpen(false);
      e.preventDefault(); // Don't exit fullscreen mode
    };
    document.addEventListener("mousedown", onDocClick);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onDocClick);
      document.removeEventListener("keydown", onKey);
    };
  }, [open]);

  // Identify which predicates correspond to agents.
  const agentPredicates = [...filterPredicates.keys()].filter((key) =>
    key.startsWith(AGENT_LOG_LEVEL_PREDICATE_KEY_PREFIX));

 /**
   * Creates or updates the log filter predicate for a specific agent.
   * Falls back to the global log level if no level is specified.
   *
   * @param agentName - The name of the agent to filter.
   * @param level - Optional log level to apply; defaults to the global level.
   */
  const setAgentPredicate = (agentName: string, level?: string ) => {
    level = level ?? filterPredicates.get(GLOBAL_LOG_LEVEL_PREDICATE_KEY)?.value ?? "ALL";
    setFilterPredicates((prev) => {
      const next = new Map(prev);
      next.set(AGENT_LOG_LEVEL_PREDICATE_KEY_PREFIX + agentName, {
        predicate: (record) => record.name === agentName
          ? record.levelno >= optionMapping.get(level!)!
          : null,
        priority: 1,
        value: {agentName, level},
      });
      return next;
    });
  }

  /**
   * Deletes the log filter predicate for a specific agent.
   *
   * @param agentName - The name of the agent whose filter should be removed.
   */
  const deleteAgentPredicate = (agentName: string) => {
    setFilterPredicates((curr) => {
      const fullName = AGENT_LOG_LEVEL_PREDICATE_KEY_PREFIX + agentName;
      if (!curr.has(fullName)) return curr; // Return unchanged, no re-render
      const next = new Map(curr);
      next.delete(fullName);
      return next;
    });
  }

  return <>
    {agentPredicates.map((key) => {
      const {agentName, level} = filterPredicates.get(key)!.value;

      return <LevelPredicateElement
        key={key}
        name={agentName}
        level={level}
        setLevel={(level) => setAgentPredicate(agentName, level)}
        onDelete={() => deleteAgentPredicate(agentName)}
      />;
    })}
    <div className={"flex-row gap-sm align-center"}>
      <label htmlFor={"add_agent"} className={"font-small"}>Add:</label>
      <select
        id={"add_agent"}
        value={""}
        onChange={(e) => !!e.target.value && setAgentPredicate(e.target.value)}
      >
        {["", ...agentNames.keys()].map((key) => (
          <option key={key} value={key}>{key.split(".").pop()}</option>
        ))}
      </select>
    </div>
  </>;
}

/**
 * Main Filters component that aggregates global and per-agent log filters.
 *
 * Combines the global log-level filter and agent-specific filters into a unified UI.
 * Updates a shared `Map<string, LogFilterPredicate>` to determine which logs are shown.
 *
 * @param filterPredicates - The map of all active log filter predicates.
 * @param setFilterPredicates - Setter to update the map of predicates.
 * @param agentNames - Set of available agent names to display filters for.
 * @returns A React component that renders all log filter controls.
 */
export default function Filters({
  filterPredicates,
  setFilterPredicates,
  agentNames,
}: {
  filterPredicates: Map<string, LogFilterPredicate>;
  setFilterPredicates: Setter<Map<string, LogFilterPredicate>>;
  agentNames: Set<string>;
}) {
  return <div className={"flex-1 flex-row flex-wrap gap-md align-center"}>
    <GlobalLevelFilter filterPredicates={filterPredicates} setFilterPredicates={setFilterPredicates} />
    <AgentLevelFilters filterPredicates={filterPredicates} setFilterPredicates={setFilterPredicates} agentNames={agentNames} />
  </div>;
}
