import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { GroupTextCracker, type ProgressReport } from 'meshcore-hashtag-cracker';
import NoSleep from 'nosleep.js';
import type { RawPacket, Channel } from '../types';
import { api } from '../api';
import { toast } from './ui/sonner';
import { cn } from '@/lib/utils';
import { extractPacketPayloadHex } from '../utils/pathUtils';

interface CrackedChannel {
  channelName: string;
  key: string;
  packetId: number;
  message: string;
  crackedAt: number;
}

interface QueueItem {
  packet: RawPacket;
  attempts: number;
  lastAttemptLength: number;
  status: 'pending' | 'cracking' | 'cracked' | 'failed';
}

export interface CrackerPanelProps {
  packets: RawPacket[];
  channels: Channel[];
  onChannelCreate: (name: string, key: string) => Promise<void>;
  onRunningChange?: (running: boolean) => void;
  visible?: boolean;
}

export function CrackerPanel({
  packets,
  channels,
  onChannelCreate,
  onRunningChange,
  visible = false,
}: CrackerPanelProps) {
  const [isRunning, setIsRunning] = useState(false);
  const [maxLength, setMaxLength] = useState(6);
  const [maxLengthInput, setMaxLengthInput] = useState('6');
  const [retryFailedAtNextLength, setRetryFailedAtNextLength] = useState(false);
  const [decryptHistorical, setDecryptHistorical] = useState(true);
  const [turboMode, setTurboMode] = useState(false);
  const [twoWordMode, setTwoWordMode] = useState(false);
  const [progress, setProgress] = useState<ProgressReport | null>(null);
  const [queue, setQueue] = useState<Map<number, QueueItem>>(new Map());
  const [crackedChannels, setCrackedChannels] = useState<CrackedChannel[]>([]);
  const [wordlistLoaded, setWordlistLoaded] = useState(false);
  const [gpuAvailable, setGpuAvailable] = useState<boolean | null>(null);
  const [undecryptedPacketCount, setUndecryptedPacketCount] = useState<number | null>(null);
  const [skippedDuplicates, setSkippedDuplicates] = useState(0);

  const crackerRef = useRef<GroupTextCracker | null>(null);
  const noSleepRef = useRef<NoSleep | null>(null);
  const isRunningRef = useRef(false);
  const abortedRef = useRef(false);
  const isProcessingRef = useRef(false);
  const queueRef = useRef<Map<number, QueueItem>>(new Map());
  const retryFailedRef = useRef(false);
  const maxLengthRef = useRef(6);
  const decryptHistoricalRef = useRef(true);
  const turboModeRef = useRef(false);
  const twoWordModeRef = useRef(false);
  const undecryptedIdsRef = useRef<Set<number>>(new Set());
  const seenPayloadsRef = useRef<Set<string>>(new Set());
  const existingChannelKeysRef = useRef<Set<string>>(new Set());

  // Initialize cracker and NoSleep
  useEffect(() => {
    const cracker = new GroupTextCracker();
    crackerRef.current = cracker;
    setGpuAvailable(cracker.isGpuAvailable());

    const noSleep = new NoSleep();
    noSleepRef.current = noSleep;

    return () => {
      cracker.destroy();
      crackerRef.current = null;
      noSleep.disable();
      noSleepRef.current = null;
    };
  }, []);

  // Load wordlist dynamically when panel becomes visible for the first time
  useEffect(() => {
    if (!visible || wordlistLoaded) return;

    import('meshcore-hashtag-cracker/wordlist')
      .then(({ ENGLISH_WORDLIST }) => {
        if (crackerRef.current) {
          crackerRef.current.setWordlist(ENGLISH_WORDLIST);
          setWordlistLoaded(true);
        }
      })
      .catch((err) => {
        console.error('Failed to load wordlist:', err);
        toast.error('Failed to load wordlist', {
          description: 'Channel finder will not be available',
        });
      });
  }, [visible, wordlistLoaded]);

  // Fetch undecrypted packet count
  useEffect(() => {
    const fetchCount = () => {
      api
        .getUndecryptedPacketCount()
        .then(({ count }) => setUndecryptedPacketCount(count))
        .catch(() => setUndecryptedPacketCount(null));
    };
    fetchCount();
    // Refresh hourly (this is just for display; not critical to be up-to-date)
    const interval = setInterval(fetchCount, 3600000);
    return () => clearInterval(interval);
  }, []);

  // Get existing channel keys for filtering (memoized to avoid recreating on every render)
  const existingChannelKeys = useMemo(
    () => new Set(channels.map((c) => c.key.toUpperCase())),
    [channels]
  );

  useEffect(() => {
    existingChannelKeysRef.current = existingChannelKeys;
  }, [existingChannelKeys]);

  // Filter packets to only undecrypted GROUP_TEXT
  const undecryptedGroupText = useMemo(
    () => packets.filter((p) => p.payload_type === 'GROUP_TEXT' && !p.decrypted),
    [packets]
  );

  // Update queue when packets change (deduplicated by payload)
  // Note: We intentionally depend on .length only to avoid re-running on every array identity change
  useEffect(() => {
    let newSkipped = 0;

    setQueue((prev) => {
      const newQueue = new Map(prev);
      let changed = false;

      for (const packet of undecryptedGroupText) {
        if (!newQueue.has(packet.id)) {
          // Extract payload and check for duplicates
          const payload = extractPacketPayloadHex(packet.data);
          if (payload && seenPayloadsRef.current.has(payload)) {
            // Skip - we already have a packet with this payload queued
            newSkipped++;
            continue;
          }

          // Track this payload as seen
          if (payload) {
            seenPayloadsRef.current.add(payload);
          }

          newQueue.set(packet.id, {
            packet,
            attempts: 0,
            lastAttemptLength: 0,
            status: 'pending',
          });
          changed = true;
        }
      }

      if (changed) {
        queueRef.current = newQueue;
        return newQueue;
      }
      return prev;
    });

    if (newSkipped > 0) {
      setSkippedDuplicates((prev) => prev + newSkipped);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [undecryptedGroupText.length]);

  // Keep refs in sync with state
  useEffect(() => {
    queueRef.current = queue;
  }, [queue]);

  useEffect(() => {
    retryFailedRef.current = retryFailedAtNextLength;
  }, [retryFailedAtNextLength]);

  useEffect(() => {
    maxLengthRef.current = maxLength;
  }, [maxLength]);

  useEffect(() => {
    setMaxLengthInput(String(maxLength));
  }, [maxLength]);

  useEffect(() => {
    decryptHistoricalRef.current = decryptHistorical;
  }, [decryptHistorical]);

  useEffect(() => {
    turboModeRef.current = turboMode;
  }, [turboMode]);

  useEffect(() => {
    twoWordModeRef.current = twoWordMode;
  }, [twoWordMode]);

  // Keep undecrypted IDs ref in sync - used to skip packets already decrypted by other means
  useEffect(() => {
    undecryptedIdsRef.current = new Set(undecryptedGroupText.map((p) => p.id));
  }, [undecryptedGroupText]);

  // Notify parent of running state changes
  useEffect(() => {
    onRunningChange?.(isRunning);
  }, [isRunning, onRunningChange]);

  // Stats (cracking count is implicit - if progress is shown, we're cracking one)
  const pendingCount = Array.from(queue.values()).filter((q) => q.status === 'pending').length;
  const crackedCount = Array.from(queue.values()).filter((q) => q.status === 'cracked').length;
  const failedCount = Array.from(queue.values()).filter((q) => q.status === 'failed').length;

  // Process next packet in queue
  const processNext = useCallback(async () => {
    // Prevent concurrent processing
    if (isProcessingRef.current) return;
    if (!crackerRef.current || !isRunningRef.current) return;

    const currentQueue = queueRef.current;

    // Find next pending packet
    let nextItem: QueueItem | null = null;
    let nextId: number | null = null;

    for (const [id, item] of currentQueue.entries()) {
      if (item.status === 'pending') {
        nextItem = item;
        nextId = id;
        break;
      }
    }

    // If no pending and retry option is enabled, pick the failed one with lowest lastAttemptLength
    if (!nextItem && retryFailedRef.current) {
      const failedItems = Array.from(currentQueue.entries()).filter(
        ([, item]) => item.status === 'failed' && item.lastAttemptLength < 10 // Hard cap at length 10
      );
      if (failedItems.length > 0) {
        // Sort by lastAttemptLength ascending and pick the first (lowest)
        failedItems.sort((a, b) => a[1].lastAttemptLength - b[1].lastAttemptLength);
        [nextId, nextItem] = failedItems[0];
      }
    }

    if (!nextItem || nextId === null) {
      // Nothing to process right now, but keep running and check again later
      if (isRunningRef.current) {
        setTimeout(() => processNext(), 1000);
      }
      return;
    }

    // Check if this packet is still undecrypted - it may have been decrypted
    // by historical decrypt when we cracked another packet from the same channel
    if (!undecryptedIdsRef.current.has(nextId)) {
      // Already decrypted by other means, remove from queue and continue
      setQueue((prev) => {
        const updated = new Map(prev);
        updated.delete(nextId);
        return updated;
      });
      if (isRunningRef.current) {
        setTimeout(() => processNext(), 10);
      }
      return;
    }

    // Lock processing
    isProcessingRef.current = true;

    const currentMaxLength = maxLengthRef.current;
    const isRetry = nextItem.lastAttemptLength > 0;
    const targetLength = isRetry ? nextItem.lastAttemptLength + 1 : currentMaxLength;

    try {
      const result = await crackerRef.current.crack(
        nextItem.packet.data,
        {
          maxLength: targetLength,
          useSenderFilter: true,
          useTimestampFilter: true,
          useUtf8Filter: true,
          useTwoWordCombinations: twoWordModeRef.current,
          ...(turboModeRef.current && { gpuDispatchMs: 10000 }),
          // For retries, skip dictionary and shorter lengths - we already checked those
          ...(isRetry && {
            useDictionary: false,
            useTwoWordCombinations: false,
            startingLength: targetLength,
          }),
        },
        (prog) => {
          setProgress(prog);
        }
      );

      if (abortedRef.current) {
        abortedRef.current = false;
        isProcessingRef.current = false;
        setProgress(null);
        return;
      }

      if (result.found && result.roomName && result.key) {
        // Success!
        setQueue((prev) => {
          const updated = new Map(prev);
          const item = updated.get(nextId!);
          if (item) {
            updated.set(nextId!, {
              ...item,
              status: 'cracked',
              attempts: item.attempts + 1,
              lastAttemptLength: targetLength,
            });
          }
          return updated;
        });

        const newCracked: CrackedChannel = {
          channelName: result.roomName,
          key: result.key,
          packetId: nextId!,
          message: result.decryptedMessage || '',
          crackedAt: Date.now(),
        };
        setCrackedChannels((prev) => [...prev, newCracked]);

        // Auto-add channel if not already exists
        const keyUpper = result.key.toUpperCase();
        if (!existingChannelKeysRef.current.has(keyUpper)) {
          try {
            const channelName = '#' + result.roomName;
            await onChannelCreate(channelName, result.key);
            // Optionally decrypt any other historical packets with this newly discovered key
            // This prevents wasting cracking cycles on packets from the same channel
            if (decryptHistoricalRef.current) {
              await api.decryptHistoricalPackets({
                key_type: 'channel',
                channel_name: channelName,
              });
            }
          } catch (err) {
            console.error('Failed to create channel or decrypt historical:', err);
            toast.error('Failed to save found channel', {
              description:
                err instanceof Error ? err.message : 'Channel discovered but could not be saved',
            });
          }
        }
      } else {
        // Failed
        setQueue((prev) => {
          const updated = new Map(prev);
          const item = updated.get(nextId!);
          if (item) {
            updated.set(nextId!, {
              ...item,
              status: 'failed',
              attempts: item.attempts + 1,
              lastAttemptLength: targetLength,
            });
          }
          return updated;
        });
      }
    } catch (err) {
      console.error('Cracking error:', err);
      setQueue((prev) => {
        const updated = new Map(prev);
        const item = updated.get(nextId!);
        if (item) {
          updated.set(nextId!, {
            ...item,
            status: 'failed',
            attempts: item.attempts + 1,
            lastAttemptLength: targetLength,
          });
        }
        return updated;
      });
    }

    // Unlock processing
    isProcessingRef.current = false;
    setProgress(null);

    // Continue processing if still running
    if (isRunningRef.current) {
      setTimeout(() => processNext(), 100);
    }
  }, [onChannelCreate]);

  // Start/stop handlers
  const handleStart = () => {
    if (!gpuAvailable) {
      toast.error('WebGPU not available', {
        description:
          typeof window !== 'undefined' && !window.isSecureContext
            ? 'WebGPU requires HTTPS when not on localhost. Set up a certificate or configure your browser to treat this origin as secure.'
            : 'Channel finder requires Chrome 113+ or Edge 113+ with WebGPU support.',
      });
      return;
    }
    setIsRunning(true);
    isRunningRef.current = true;
    abortedRef.current = false;
    noSleepRef.current?.enable();
    processNext();
  };

  const handleStop = () => {
    setIsRunning(false);
    isRunningRef.current = false;
    abortedRef.current = true;
    crackerRef.current?.abort();
    noSleepRef.current?.disable();
  };

  return (
    <div className="flex flex-col h-full p-3 gap-3 bg-background border-t border-border overflow-auto">
      <div className="flex items-center gap-3 flex-wrap">
        <div className="flex items-center gap-2">
          <label htmlFor="cracker-max-length" className="text-sm text-muted-foreground">
            Max Length:
          </label>
          <input
            id="cracker-max-length"
            type="number"
            min={1}
            max={10}
            value={maxLengthInput}
            onChange={(e) => {
              const nextValue = e.target.value;
              setMaxLengthInput(nextValue);
              if (nextValue === '') return;
              const parsed = Number.parseInt(nextValue, 10);
              if (Number.isNaN(parsed)) return;
              setMaxLength(Math.min(10, Math.max(1, parsed)));
            }}
            onBlur={() => {
              const parsed = Number.parseInt(maxLengthInput, 10);
              const nextValue = Number.isNaN(parsed)
                ? maxLength
                : Math.min(10, Math.max(1, parsed));
              setMaxLengthInput(String(nextValue));
              if (nextValue !== maxLength) {
                setMaxLength(nextValue);
              }
            }}
            className="w-14 px-2 py-1 text-sm bg-muted border border-border rounded"
          />
        </div>

        <label className="flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
          <input
            type="checkbox"
            checked={retryFailedAtNextLength}
            onChange={(e) => setRetryFailedAtNextLength(e.target.checked)}
            className="rounded"
          />
          Retry failed at n+1
        </label>

        <label className="flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
          <input
            type="checkbox"
            checked={decryptHistorical}
            onChange={(e) => setDecryptHistorical(e.target.checked)}
            className="rounded"
          />
          Decrypt historical packets if key found
        </label>
        {decryptHistorical && (
          <span className="text-xs text-muted-foreground">
            {undecryptedPacketCount !== null && undecryptedPacketCount > 0
              ? `(${undecryptedPacketCount.toLocaleString()} packets; messages will stream in as decrypted)`
              : '(messages will stream in as decrypted)'}
          </span>
        )}

        <label className="flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
          <input
            type="checkbox"
            checked={twoWordMode}
            onChange={(e) => setTwoWordMode(e.target.checked)}
            className="rounded"
          />
          Try word pairs
        </label>

        <label className="flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
          <input
            type="checkbox"
            checked={turboMode}
            onChange={(e) => setTurboMode(e.target.checked)}
            className="rounded"
          />
          Turbo mode (experimental)
        </label>
      </div>

      <button
        onClick={isRunning ? handleStop : handleStart}
        disabled={!wordlistLoaded || gpuAvailable === false}
        className={cn(
          'w-48 px-4 py-1.5 rounded text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
          isRunning
            ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
            : 'bg-primary text-primary-foreground hover:bg-primary/90',
          'disabled:opacity-50 disabled:cursor-not-allowed'
        )}
      >
        {isRunning
          ? 'Stop Search'
          : gpuAvailable === false
            ? 'GPU Not Available'
            : !wordlistLoaded
              ? 'Loading dictionary...'
              : 'Find Channels'}
      </button>

      {/* Status */}
      <div className="flex gap-4 text-sm">
        <span className="text-muted-foreground">
          Pending: <span className="text-foreground font-medium">{pendingCount}</span>
        </span>
        <span className="text-muted-foreground">
          Found: <span className="text-success font-medium">{crackedCount}</span>
        </span>
        <span className="text-muted-foreground">
          Failed: <span className="text-destructive font-medium">{failedCount}</span>
        </span>
        {skippedDuplicates > 0 && (
          <span className="text-muted-foreground">
            Skipped (dup):{' '}
            <span className="text-muted-foreground font-medium">{skippedDuplicates}</span>
          </span>
        )}
      </div>

      {/* Progress */}
      {progress && (
        <div className="space-y-1">
          <div className="flex justify-between text-xs text-muted-foreground">
            <span>
              {progress.phase === 'wordlist'
                ? 'Dictionary'
                : progress.phase === 'wordlist-pairs'
                  ? 'Word Pairs'
                  : progress.phase === 'bruteforce'
                    ? 'Bruteforce'
                    : 'Public Key'}
              {progress.phase === 'bruteforce' && ` - Length ${progress.currentLength}`}:{' '}
              {progress.currentPosition}
            </span>
            <span>
              {progress.rateKeysPerSec >= 1e9
                ? `${(progress.rateKeysPerSec / 1e9).toFixed(2)} Gkeys/s`
                : `${(progress.rateKeysPerSec / 1e6).toFixed(1)} Mkeys/s`}{' '}
              • ETA:{' '}
              {progress.etaSeconds < 60
                ? `${Math.round(progress.etaSeconds)}s`
                : `${Math.round(progress.etaSeconds / 60)}m`}
            </span>
          </div>
          <div
            className="h-2 bg-muted rounded overflow-hidden"
            role="progressbar"
            aria-valuenow={Math.round(progress.percent)}
            aria-valuemin={0}
            aria-valuemax={100}
            aria-label="Channel finder progress"
          >
            <div
              className="h-full bg-primary transition-all duration-200"
              style={{ width: `${progress.percent}%` }}
            />
          </div>
        </div>
      )}

      {/* GPU status */}
      {gpuAvailable === false && (
        <div className="text-sm text-destructive space-y-1.5" role="alert">
          <p>WebGPU not available.</p>
          {typeof window !== 'undefined' && !window.isSecureContext ? (
            <div className="rounded-md border border-destructive/30 bg-destructive/5 p-2.5 text-xs text-destructive/90">
              <p className="font-medium mb-1">WebGPU requires HTTPS when not on localhost.</p>
              <p>To enable it:</p>
              <ul className="list-disc ml-4 mt-1 space-y-0.5">
                <li>
                  Set up a TLS certificate (see the HTTPS section of README_ADVANCED.md, or re-run
                  the Docker setup script which can generate one automatically)
                </li>
                <li>
                  Or configure your browser to treat this origin as secure (sometimes called
                  &ldquo;insecure origins treated as secure&rdquo; in browser flags)
                </li>
              </ul>
            </div>
          ) : (
            <p>Channel finder requires Chrome 113+ or Edge 113+ with WebGPU support.</p>
          )}
        </div>
      )}
      {!wordlistLoaded && gpuAvailable !== false && (
        <div className="text-sm text-muted-foreground" role="status">
          Loading wordlist...
        </div>
      )}

      {/* Found channels list */}
      {crackedChannels.length > 0 && (
        <div>
          <div className="text-xs text-muted-foreground mb-1">Found Channels:</div>
          <div className="space-y-1">
            {crackedChannels.map((channel, i) => (
              <div
                key={i}
                className="text-sm bg-success/10 border border-success/20 rounded px-2 py-1"
              >
                <span className="text-success font-medium">#{channel.channelName}</span>
                <span className="text-muted-foreground ml-2 text-xs">
                  "{channel.message.slice(0, 50)}
                  {channel.message.length > 50 ? '...' : ''}"
                </span>
              </div>
            ))}
          </div>
        </div>
      )}

      <hr className="border-border" />
      <p className="text-sm text-muted-foreground leading-relaxed">
        For unknown-keyed GroupText packets, this will attempt to dictionary attack, then brute
        force payloads as they arrive, testing channel names up to the specified length to discover
        active channels on the local mesh (GroupText packets may not be hashtag messages; we have no
        way of knowing but try as if they are).
        <strong> Retry failed at n+1</strong> will return to the failed queue and pick up messages
        it couldn't find a key for, attempting them at one longer length.
        <strong> Try word pairs</strong> will also try every combination of two dictionary words
        concatenated together (e.g. "hello" + "world" = "#helloworld") after the single-word
        dictionary pass; this can substantially increase search time and also result in
        false-positives.
        <strong> Decrypt historical</strong> will run an async job on any channel name it finds to
        see if any historically captured packets will decrypt with that key.
        <strong> Turbo mode</strong> will push your GPU to the max (target dispatch time of 10s) and
        may allow accelerated searching and/or system instability.
      </p>
    </div>
  );
}
