import {
  Fragment,
  memo,
  useCallback,
  useDeferredValue,
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { api } from '../api/client';
import { num } from '../lib/format';
import { Banner, Button, Card, Field, Note, PageHead } from '../components/ui';
import type { LogEvent, LogResponse } from '../api/types';

/* The live server log.
 *
 * Network delivery is already cursor based: each server record crosses the wire once.
 * Rendering is virtualised as well, so retaining and filtering thousands of records does
 * not mean mounting thousands of details trees. Only the rows around the viewport exist
 * in the DOM; selecting one opens its complete structured data below the window. */

const RANKS: Record<string, number> = { TRACE: 5, DEBUG: 10, INFO: 20, WARN: 30, ERROR: 40 };
const RETAIN = 20_000;
const POLL_MS = 5_000;
const ROW_HEIGHT = 48;
const HEADER_HEIGHT = 31;
const OVERSCAN = 8;

const FIELD_ORDER = [
  'component',
  'user',
  'device',
  'client',
  'protocol',
  'method',
  'path',
  'status',
  'duration',
  'version',
  'gateway_version',
];
const FIELD_RANK = new Map(FIELD_ORDER.map((key, index) => [key, index]));
const dateTime = new Intl.DateTimeFormat(undefined, { dateStyle: 'short', timeStyle: 'medium' });

interface CachedEvent {
  fields: [string, unknown][];
  summary: string;
  haystack: string;
  occurred: string;
}

// API event objects remain stable for their retained lifetime. A WeakMap gives formatting
// and search indexing the same lifetime without adding private fields to JSON exports.
const eventCache = new WeakMap<LogEvent, CachedEvent>();

function cached(event: LogEvent): CachedEvent {
  const existing = eventCache.get(event);
  if (existing) return existing;
  const fields = Object.entries(event.attributes ?? {}).sort((left, right) => {
    const leftRank = FIELD_RANK.get(left[0]) ?? (left[0] === 'error' ? 1000 : 100);
    const rightRank = FIELD_RANK.get(right[0]) ?? (right[0] === 'error' ? 1000 : 100);
    return leftRank - rightRank || left[0].localeCompare(right[0]);
  });
  const value = {
    fields,
    summary: fields.map(([key, fieldValue]) => `${key}=${String(fieldValue)}`).join(' '),
    haystack: [event.message, ...fields.flat()].join(' ').toLowerCase(),
    occurred: dateTime.format(new Date(event.occurredAt)),
  };
  eventCache.set(event, value);
  return value;
}

const readableKey = (key: string) => key.replace(/_/g, ' ');

const LogLine = memo(function LogLine({
  event,
  index,
  onInspect,
}: {
  event: LogEvent;
  index: number;
  onInspect: (sequence: number) => void;
}) {
  const display = cached(event);
  return (
    <div
      className="logline"
      data-level={event.level}
      data-virtual="true"
      style={{ transform: `translateY(${index * ROW_HEIGHT}px)` }}
    >
      <time title={event.occurredAt}>
        {display.occurred}
        <small>#{event.sequence}</small>
      </time>
      <span className="lvl">{event.level}</span>
      <span className="msg" title={event.message}>{event.message}</span>
      <button
        type="button"
        className="logattrs-button"
        title={display.summary || 'No structured details'}
        onClick={() => onInspect(event.sequence)}
      >
        {display.summary || 'View record'}
      </button>
    </div>
  );
});

export function LogsPage() {
  const [records, setRecords] = useState<LogEvent[]>([]);
  const [dropped, setDropped] = useState(0);
  const [paused, setPaused] = useState(false);
  const [level, setLevel] = useState('INFO');
  const [search, setSearch] = useState('');
  const [error, setError] = useState('');
  const [viewport, setViewport] = useState({ top: 0, height: 600 });
  const [selectedSequence, setSelectedSequence] = useState<number | null>(null);

  const deferredSearch = useDeferredValue(search.trim().toLowerCase());
  const cursor = useRef(0);
  const fetching = useRef(false);
  const viewGeneration = useRef(0);
  const view = useRef<HTMLDivElement>(null);
  const pinned = useRef(true);
  const scrollFrame = useRef<number | undefined>(undefined);

  const drain = useCallback(async () => {
    if (paused || fetching.current || document.hidden) return;
    fetching.current = true;
    const generation = viewGeneration.current;
    const batch: LogEvent[] = [];
    let droppedInDrain = 0;
    try {
      let pages = 0;
      let page: LogResponse;
      do {
        page = await api.get<LogResponse>(`/admin/api/events?after=${cursor.current}&limit=1000`);
        cursor.current = page.next || cursor.current;
        droppedInDrain += page.dropped || 0;
        batch.push(...(page.events ?? []));
        pages += 1;
      } while (page.hasMore && pages < 20);
      setError('');
    } catch (cause) {
      setError(cause instanceof Error ? cause.message : String(cause));
    } finally {
      // One React update for a complete catch-up prevents the initial 5,000-record drain
      // from redrawing the page once per network page.
      if (batch.length > 0 && generation === viewGeneration.current) {
        setRecords((current) => {
          const next = current.concat(batch);
          return next.length > RETAIN ? next.slice(next.length - RETAIN) : next;
        });
      }
      if (droppedInDrain > 0 && generation === viewGeneration.current) {
        setDropped((current) => current + droppedInDrain);
      }
      fetching.current = false;
    }
  }, [paused]);

  useEffect(() => {
    if (paused) return;
    let timer: number | undefined;
    const schedule = () => {
      window.clearInterval(timer);
      timer = document.hidden ? undefined : window.setInterval(() => void drain(), POLL_MS);
    };
    const visibilityChanged = () => {
      schedule();
      if (!document.hidden) void drain();
    };
    void drain();
    schedule();
    document.addEventListener('visibilitychange', visibilityChanged);
    return () => {
      window.clearInterval(timer);
      document.removeEventListener('visibilitychange', visibilityChanged);
    };
  }, [drain, paused]);

  const filtered = useMemo(() => {
    const minimum = RANKS[level] ?? 20;
    return records.filter(
      (event) =>
        (RANKS[event.level] ?? 0) >= minimum &&
        (!deferredSearch || cached(event).haystack.includes(deferredSearch)),
    );
  }, [records, level, deferredSearch]);

  const lastSequence = filtered.at(-1)?.sequence ?? 0;
  const bodyTop = Math.max(0, viewport.top - HEADER_HEIGHT);
  const count = Math.ceil(viewport.height / ROW_HEIGHT) + OVERSCAN * 2;
  // A restrictive filter can make the old scroll offset larger than the new body before
  // the browser dispatches its compensating scroll event. Clamp immediately so that
  // transition never paints an apparently empty log.
  const first = Math.min(
    Math.max(0, Math.floor(bodyTop / ROW_HEIGHT) - OVERSCAN),
    Math.max(0, filtered.length - count),
  );
  const windowed = filtered.slice(first, first + count);
  const selected = useMemo(
    () => records.find((event) => event.sequence === selectedSequence),
    [records, selectedSequence],
  );

  useLayoutEffect(() => {
    const node = view.current;
    if (!node || !pinned.current) return;
    node.scrollTop = node.scrollHeight;
    setViewport({ top: node.scrollTop, height: node.clientHeight });
  }, [lastSequence, deferredSearch, level]);

  useEffect(() => () => window.cancelAnimationFrame(scrollFrame.current ?? 0), []);

  const onScroll = () => {
    const node = view.current;
    if (!node) return;
    pinned.current = node.scrollHeight - node.scrollTop - node.clientHeight < ROW_HEIGHT;
    window.cancelAnimationFrame(scrollFrame.current ?? 0);
    scrollFrame.current = window.requestAnimationFrame(() => {
      setViewport({ top: node.scrollTop, height: node.clientHeight });
    });
  };

  const exportJson = () => {
    const blob = new Blob([JSON.stringify(records, null, 2)], { type: 'application/json' });
    const link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = `memby-events-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
    link.click();
    window.setTimeout(() => URL.revokeObjectURL(link.href), 1000);
  };

  return (
    <>
      <PageHead title="Server logs" intro="Structured gateway events as they happen." />
      <Banner message={error} />

      <Card>
        <div className="filters">
          <Field label="Level">
            <select value={level} onChange={(event) => setLevel(event.target.value)}>
              <option value="DEBUG">Debug and above</option>
              <option value="INFO">Info and above</option>
              <option value="WARN">Warnings and errors</option>
              <option value="ERROR">Errors only</option>
            </select>
          </Field>
          <Field label="Filter" grow>
            <input
              type="search"
              value={search}
              placeholder="Person, television, title, component, path…"
              onChange={(event) => setSearch(event.target.value)}
            />
          </Field>
          <div className="filter-actions">
            <Button onClick={() => setPaused((current) => !current)} icon={paused ? 'play' : 'clock'}>
              {paused ? 'Resume' : 'Pause'}
            </Button>
            <Button
              onClick={() => {
                // A page already in flight may finish after this click. Advancing the
                // generation makes that batch part of the cleared past, not a flash of
                // old lines reappearing after the view was emptied.
                viewGeneration.current += 1;
                setRecords([]);
                setDropped(0);
                setSelectedSequence(null);
              }}
            >
              Clear view
            </Button>
            <Button onClick={exportJson} icon="download">Export JSON</Button>
          </div>
        </div>

        <div className="logview" ref={view} onScroll={onScroll} role="log" aria-label="Server events">
          <div className="loghead" aria-hidden="true">
            <span>Time</span>
            <span>Level</span>
            <span>Event</span>
            <span>Details</span>
          </div>
          {filtered.length === 0 ? (
            <p className="empty">{records.length === 0 ? 'Waiting for server events…' : 'No events match this filter.'}</p>
          ) : (
            <div className="logbody" style={{ height: `${filtered.length * ROW_HEIGHT}px` }}>
              {windowed.map((event, offset) => (
                <LogLine key={event.sequence} event={event} index={first + offset} onInspect={setSelectedSequence} />
              ))}
            </div>
          )}
        </div>

        <p className="hint">
          {num(records.length)} retained · {num(filtered.length)} matching
          {filtered.length ? ` · ${num(windowed.length)} rows mounted` : ''}
          {dropped ? ` · ${num(dropped)} overwritten before delivery` : ''}
          {paused ? ' · paused' : ''}
        </p>

        {selected ? (
          <section className="log-inspector" aria-label={`Log record ${selected.sequence}`}>
            <div className="log-inspector-head">
              <div>
                <b>{selected.message}</b>
                <span>{cached(selected).occurred} · {selected.level} · record #{selected.sequence}</span>
              </div>
              <Button size="sm" variant="quiet" onClick={() => setSelectedSequence(null)}>Close</Button>
            </div>
            {cached(selected).fields.length ? (
              <dl>
                {cached(selected).fields.map(([key, fieldValue]) => (
                  <Fragment key={key}>
                    <dt>{readableKey(key)}</dt>
                    <dd>{String(fieldValue)}</dd>
                  </Fragment>
                ))}
              </dl>
            ) : (
              <Note>No structured details were attached to this record.</Note>
            )}
          </section>
        ) : null}
      </Card>
    </>
  );
}
