// 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 {type ComponentType, useEffect, useRef, useState} from "react";
import formatDuration from "../../utils/formatDuration.ts";
import {type LogFilterPredicate, type LogRecord, useLogs} from "./useLogs.ts";
import Filters from "./Filters.tsx";
import {type Cell, useCell} from "../../utils/cellStore.ts";
import styles from "./Logging.module.css";
import {
  EXPERIMENT_FILTER_KEY,
  EXPERIMENT_LOGGER_NAME,
  type LoggingSettings,
  type MessageComponentProps
} from "./Definitions.ts";
import {create} from "zustand";

/**
 * Local Zustand store for logging UI preferences.
 */
const useLoggingSettings = create<LoggingSettings>((set) => ({
  showRelativeTime: false,
  setShowRelativeTime: (showRelativeTime: boolean) => set({ showRelativeTime }),
}));

/**
 * Renders a single log message entry with colored level indicators and timestamp formatting.
 *
 * This component automatically re-renders when the underlying log record (`recordCell`)
 * changes. It also triggers the `onUpdate` callback whenever the record updates (e.g., for auto-scrolling).
 *
 * @param recordCell - A reactive `Cell` containing a single `LogRecord`.
 * @param onUpdate - Optional callback triggered when the log entry updates.
 * @returns A JSX element displaying a formatted log message.
 */
function LogMessage({ recordCell, onUpdate }: MessageComponentProps) {
  const { showRelativeTime, setShowRelativeTime } = useLoggingSettings();
  const record = useCell(recordCell);

  /**
   * Normalizes the log level number to a multiple of 10, 
   * for which there are CSS styles. (e.g., INFO = 20, ERROR = 40).
   */
  const normalizedLevelNo = (() => {
    // By default, the highest level is 50 (CRITICAL). Custom levels can be higher, but we don't have more critical color.
    if (record.levelno >= 50) return 50;

    return Math.round(record.levelno / 10) * 10;
  })();

  /** Simplifies the logger name by showing only the last path segment. */
  const normalizedName = record.name.split(".").pop() || record.name;

  // Notify the parent component (e.g., for scroll updates) when this record changes.
  useEffect(() => {
    if (onUpdate) onUpdate();
  }, [record, onUpdate]);

  return <div className={`${styles.logContainer} round-md border-lg flex-row gap-md`}>
    <div className={`${styles[`accented${normalizedLevelNo}`]} flex-col padding-sm justify-between`}>
      <span className={"mono bold"}>{record.levelname}</span>
      <span className={"mono clickable font-small"} onClick={() => setShowRelativeTime(!showRelativeTime)}>{
        showRelativeTime
          ? formatDuration(record.relativeCreated)
          : new Date(record.created * 1000).toLocaleTimeString()
      }</span>
    </div>
    <div className={"flex-col flex-1 padding-sm"}>
      <span className={"mono"}>{normalizedName}</span>
      <span>{record.message}</span>
    </div>
  </div>;
}

/**
 * Displays a scrollable list of log messages.
 *
 * Handles:
 * - Auto-scrolling when new messages arrive.
 * - Allowing users to scroll manually and disable auto-scroll.
 * - A floating "Scroll to bottom" button when not at the bottom.
 *
 * @param recordCells - Array of reactive log records to display.
 * @param MessageComponent - A component to use to render each log message entry.
 * @returns A scrollable log list component.
 */
export function LogMessages({
  recordCells,
  MessageComponent,
}: {
  recordCells: Cell<LogRecord>[],
  MessageComponent: ComponentType<MessageComponentProps>,
}) {
  const scrollableRef = useRef<HTMLDivElement>(null);
  const [scrollToBottom, setScrollToBottom] = useState(true);

  // Disable auto-scroll if the user manually scrolls.
  useEffect(() => {
    if (!scrollableRef.current) return;
    const currentScrollableRef = scrollableRef.current;

    const handleScroll = () => setScrollToBottom(false);

    currentScrollableRef.addEventListener("wheel", handleScroll);
    currentScrollableRef.addEventListener("touchmove", handleScroll);

    return () => {
      currentScrollableRef.removeEventListener("wheel", handleScroll);
      currentScrollableRef.removeEventListener("touchmove", handleScroll);
    }
  }, [scrollableRef, setScrollToBottom]);

  /**
   * Scrolls the log messages to the bottom, making the latest messages visible.
   *
   * @param force - If true, forces scrolling even if `scrollToBottom` is false.
   */
  function showBottom(force = false) {
    if ((!scrollToBottom && !force) || !scrollableRef.current) return;
    scrollableRef.current.scrollTo({top: scrollableRef.current.scrollHeight, left: 0, behavior: "smooth"});
  }

  return <div ref={scrollableRef} className={"min-height-0 scroll-y padding-h-lg padding-b-lg flex-1"}>
    <ol className={`${styles.noNumbers} margin-0 flex-col gap-md`}>
      {recordCells.map((recordCell, i) => (
        <li key={`${i}_${recordCell.get().firstRelativeCreated}`}>
          <MessageComponent recordCell={recordCell} onUpdate={showBottom} />
        </li>
      ))}
    </ol>
    {!scrollToBottom && <button
      className={styles.floatingButton}
      onClick={() => {
        setScrollToBottom(true);
        showBottom(true);
      }}
    >
      Scroll to bottom
    </button>}
  </div>;
}

/**
 * Top-level logging panel component.
 *
 * Combines:
 * - The `Filters` component for adjusting log visibility.
 * - The `LogMessages` component for displaying filtered logs.
 * - Zustand-managed UI settings (auto-scroll, timestamp display).
 *
 * This component uses the `useLogs` hook to fetch and filter logs based on
 * active predicates and re-renders automatically as new logs arrive.
 *
 * @returns The complete logging UI as a React element.
 */
export default function Logging() {
  // By default, filter experiment logs from this debug logger
  const [filterPredicates, setFilterPredicates] = useState(new Map<string, LogFilterPredicate>([
    [
      EXPERIMENT_FILTER_KEY,
      {
        predicate: (r) => r.name == EXPERIMENT_LOGGER_NAME ? false : null,
        priority: 999,
        value: null,
      } as LogFilterPredicate,
    ],
  ]));
  const { filteredLogs, distinctNames } = useLogs(filterPredicates)
  distinctNames.delete(EXPERIMENT_LOGGER_NAME);

  return <div className={`flex-col min-height-0 relative ${styles.loggingContainer}`}>
    <div className={"flex-row gap-lg justify-between align-center padding-lg"}>
      <h2 className={"margin-0"}>Logs</h2>
      <Filters
        filterPredicates={filterPredicates}
        setFilterPredicates={setFilterPredicates}
        agentNames={distinctNames}
      />
    </div>
    <LogMessages recordCells={filteredLogs} MessageComponent={LogMessage} />
  </div>;
}
