// 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 {useCallback, useEffect, useRef, useState} from "react";

import {applyPriorityPredicates, type PriorityFilterPredicate} from "../../utils/priorityFiltering.ts";
import {cell, type Cell} from "../../utils/cellStore.ts";
import { API_BASE_URL } from "../../config/api.ts";

type ExtraLevelName = 'LLM' | 'OBSERVATION' | 'ACTION' | 'CHAT';

export type LevelName = ExtraLevelName | 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' | string;

/**
 * Extra fields that are added to log records in the backend but are not part of the standard `LogRecord` type.
 *
 * @property reference - (Optional) A reference identifier linking related log messages.
 * @property role - (Optional) For chat log messages, the role of the agent that generated the message.
 */
type ExtraLogRecordFields = {
  reference?: string;
  role?: string;
}

/**
 * Represents a single log record emitted by the backend logging system.
 *
 * @property name - The name of the logger or source (e.g., `"agent.core"`).
 * @property message - The message content of the log record.
 * @property levelname - The human-readable severity level (e.g., `"INFO"`, `"ERROR"`).
 * @property levelno - The numeric severity value corresponding to `levelname`.
 * @property created - The UNIX timestamp (in seconds) when this record was created.
 * @property relativeCreated - The time (in milliseconds) since the logging system started.
 * @property firstCreated - Timestamp of the first log in this reference group.
 * @property firstRelativeCreated - Relative timestamp of the first log in this reference group.
 */
export type LogRecord = {
  name: string;
  message: string;
  levelname: LevelName;
  levelno: number;
  created: number;
  relativeCreated: number;
  firstCreated: number;
  firstRelativeCreated: number;
} & ExtraLogRecordFields;

/**
 * A log filter predicate with priority support, used to determine whether
 * a log record should be displayed.
 *
 * This extends a general `PriorityFilterPredicate` and includes an optional
 * `value` field for UI metadata (e.g., selected log level or agent).
 *
 * @template T - The type of record being filtered (here, `LogRecord`).
 */
export type LogFilterPredicate = PriorityFilterPredicate<LogRecord> & {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  value: any };

 /**
 * React hook that manages the lifecycle of log records, including:
 * - Receiving live log messages via Server-Sent Events (SSE),
 * - Applying priority-based filtering rules,
 * - Managing distinct logger names and reference-linked messages.
 *
 * Returns both the filtered logs (as reactive `Cell<LogRecord>` objects)
 * and a set of distinct logger names for use in UI components (e.g., Filters).
 *
 * @param filterPredicates - A `Map` of log filter predicates, keyed by ID or type.
 * @returns An object containing:
 * - `filteredLogs`: The currently visible (filtered) log messages.
 * - `distinctNames`: A set of all distinct logger names encountered.
 *
 * @example
 * ```ts
 * const { filteredLogs, distinctNames } = useLogs(activeFilters);
 * ```
 */
export function useLogs(filterPredicates: Map<string, LogFilterPredicate>) {
  /** Distinct logger names encountered across all logs. */
  const [distinctNames, setDistinctNames] = useState<Set<string>>(new Set());
  /** Filtered logs that pass all active predicates, stored as reactive cells. */
  const [filtered, setFiltered] = useState<Cell<LogRecord>[]>([]);

  /** Persistent reference to the active EventSource connection. */
  const sseRef = useRef<EventSource | null>(null);
  /** Keeps a stable reference to the current filter map (avoids re-renders). */
  const filtersRef = useRef(filterPredicates);
  /** Stores all received logs (the unfiltered full history). */
  const logsRef = useRef<LogRecord[]>([]);

  /** Map to store the first message for each reference, instance can be updated to change contents. */
  const firstByRefRef = useRef<Map<string, Cell<LogRecord>>>(new Map());

  /**
   * Apply all active filter predicates to a log record.
   * @param log The log record to apply the filters to.
   * @returns `true` if the record passes all filters; otherwise `false`.
   */
  const applyFilters = useCallback((log: LogRecord) =>
    applyPriorityPredicates(log, [...filtersRef.current.values()]), []);

  /**
   * Fully recomputes the filtered log list based on the current
   * filter predicates and historical logs.
   *
   * Should be invoked whenever the filter map changes.
   */
  const recomputeFiltered = useCallback(() => {
    const newFiltered: Cell<LogRecord>[] = [];
    firstByRefRef.current = new Map();

    for (const message of logsRef.current) {
      const messageCell = cell<LogRecord>({
        ...message,
        firstCreated: message.created,
        firstRelativeCreated: message.relativeCreated,
      });

      // Handle reference grouping: update the first message in the group.
      if (message.reference) {
        const first = firstByRefRef.current.get(message.reference);
        if (first) {
          // Update the first's contents
          first.set((prev) => ({
            ...message,
            firstCreated: prev.firstCreated ?? prev.created,
            firstRelativeCreated: prev.firstRelativeCreated ?? prev.relativeCreated,
          }));

          continue; // Don't add it to the list again (it's a duplicate).
        } else {
          // Add the first message with this reference to the registry
          firstByRefRef.current.set(message.reference, messageCell);
        }
      }

      // Include only if it passes current filters.
      if (applyFilters(message)) {
        newFiltered.push(messageCell);
      }
    }

    setFiltered(newFiltered);
  }, [applyFilters, setFiltered]);

  // Re-filter all logs whenever filter predicates change.
  useEffect(() => {
    filtersRef.current = filterPredicates;
    recomputeFiltered();
  }, [filterPredicates, recomputeFiltered]);

  /**
   * Handles a newly received log record.
   * Updates the full log history, distinct names set, and filtered log list.
   *
   * @param message - The new log record to process.
   */
  const handleNewMessage = useCallback((message: LogRecord) => {
    // Store in complete history for future refiltering.
    logsRef.current.push(message);

    // Track distinct logger names.
    setDistinctNames((prev) => {
      if (prev.has(message.name)) return prev;
      const newSet = new Set(prev);
      newSet.add(message.name);
      return newSet;
    });

    // Wrap in a reactive cell for UI binding.
    const messageCell = cell<LogRecord>({
      ...message,
      firstCreated: message.created,
      firstRelativeCreated: message.relativeCreated,
    });

    // Handle reference-linked updates.
    if (message.reference) {
      const first = firstByRefRef.current.get(message.reference);
      if (first) {
        // Update the first's contents
        first.set((prev) => ({
          ...message,
          firstCreated: prev.firstCreated ?? prev.created,
          firstRelativeCreated: prev.firstRelativeCreated ?? prev.relativeCreated,
        }));

        return; // Do not duplicate reference group entries.
      } else {
        firstByRefRef.current.set(message.reference, messageCell);
      }
    }

    // Only append if message passes filters.
    if (applyFilters(message)) {
      setFiltered((curr) => [...curr, messageCell]);
    }
  }, [applyFilters, setFiltered]);

  /**
   * Initializes the SSE (Server-Sent Events) stream for real-time logs.
   *
   * Subscribes to messages from the backend logging endpoint and
   * dispatches each message to `handleNewMessage`.
   *
   * Cleans up the EventSource connection when the component unmounts.
   */
  useEffect(() => {
    // Only create one SSE connection for the lifetime of the hook.
    if (sseRef.current) return;

    const es = new EventSource(`${API_BASE_URL}/logs/stream`);
    sseRef.current = es;

    es.onmessage = (event) => {
      const data: LogRecord = JSON.parse(event.data);
      handleNewMessage(data);
    };

    return () => {
      es.close();
      sseRef.current = null;
    };
  }, [handleNewMessage]);

  return {filteredLogs: filtered, distinctNames};
}
