import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Box, Button, Icon, IconButton, Icons, Input, Scroll, Text } from 'folds';
import { EventType } from 'matrix-js-sdk';
import { Page, PageContent, PageHeader } from '../../components/page';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoom } from '../../hooks/useRoom';
import { useRoomName } from '../../hooks/useRoomMeta';
import { SequenceCard } from '../../components/sequence-card';
import { SequenceCardStyle } from '../common-settings/styles.css';

type ExportFormat = 'txt' | 'json' | 'html';

const FORMAT_LABELS: Record<ExportFormat, string> = {
  txt: 'Plain Text',
  json: 'JSON',
  html: 'HTML',
};

const PAGE_LIMIT = 100;
// Hard cap on back-pagination requests. Without a fromDate, "export all" would
// otherwise decrypt and hold every message in the room, hammering the server and
// risking an OOM/freeze with no way to stop. 200 pages × 100 ≈ 20,000 events.
const MAX_EXPORT_PAGES = 200;

type ExportRoomHistoryProps = {
  requestClose: () => void;
};

export function ExportRoomHistory({ requestClose }: ExportRoomHistoryProps) {
  const mx = useMatrixClient();
  const room = useRoom();
  const roomName = useRoomName(room);

  const [format, setFormat] = useState<ExportFormat>('txt');
  const [fromDate, setFromDate] = useState('');
  const [toDate, setToDate] = useState('');
  const [exporting, setExporting] = useState(false);
  const [exportCount, setExportCount] = useState(0);
  const [notice, setNotice] = useState('');
  const cancelledRef = useRef(false);

  const handleCancel = useCallback(() => {
    cancelledRef.current = true;
  }, []);

  // Stop an in-flight export if the panel unmounts (closing settings mid-export
  // would otherwise keep paginating + decrypting in the background).
  useEffect(
    () => () => {
      cancelledRef.current = true;
    },
    [],
  );

  const handleExport = useCallback(async () => {
    if (exporting) return;
    cancelledRef.current = false;
    setExporting(true);
    setExportCount(0);
    setNotice('');

    try {
      const fromTs = fromDate ? new Date(`${fromDate}T00:00:00`).getTime() : null;
      const toTs = toDate ? new Date(`${toDate}T23:59:59`).getTime() : null;

      type MsgRecord = {
        ts: number;
        sender: string;
        body: string;
        eventId: string;
        msgtype: string;
      };

      const collected: MsgRecord[] = [];
      // timeline.getEvents() returns the entire growing window on every call,
      // so we must deduplicate by eventId to avoid re-adding the same events
      // on each pagination step.
      const seen = new Set<string>();
      const timeline = room.getLiveTimeline();
      let canLoadMore = true;
      // Track the oldest collected timestamp incrementally so the fromTs check
      // doesn't rescan the whole `collected` array on every pagination step.
      let oldestTs = Number.POSITIVE_INFINITY;
      // Oldest RAW message ts paginated (tracked BEFORE the fromTs filter). The
      // date-range early-break must use this — oldestTs only ever holds collected
      // events (all >= fromTs), so it can never fall below fromTs and the export
      // would over-paginate to the page cap and show a misleading "truncated".
      let oldestRawTs = Number.POSITIVE_INFINITY;

      const addEvents = async (events: ReturnType<typeof timeline.getEvents>) => {
        for (const ev of events) {
          const evId = ev.getId();
          if (!evId || seen.has(evId)) continue;
          seen.add(evId);
          // Advance the raw pagination boundary for EVERY event (any type,
          // decrypted or not) — getTs() is unencrypted metadata. Gating this on
          // a decrypted m.room.message let undecryptable/non-message old events
          // stall oldestRawTs, so the fromTs break never fired → over-paginate
          // and a false "truncated".
          const ts = ev.getTs();
          // Require a positive ts: an event with a bogus 0/negative
          // origin_server_ts must not collapse the boundary and trigger an early
          // break (silent under-pagination in the export).
          if (ts > 0 && ts < oldestRawTs) oldestRawTs = ts;
          // Attempt decryption for events that haven't been decrypted yet
          // (paginateEventTimeline may fetch events before the SDK decrypts them)
          if (ev.isEncrypted() && !ev.getClearContent()) {
            // eslint-disable-next-line no-await-in-loop
            await mx.decryptEventIfNeeded(ev).catch(() => undefined);
          }
          if (ev.getType() !== EventType.RoomMessage) continue;
          if (ev.isDecryptionFailure()) continue;
          if (fromTs !== null && ts < fromTs) continue;
          if (toTs !== null && ts > toTs) continue;
          const content = ev.getContent();
          const body: string = content.body ?? '';
          const msgtype: string = content.msgtype ?? '';
          if (!body) continue;
          if (ts < oldestTs) oldestTs = ts;
          collected.push({
            ts,
            sender: ev.getSender() ?? '',
            body,
            eventId: evId,
            msgtype,
          });
        }
        setExportCount(collected.length);
      };

      await addEvents(timeline.getEvents());

      // Paginate backwards until start, date range exceeded, cap hit, or cancel
      let pageCount = 0;
      let truncated = false;
      let cancelled = false;
      while (canLoadMore) {
        if (cancelledRef.current) {
          cancelled = true;
          break;
        }
        // If we've paginated back past the fromTs boundary, there's nothing more
        // in range to fetch (use the raw paginated ts, not the collected one).
        if (fromTs !== null && oldestRawTs < fromTs) break;
        // Hard cap so "export all" can't run away and OOM the tab.
        if (pageCount >= MAX_EXPORT_PAGES) {
          truncated = true;
          break;
        }
        pageCount += 1;

        // eslint-disable-next-line no-await-in-loop
        canLoadMore = await mx.paginateEventTimeline(timeline, {
          backwards: true,
          limit: PAGE_LIMIT,
        });

        // eslint-disable-next-line no-await-in-loop
        await addEvents(timeline.getEvents());
      }

      if (cancelled) {
        setNotice(`Export cancelled after ${collected.length} messages.`);
        return;
      }

      // Sort chronologically (oldest first)
      collected.sort((a, b) => a.ts - b.ts);

      const exportedAt = new Date().toISOString();
      const dateStr = exportedAt.slice(0, 10);
      const safeRoomName = roomName.replace(/[^a-z0-9_-]/gi, '_').toLowerCase();

      let content = '';
      let mimeType = 'text/plain';
      let ext: string = format;

      if (format === 'txt') {
        const lines: string[] = [
          `# Export: ${roomName}`,
          `# Exported: ${exportedAt}`,
          `# Messages: ${collected.length}`,
          '',
        ];
        for (const msg of collected) {
          const d = new Date(msg.ts);
          const pad = (n: number) => String(n).padStart(2, '0');
          const dateLabel = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
          lines.push(`[${dateLabel}] ${msg.sender}: ${msg.body}`);
        }
        content = lines.join('\n');
        mimeType = 'text/plain';
        ext = 'txt';
      } else if (format === 'json') {
        const payload = {
          room: roomName,
          exportedAt,
          messages: collected.map((m) => ({
            ts: m.ts,
            sender: m.sender,
            body: m.body,
            eventId: m.eventId,
            type: m.msgtype,
          })),
        };
        content = JSON.stringify(payload, null, 2);
        mimeType = 'application/json';
        ext = 'json';
      } else {
        // HTML
        const esc = (s: string) =>
          s
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;');

        const msgRows = collected
          .map((msg) => {
            const d = new Date(msg.ts);
            const pad = (n: number) => String(n).padStart(2, '0');
            const dateLabel = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
            return `<div class="msg"><span class="ts">[${esc(dateLabel)}]</span> <span class="sender">${esc(msg.sender)}</span><span class="body">: ${esc(msg.body)}</span></div>`;
          })
          .join('\n');

        content = `<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>Export: ${esc(roomName)}</title>
<style>body{background:#0d0d0d;color:#c4d9ee;font-family:monospace;padding:20px}
.msg{margin:4px 0;line-height:1.5}
.ts{color:#555;font-size:0.85em}
.sender{color:#FF6B00;font-weight:bold}
.body{color:#c4d9ee}</style></head>
<body><h2 style="color:#00D4FF">Room: ${esc(roomName)}</h2>
<p style="color:#555">Exported ${esc(exportedAt)} &mdash; ${collected.length} messages</p>
<div class="messages">
${msgRows}
</div></body></html>`;
        mimeType = 'text/html';
        ext = 'html';
      }

      const blob = new Blob([content], { type: mimeType });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `export-${safeRoomName}-${dateStr}.${ext}`;
      a.click();
      URL.revokeObjectURL(url);

      if (truncated) {
        setNotice(
          `Export truncated to ${collected.length} messages (reached the ${MAX_EXPORT_PAGES}-page limit). Narrow the date range to export older history.`,
        );
      }
    } finally {
      setExporting(false);
    }
  }, [exporting, format, fromDate, toDate, mx, room, roomName]);

  return (
    <Page>
      <PageHeader outlined={false}>
        <Box grow="Yes" gap="200">
          <Box grow="Yes" alignItems="Center" gap="200">
            <Text as="h2" size="H3" truncate>
              Export
            </Text>
          </Box>
          <Box shrink="No">
            <IconButton onClick={requestClose} variant="Surface" aria-label="Close">
              <Icon src={Icons.Cross} />
            </IconButton>
          </Box>
        </Box>
      </PageHeader>
      <Box grow="Yes">
        <Scroll hideTrack visibility="Hover">
          <PageContent>
            <Box direction="Column" gap="700">
              {/* Format */}
              <Box direction="Column" gap="100">
                <Text size="L400">Format</Text>
                <SequenceCard
                  className={SequenceCardStyle}
                  variant="SurfaceVariant"
                  direction="Column"
                  gap="400"
                >
                  <Box gap="200" wrap="Wrap">
                    {(Object.keys(FORMAT_LABELS) as ExportFormat[]).map((f) => (
                      <Button
                        key={f}
                        size="300"
                        variant={format === f ? 'Primary' : 'Secondary'}
                        fill={format === f ? 'Soft' : 'None'}
                        radii="300"
                        onClick={() => setFormat(f)}
                        aria-pressed={format === f}
                      >
                        <Text size="B300">{FORMAT_LABELS[f]}</Text>
                      </Button>
                    ))}
                  </Box>
                </SequenceCard>
              </Box>

              {/* Date range */}
              <Box direction="Column" gap="100">
                <Text size="L400">Date Range (optional)</Text>
                <SequenceCard
                  className={SequenceCardStyle}
                  variant="SurfaceVariant"
                  direction="Column"
                  gap="400"
                >
                  <Box gap="400" wrap="Wrap">
                    <Box direction="Column" gap="100" style={{ flex: 1, minWidth: 160 }}>
                      <Text size="T300">From</Text>
                      <Input
                        type="date"
                        variant="Secondary"
                        size="400"
                        radii="300"
                        value={fromDate}
                        onChange={(e) => setFromDate(e.target.value)}
                      />
                    </Box>
                    <Box direction="Column" gap="100" style={{ flex: 1, minWidth: 160 }}>
                      <Text size="T300">To</Text>
                      <Input
                        type="date"
                        variant="Secondary"
                        size="400"
                        radii="300"
                        value={toDate}
                        onChange={(e) => setToDate(e.target.value)}
                      />
                    </Box>
                  </Box>
                  <Text size="T200" priority="300">
                    Leave blank to export all available history.
                  </Text>
                </SequenceCard>
              </Box>

              {/* Export */}
              <Box direction="Column" gap="100">
                <Text size="L400">Download</Text>
                <SequenceCard
                  className={SequenceCardStyle}
                  variant="SurfaceVariant"
                  direction="Column"
                  gap="400"
                >
                  <Box alignItems="Center" gap="400" justifyContent="SpaceBetween">
                    <Text size="T300" priority="300">
                      {exporting
                        ? `Exporting… ${exportCount} messages`
                        : 'Export will download automatically.'}
                    </Text>
                    {exporting ? (
                      <Button
                        size="400"
                        variant="Critical"
                        fill="Soft"
                        radii="300"
                        onClick={handleCancel}
                        before={<Icon src={Icons.Cross} size="100" />}
                      >
                        <Text size="B400">Cancel</Text>
                      </Button>
                    ) : (
                      <Button
                        size="400"
                        variant="Primary"
                        fill="Solid"
                        radii="300"
                        onClick={handleExport}
                        before={<Icon src={Icons.Download} size="100" />}
                      >
                        <Text size="B400">Export</Text>
                      </Button>
                    )}
                  </Box>
                  {notice && (
                    <Text size="T200" priority="400">
                      {notice}
                    </Text>
                  )}
                </SequenceCard>
              </Box>
            </Box>
          </PageContent>
        </Scroll>
      </Box>
    </Page>
  );
}
