/**
 * AuditConsoleApp — TUI for browsing audit events from mcpd.
 *
 * Navigation follows the same patterns as the main unified console:
 * - Sidebar open: arrows navigate sessions, Enter selects, Escape closes
 * - Sidebar closed: arrows navigate timeline, Escape reopens sidebar
 *
 * Sidebar groups sessions by project → user.
 * `d` key cycles through date filter presets.
 */

import { useState, useEffect, useCallback, useRef } from 'react';
import { render, Box, Text, useInput, useApp, useStdout } from 'ink';
import type { AuditSession, AuditEvent, AuditConsoleState, DateFilterPreset } from './audit-types.js';
import { EVENT_KIND_COLORS, EVENT_KIND_LABELS, DATE_FILTER_CYCLE, DATE_FILTER_LABELS, dateFilterToFrom } from './audit-types.js';
import http from 'node:http';

const POLL_INTERVAL_MS = 3_000;
const MAX_EVENTS = 500;

// ── HTTP helpers ──

function fetchJson<T>(url: string, token?: string): Promise<T> {
  return new Promise((resolve, reject) => {
    const parsed = new URL(url);
    const headers: Record<string, string> = { 'Accept': 'application/json' };
    if (token) headers['Authorization'] = `Bearer ${token}`;

    const req = http.get({ hostname: parsed.hostname, port: parsed.port, path: parsed.pathname + parsed.search, headers, timeout: 5000 }, (res) => {
      let data = '';
      res.on('data', (chunk: Buffer) => { data += chunk.toString(); });
      res.on('end', () => {
        try {
          resolve(JSON.parse(data) as T);
        } catch {
          reject(new Error(`Invalid JSON from ${url}`));
        }
      });
    });
    req.on('error', (err) => reject(err));
    req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
  });
}

// ── Format helpers ──

function formatTime(ts: string): string {
  const d = new Date(ts);
  return d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}

function trunc(s: string, max: number): string {
  return s.length > max ? s.slice(0, max - 1) + '\u2026' : s;
}

function formatPayload(payload: Record<string, unknown>): string {
  const parts: string[] = [];
  for (const [k, v] of Object.entries(payload)) {
    if (v === null || v === undefined) continue;
    if (typeof v === 'string') {
      parts.push(`${k}=${trunc(v, 30)}`);
    } else if (typeof v === 'number' || typeof v === 'boolean') {
      parts.push(`${k}=${String(v)}`);
    }
  }
  return parts.join('  ');
}

function formatDetailPayload(payload: Record<string, unknown>): string[] {
  const lines: string[] = [];
  for (const [k, v] of Object.entries(payload)) {
    if (v === null || v === undefined) {
      lines.push(`  ${k}: null`);
    } else if (typeof v === 'object') {
      lines.push(`  ${k}: ${JSON.stringify(v, null, 2).split('\n').join('\n    ')}`);
    } else {
      lines.push(`  ${k}: ${String(v)}`);
    }
  }
  return lines;
}

// ── Sidebar grouping ──

interface SidebarLine {
  type: 'project-header' | 'user-header' | 'session';
  label: string;
  sessionIdx?: number; // flat index into sessions array (only for type=session)
}

function buildGroupedLines(sessions: AuditSession[]): SidebarLine[] {
  // Group by project → user
  const projectMap = new Map<string, Map<string, number[]>>();
  const projectOrder: string[] = [];

  for (let i = 0; i < sessions.length; i++) {
    const s = sessions[i]!;
    let userMap = projectMap.get(s.projectName);
    if (!userMap) {
      userMap = new Map();
      projectMap.set(s.projectName, userMap);
      projectOrder.push(s.projectName);
    }
    const userName = s.userName ?? '(unknown)';
    let indices = userMap.get(userName);
    if (!indices) {
      indices = [];
      userMap.set(userName, indices);
    }
    indices.push(i);
  }

  const lines: SidebarLine[] = [];
  for (const proj of projectOrder) {
    lines.push({ type: 'project-header', label: proj });
    const userMap = projectMap.get(proj)!;
    for (const [user, indices] of userMap) {
      lines.push({ type: 'user-header', label: user });
      for (const idx of indices) {
        const s = sessions[idx]!;
        const time = formatTime(s.lastSeen);
        lines.push({
          type: 'session',
          label: `${s.sessionId.slice(0, 8)} \u00B7 ${s.eventCount} ev \u00B7 ${time}`,
          sessionIdx: idx,
        });
      }
    }
  }
  return lines;
}

/** Extract session indices in visual (grouped) order. */
function visualSessionOrder(sessions: AuditSession[]): number[] {
  return buildGroupedLines(sessions)
    .filter((l) => l.type === 'session')
    .map((l) => l.sessionIdx!);
}

// ── Session Sidebar ──

function AuditSidebar({ sessions, selectedIdx, projectFilter, dateFilter, height }: {
  sessions: AuditSession[];
  selectedIdx: number;
  projectFilter: string | null;
  dateFilter: DateFilterPreset;
  height: number;
}) {
  const grouped = buildGroupedLines(sessions);

  const headerLines = 4; // title + filter info + blank + "All" row
  const footerLines = 0;
  const bodyHeight = Math.max(1, height - headerLines - footerLines);

  // Find which render line corresponds to the selected session
  let selectedLineIdx = -1;
  if (selectedIdx >= 0) {
    selectedLineIdx = grouped.findIndex((l) => l.sessionIdx === selectedIdx);
  }

  // Scroll to keep selected visible
  let scrollStart = 0;
  if (selectedLineIdx >= 0) {
    if (selectedLineIdx >= scrollStart + bodyHeight) {
      scrollStart = selectedLineIdx - bodyHeight + 1;
    }
    if (selectedLineIdx < scrollStart) {
      scrollStart = selectedLineIdx;
    }
  }
  scrollStart = Math.max(0, scrollStart);

  const visibleLines = grouped.slice(scrollStart, scrollStart + bodyHeight);

  return (
    <Box flexDirection="column" width={34} height={height} borderStyle="single" borderColor="gray" paddingX={1}>
      <Text bold>Sessions ({sessions.length})</Text>
      <Text dimColor>
        {projectFilter ? `project: ${projectFilter}` : 'all projects'}
        {dateFilter !== 'all' ? ` \u00B7 ${DATE_FILTER_LABELS[dateFilter]}` : ''}
      </Text>
      <Text> </Text>
      <Text color={selectedIdx === -1 ? 'cyan' : undefined} bold={selectedIdx === -1}>
        {selectedIdx === -1 ? '\u25B8 ' : '  '}All ({sessions.reduce((s, x) => s + x.eventCount, 0)} events)
      </Text>

      {visibleLines.map((line, vi) => {
        if (line.type === 'project-header') {
          return (
            <Text key={`p-${line.label}-${vi}`} bold wrap="truncate">
              {'  '}{trunc(line.label, 28)}
            </Text>
          );
        }
        if (line.type === 'user-header') {
          return (
            <Text key={`u-${line.label}-${vi}`} dimColor wrap="truncate">
              {'    '}{trunc(line.label, 26)}
            </Text>
          );
        }
        // session
        const isSel = line.sessionIdx === selectedIdx;
        return (
          <Text key={`s-${line.sessionIdx}-${vi}`} color={isSel ? 'cyan' : undefined} bold={isSel} wrap="truncate">
            {isSel ? '    \u25B8 ' : '      '}{trunc(line.label, 24)}
          </Text>
        );
      })}
      {sessions.length === 0 && <Text dimColor>  No sessions</Text>}
    </Box>
  );
}

// ── Event Timeline ──

function AuditTimeline({ events, height, focusedIdx }: { events: AuditEvent[]; height: number; focusedIdx: number }) {
  const maxVisible = Math.max(1, height - 2);
  let startIdx: number;
  if (focusedIdx >= 0) {
    startIdx = Math.max(0, Math.min(focusedIdx - Math.floor(maxVisible / 2), events.length - maxVisible));
  } else {
    startIdx = Math.max(0, events.length - maxVisible);
  }
  const visible = events.slice(startIdx, startIdx + maxVisible);

  return (
    <Box flexDirection="column" flexGrow={1} paddingLeft={1}>
      <Text bold>
        Events <Text dimColor>({events.length}{focusedIdx >= 0 ? ` \u00B7 #${focusedIdx + 1}` : ' \u00B7 following'})</Text>
      </Text>
      {visible.length === 0 && (
        <Box marginTop={1}>
          <Text dimColor>{'  No audit events yet\u2026'}</Text>
        </Box>
      )}
      {visible.map((event, vi) => {
        const absIdx = startIdx + vi;
        const isFocused = absIdx === focusedIdx;
        const kindColor = EVENT_KIND_COLORS[event.eventKind] ?? 'white';
        const kindLabel = EVENT_KIND_LABELS[event.eventKind] ?? event.eventKind.toUpperCase();
        const verified = event.verified ? '\u2713' : '\u2717';
        const verifiedColor = event.verified ? 'green' : 'red';
        const summary = formatPayload(event.payload);

        return (
          <Text key={event.id} wrap="truncate">
            <Text color={isFocused ? 'cyan' : undefined}>{isFocused ? '\u25B8' : ' '}</Text>
            <Text dimColor>{formatTime(event.timestamp)}  </Text>
            <Text color={verifiedColor}>{verified}</Text>
            <Text> </Text>
            <Text color={kindColor} bold>{trunc(kindLabel, 9).padEnd(9)}</Text>
            {event.serverName && <Text color="gray"> [{trunc(event.serverName, 14)}]</Text>}
            <Text dimColor> {trunc(summary, 60)}</Text>
          </Text>
        );
      })}
    </Box>
  );
}

// ── Detail View ──

function AuditDetail({ event, scrollOffset, height }: { event: AuditEvent; scrollOffset: number; height: number }) {
  const kindColor = EVENT_KIND_COLORS[event.eventKind] ?? 'white';
  const kindLabel = EVENT_KIND_LABELS[event.eventKind] ?? event.eventKind;
  const lines = [
    `Kind:      ${kindLabel}`,
    `Session:   ${event.sessionId}`,
    `Project:   ${event.projectName}`,
    `Source:    ${event.source}`,
    `Verified:  ${event.verified ? 'yes' : 'no'}`,
    `Server:    ${event.serverName ?? '-'}`,
    `Time:      ${new Date(event.timestamp).toLocaleString()}`,
    `ID:        ${event.id}`,
    '',
    'Payload:',
    ...formatDetailPayload(event.payload),
  ];

  const maxVisible = Math.max(1, height - 2);
  const visible = lines.slice(scrollOffset, scrollOffset + maxVisible);

  return (
    <Box flexDirection="column" flexGrow={1} paddingLeft={1}>
      <Text bold color={kindColor}>
        {kindLabel} Detail <Text dimColor>(line {scrollOffset + 1}/{lines.length})</Text>
      </Text>
      {visible.map((line, i) => (
        <Text key={i} wrap="truncate">{line}</Text>
      ))}
    </Box>
  );
}

// ── Main App ──

interface AuditAppProps {
  mcpdUrl: string;
  token?: string;
  projectFilter?: string;
}

function AuditApp({ mcpdUrl, token, projectFilter }: AuditAppProps) {
  const { exit } = useApp();
  const { stdout } = useStdout();

  const [state, setState] = useState<AuditConsoleState>({
    phase: 'loading',
    error: null,
    sessions: [],
    selectedSessionIdx: -1,
    showSidebar: true,
    events: [],
    focusedEventIdx: -1,
    totalEvents: 0,
    detailEvent: null,
    detailScrollOffset: 0,
    projectFilter: projectFilter ?? null,
    kindFilter: null,
    dateFilter: 'all',
  });

  // Use refs for polling to avoid re-creating intervals on every state change
  const stateRef = useRef(state);
  stateRef.current = state;

  // Fetch sessions (stable — no state deps)
  const fetchSessions = useCallback(async () => {
    try {
      const params = new URLSearchParams();
      const s = stateRef.current;
      if (s.projectFilter) params.set('projectName', s.projectFilter);
      const from = dateFilterToFrom(s.dateFilter);
      if (from) params.set('from', from);
      params.set('limit', '50');
      const url = `${mcpdUrl}/api/v1/audit/sessions?${params.toString()}`;
      const data = await fetchJson<{ sessions?: AuditSession[]; total?: number }>(url, token);
      if (data.sessions && Array.isArray(data.sessions)) {
        setState((prev) => ({ ...prev, sessions: data.sessions!, phase: 'ready' }));
      }
    } catch (err) {
      setState((prev) => {
        // Only show error if we haven't loaded anything yet
        if (prev.phase === 'loading') {
          return { ...prev, phase: 'error', error: err instanceof Error ? err.message : String(err) };
        }
        return prev; // Keep existing data on transient errors
      });
    }
  }, [mcpdUrl, token]);

  // Fetch events (stable — no state deps)
  const fetchEvents = useCallback(async () => {
    try {
      const s = stateRef.current;
      const params = new URLSearchParams();
      const selectedSession = s.selectedSessionIdx >= 0 ? s.sessions[s.selectedSessionIdx] : undefined;
      if (selectedSession) {
        params.set('sessionId', selectedSession.sessionId);
      } else if (s.projectFilter) {
        params.set('projectName', s.projectFilter);
      }
      if (s.kindFilter) params.set('eventKind', s.kindFilter);
      const from = dateFilterToFrom(s.dateFilter);
      if (from) params.set('from', from);
      params.set('limit', String(MAX_EVENTS));
      const url = `${mcpdUrl}/api/v1/audit/events?${params.toString()}`;
      const data = await fetchJson<{ events?: AuditEvent[]; total?: number }>(url, token);
      if (data.events && Array.isArray(data.events)) {
        // API returns newest first — reverse for timeline display
        setState((prev) => ({ ...prev, events: data.events!.reverse(), totalEvents: data.total ?? data.events!.length }));
      }
    } catch {
      // Non-fatal — keep existing events
    }
  }, [mcpdUrl, token]);

  // Initial load + polling (single stable interval)
  useEffect(() => {
    void fetchSessions();
    void fetchEvents();
    const timer = setInterval(() => {
      void fetchSessions();
      void fetchEvents();
    }, POLL_INTERVAL_MS);
    return () => clearInterval(timer);
  }, [fetchSessions, fetchEvents]);

  // Date filter handler — shared between sidebar and timeline
  const handleDateFilter = useCallback(() => {
    setState((prev) => {
      const currentIdx = DATE_FILTER_CYCLE.indexOf(prev.dateFilter);
      const nextIdx = (currentIdx + 1) % DATE_FILTER_CYCLE.length;
      const next = { ...prev, dateFilter: DATE_FILTER_CYCLE[nextIdx]!, focusedEventIdx: -1, selectedSessionIdx: -1 };
      stateRef.current = next;
      return next;
    });
    void fetchSessions();
    void fetchEvents();
  }, [fetchSessions, fetchEvents]);

  // Kind filter handler — shared between sidebar and timeline
  const handleKindFilter = useCallback(() => {
    const kinds = [null, 'tool_call_trace', 'gate_decision', 'pipeline_execution', 'stage_execution', 'prompt_delivery', 'session_bind'];
    setState((prev) => {
      const currentIdx = kinds.indexOf(prev.kindFilter);
      const nextIdx = (currentIdx + 1) % kinds.length;
      const next = { ...prev, kindFilter: kinds[nextIdx] ?? null, focusedEventIdx: -1 };
      stateRef.current = next;
      return next;
    });
    void fetchEvents();
  }, [fetchEvents]);

  // Keyboard input
  useInput((input, key) => {
    const s = stateRef.current;

    // Quit
    if (input === 'q') {
      exit();
      return;
    }

    // ── Detail view navigation ──
    if (s.detailEvent) {
      if (key.escape) {
        setState((prev) => ({ ...prev, detailEvent: null, detailScrollOffset: 0 }));
        return;
      }
      if (key.downArrow) {
        setState((prev) => ({ ...prev, detailScrollOffset: prev.detailScrollOffset + 1 }));
        return;
      }
      if (key.upArrow) {
        setState((prev) => ({ ...prev, detailScrollOffset: Math.max(0, prev.detailScrollOffset - 1) }));
        return;
      }
      if (key.pageDown) {
        const pageSize = Math.max(1, Math.floor(stdout.rows * 0.5));
        setState((prev) => ({ ...prev, detailScrollOffset: prev.detailScrollOffset + pageSize }));
        return;
      }
      if (key.pageUp) {
        const pageSize = Math.max(1, Math.floor(stdout.rows * 0.5));
        setState((prev) => ({ ...prev, detailScrollOffset: Math.max(0, prev.detailScrollOffset - pageSize) }));
        return;
      }
      return;
    }

    // ── Sidebar navigation (arrows = sessions, Enter = select, Escape = close) ──
    if (s.showSidebar) {
      const navigateSidebar = (direction: number, step: number = 1) => {
        setState((prev) => {
          const order = visualSessionOrder(prev.sessions);
          if (order.length === 0) return prev;
          const curPos = prev.selectedSessionIdx === -1 ? -1 : order.indexOf(prev.selectedSessionIdx);
          let newPos = curPos + direction * step;
          let newIdx: number;
          if (newPos < 0) {
            newIdx = -1; // "All" selection
          } else {
            newPos = Math.min(order.length - 1, Math.max(0, newPos));
            newIdx = order[newPos]!;
          }
          if (newIdx === prev.selectedSessionIdx) return prev;
          const next = { ...prev, selectedSessionIdx: newIdx, focusedEventIdx: -1 };
          stateRef.current = next;
          return next;
        });
        void fetchEvents();
      };

      if (key.downArrow) { navigateSidebar(1); return; }
      if (key.upArrow) { navigateSidebar(-1); return; }
      if (key.pageDown) { navigateSidebar(1, Math.max(1, Math.floor(stdout.rows * 0.5))); return; }
      if (key.pageUp) { navigateSidebar(-1, Math.max(1, Math.floor(stdout.rows * 0.5))); return; }

      if (key.return) {
        // Enter closes sidebar, keeping the selected session
        setState((prev) => ({ ...prev, showSidebar: false, focusedEventIdx: -1 }));
        return;
      }
      if (key.escape) {
        setState((prev) => ({ ...prev, showSidebar: false }));
        return;
      }

      if (input === 'k') { handleKindFilter(); return; }
      if (input === 'd') { handleDateFilter(); return; }

      return; // Absorb all other input when sidebar is open
    }

    // ── Timeline navigation (sidebar closed) ──

    // Escape reopens sidebar
    if (key.escape) {
      setState((prev) => ({ ...prev, showSidebar: true, focusedEventIdx: -1 }));
      return;
    }

    // Auto-scroll resume
    if (input === 'a') {
      setState((prev) => ({ ...prev, focusedEventIdx: -1 }));
      return;
    }

    if (input === 'k') { handleKindFilter(); return; }
    if (input === 'd') { handleDateFilter(); return; }

    // Enter: detail view
    if (key.return) {
      setState((prev) => {
        const idx = prev.focusedEventIdx === -1 ? prev.events.length - 1 : prev.focusedEventIdx;
        const event = prev.events[idx];
        if (!event) return prev;
        return { ...prev, detailEvent: event, detailScrollOffset: 0 };
      });
      return;
    }

    // Arrow navigation
    if (key.downArrow) {
      setState((prev) => {
        if (prev.focusedEventIdx === -1) return prev;
        return { ...prev, focusedEventIdx: Math.min(prev.events.length - 1, prev.focusedEventIdx + 1) };
      });
      return;
    }
    if (key.upArrow) {
      setState((prev) => {
        if (prev.focusedEventIdx === -1) {
          return prev.events.length > 0 ? { ...prev, focusedEventIdx: prev.events.length - 1 } : prev;
        }
        return { ...prev, focusedEventIdx: prev.focusedEventIdx <= 0 ? -1 : prev.focusedEventIdx - 1 };
      });
      return;
    }
    if (key.pageDown) {
      const pageSize = Math.max(1, stdout.rows - 8);
      setState((prev) => {
        if (prev.focusedEventIdx === -1) return prev;
        return { ...prev, focusedEventIdx: Math.min(prev.events.length - 1, prev.focusedEventIdx + pageSize) };
      });
      return;
    }
    if (key.pageUp) {
      const pageSize = Math.max(1, stdout.rows - 8);
      setState((prev) => {
        const current = prev.focusedEventIdx === -1 ? prev.events.length - 1 : prev.focusedEventIdx;
        return { ...prev, focusedEventIdx: Math.max(0, current - pageSize) };
      });
      return;
    }
  });

  const height = stdout.rows - 3; // header + footer

  if (state.phase === 'loading') {
    return (
      <Box flexDirection="column">
        <Text bold color="cyan">Audit Console</Text>
        <Text dimColor>Connecting to mcpd{'\u2026'}</Text>
      </Box>
    );
  }

  if (state.phase === 'error') {
    return (
      <Box flexDirection="column">
        <Text bold color="red">Audit Console — Error</Text>
        <Text color="red">{state.error}</Text>
        <Text dimColor>Check mcpd is running and accessible at {mcpdUrl}</Text>
      </Box>
    );
  }

  // Detail view
  if (state.detailEvent) {
    return (
      <Box flexDirection="column" height={stdout.rows}>
        <Box flexGrow={1}>
          <AuditDetail event={state.detailEvent} scrollOffset={state.detailScrollOffset} height={height} />
        </Box>
        <Box borderStyle="single" borderColor="gray" paddingX={1}>
          <Text dimColor>[{'\u2191\u2193'}] scroll  [PgUp/Dn] page  [Esc] back  [q] quit</Text>
        </Box>
      </Box>
    );
  }

  // Main view
  const sidebarHint = state.showSidebar
    ? '[\u2191\u2193] session  [Enter] select  [k] kind  [d] date  [Esc] close  [q] quit'
    : state.focusedEventIdx === -1
      ? '[\u2191] nav  [k] kind  [d] date  [Enter] detail  [Esc] sidebar  [q] quit'
      : '[\u2191\u2193] nav  [PgUp/Dn] page  [a] follow  [k] kind  [d] date  [Enter] detail  [Esc] sidebar  [q] quit';

  return (
    <Box flexDirection="column" height={stdout.rows}>
      {/* Header */}
      <Box paddingX={1}>
        <Text bold color="cyan">Audit Console</Text>
        <Text dimColor>  {state.totalEvents} total events</Text>
        {state.kindFilter && <Text color="yellow">  kind: {EVENT_KIND_LABELS[state.kindFilter] ?? state.kindFilter}</Text>}
        {state.dateFilter !== 'all' && <Text color="magenta">  date: {DATE_FILTER_LABELS[state.dateFilter]}</Text>}
      </Box>

      {/* Body */}
      <Box flexGrow={1}>
        {state.showSidebar && (
          <AuditSidebar
            sessions={state.sessions}
            selectedIdx={state.selectedSessionIdx}
            projectFilter={state.projectFilter}
            dateFilter={state.dateFilter}
            height={height}
          />
        )}
        <AuditTimeline events={state.events} height={height} focusedIdx={state.focusedEventIdx} />
      </Box>

      {/* Footer */}
      <Box borderStyle="single" borderColor="gray" paddingX={1}>
        <Text dimColor>{sidebarHint}</Text>
      </Box>
    </Box>
  );
}

// ── Render entry point ──

export interface AuditRenderOptions {
  mcpdUrl: string;
  token?: string;
  projectFilter?: string;
}

export async function renderAuditConsole(opts: AuditRenderOptions): Promise<void> {
  const instance = render(
    <AuditApp mcpdUrl={opts.mcpdUrl} token={opts.token} projectFilter={opts.projectFilter} />,
  );
  await instance.waitUntilExit();
}
