import { useEffect, useRef, useState } from 'react';
import { CheckCircle2, AlertCircle, Loader2, X, StopCircle, Ban, Minus, Maximize2 } from 'lucide-react';
import { Card, CardBody } from './ui/Card';
import { Button } from './ui/Button';
import { Progress } from './ui/Progress';
import { api, type Backup, type LogEntry } from '@/lib/api';
import { cn } from '@/lib/cn';
import { formatBytes, backupSlug, formatDuration } from '@/lib/format';
import { useConfirm } from './ConfirmProvider';

const levelClass: Record<string, string> = {
  info:    'text-ink-400',
  success: 'text-emerald-400',
  warn:    'text-amber-400',
  error:   'text-red-400',
};

/**
 * Polls backup status + logs until the backup reaches a terminal state.
 * Renders as a floating card in the bottom-right corner.
 */
export function BackupProgress({
  id,
  onTerminal,
  onDismiss,
}: {
  id: string;
  /** Fired once when the backup transitions to a terminal state. The parent
   *  uses this to flip the "Backup running…" button back to "Backup now"
   *  immediately, even though this panel stays open until the user dismisses. */
  onTerminal: (b: Backup) => void;
  /** Fired when the user explicitly closes the panel. Parent should unmount us.
   *  Used to take no argument here — `(b: Backup | null) => void` — but the
   *  only caller (App.tsx) ignores the arg entirely. Trimmed to () => void
   *  so the type stops lying about what flows through. */
  onDismiss: () => void;
}) {
  const [backup, setBackup] = useState<Backup | null>(null);
  const [logs, setLogs]     = useState<LogEntry[]>([]);
  const [minimized, setMinimized] = useState(false);
  const [cancelling, setCancelling] = useState(false);
  const timer = useRef<number | undefined>(undefined);
  const terminalFired = useRef(false);
  const confirm = useConfirm();
  // Auto-scroll the log feed to the bottom on new lines, but ONLY when the
  // user is already at (or near) the bottom. If they've scrolled up to
  // read an earlier line, the next 2s poll would otherwise yank them back
  // down mid-sentence. We snapshot the scroll position right before each
  // state update and re-stick after if the user was tailing.
  const logBoxRef   = useRef<HTMLDivElement | null>(null);
  const wasAtBottom = useRef(true);
  const STICK_THRESHOLD_PX = 32;

  useEffect(() => {
    // Reset the "we already told the parent this backup ended" latch every
    // time the panel is pointed at a NEW backup id. Without this, the panel
    // being reused for backup B after backup A finished would never fire
    // onTerminal for B (the ref still reads `true` from A), so the parent's
    // "Backup running…" button stays stuck on Running after B completes —
    // until the user reloads the page. The two `terminalFired` write sites
    // below set it to true; this effect is the only place that clears it.
    terminalFired.current = false;
    let cancelled = false;
    const tick = async () => {
      try {
        const [b, l] = await Promise.all([
          api.backups.get(id),
          api.logs.list({ backup_id: id, limit: 30 }),
        ]);
        if (cancelled) return;
        // Snapshot scroll state BEFORE the state update triggers a re-render.
        // The post-render effect uses this to decide whether to re-stick.
        const box = logBoxRef.current;
        if (box) {
          wasAtBottom.current =
            (box.scrollHeight - box.scrollTop - box.clientHeight) <= STICK_THRESHOLD_PX;
        }
        setBackup(b);
        setLogs(l.items);
        if (['success', 'failed', 'cancelled'].includes(b.status)) {
          window.clearInterval(timer.current);
          // Fire onTerminal exactly once. The panel stays open so the user
          // can read final logs — the parent uses this callback to clear
          // its `activeBackupId` so the Backup-now button reverts.
          if ( ! terminalFired.current ) {
            terminalFired.current = true;
            onTerminal( b );
          }
        }
      } catch { /* keep trying */ }
    };
    tick();
    timer.current = window.setInterval(tick, 2000);
    return () => { cancelled = true; window.clearInterval(timer.current); };
  }, [id, onTerminal]);

  // Re-stick the log feed to the bottom after every log update, but only
  // when the user was already tailing. If they've scrolled up to read an
  // earlier line we leave them alone. Mirrors the pattern in MigrationProgress.
  useEffect(() => {
    const box = logBoxRef.current;
    if (!box) return;
    if (wasAtBottom.current) {
      box.scrollTop = box.scrollHeight;
    }
  }, [logs]);

  // No beforeunload guard here: the backup runs entirely server-side via
  // WP-Cron (self-rescheduling tick chain), and this view is only a status
  // poller. Navigating away — including clicking a WP-admin menu item, which
  // is a full-page load — does NOT stop or lose the backup, so a "Leave site?
  // changes may not be saved" prompt would be misleading. (The Import page
  // DOES keep its beforeunload because that upload happens in the browser and
  // really is lost on navigation.)

  const status = backup?.status ?? 'pending';
  const lastLine = logs[0];
  const isTerminal = ['success', 'failed', 'cancelled'].includes(status);

  // Restore-pivot: detect when this widget is showing a restore-in-flight
  // rather than a backup-in-flight, so the title doesn't lie. The old logic
  // checked "any log line in the window has a restore signature" — but a
  // backup record that was once used as a restore SOURCE will keep those old
  // restore lines forever, and a later RESUMED backup on the same record
  // would falsely render as "Restore complete" with backup-engine logs
  // beneath it. The user reported exactly that contradiction.
  //
  // The reliable signal is "what's happening RIGHT NOW" — the most recent
  // log line. Restore-pivot only when the latest event is restore-engine
  // output AND the backup record itself is NOT in a running/pending state
  // (a running backup is unambiguously a backup, never a restore).
  const restoreSignatures = [
    'Restoring from ',
    'Restoring from backup',
    'Restoring database from ',
    'Restoring ', // matches "Restoring uploads from ..."
    'All files downloaded and verified',
    'Database restored',
    'Restore complete',
    'Migration requested but source/target match',
    'Running URL/path migration',
  ];
  const latestMessage = logs[0]?.message ?? '';
  const latestIsRestore = restoreSignatures.some((sig) => latestMessage.startsWith(sig));
  // Backup-engine signatures that override a restore pivot. If the most
  // recent line is from the backup engine, this is definitely a backup
  // (probably a Resume), regardless of older restore lines in the window.
  const backupOngoingSignatures = [
    'Starting full backup',
    'Starting database backup',
    'Starting files backup',
    'Resume requested',
    'Resuming previous attempt',
    'Building uploads',
    'Building themes',
    'Building plugins',
    'Building mu-plugins',
    'Building others',
    'Building database',
    'Building db',
    'Building manifest',
    'Dumping database',
    'Database dump complete',
    'Backup manifest written',
    'Uploading ',
    'Uploaded ',
    'Chunk threshold',
    'Disk check OK',
  ];
  const latestIsBackup = backupOngoingSignatures.some((sig) => latestMessage.startsWith(sig));
  // Final decision: restore-pivot only when (a) latest is restore-engine,
  // (b) latest is NOT backup-engine, and (c) the backup record itself isn't
  // currently in flight (running/pending always means "this is a backup").
  const recordInFlight = status === 'running' || status === 'pending';
  const isRestoring = latestIsRestore && !latestIsBackup && !recordInFlight;
  const restoreDone = isRestoring && latestMessage === 'Restore complete';

  // If we're in restore-pivot mode and the restore finished, fire onTerminal
  // exactly once so the parent flips the "Running…" button back. Without this
  // the button stays stuck because the backup record's own status may never
  // transition to a terminal value (the engine isn't the one running the
  // restore — the restore endpoint runs synchronously in its own request).
  useEffect(() => {
    if (isRestoring && restoreDone && backup && !terminalFired.current) {
      terminalFired.current = true;
      onTerminal(backup);
      window.clearInterval(timer.current);
    }
  }, [isRestoring, restoreDone, backup, onTerminal]);

  // Granular progress: every backup is a sequence of discrete steps. Build
  // events (each archive part landing in files[]) and upload events (each
  // file × each destination) both count. That makes the bar advance during
  // BOTH phases instead of jumping only when uploads complete.
  //
  // Work model:
  //   build_units  = files built so far (we don't know the total until
  //                  building finishes; estimate with components.length as
  //                  a floor so the bar has something to grow against)
  //   upload_units = files × destinations
  //
  // We use the estimated component count up front so the user sees motion
  // immediately. Once all categories are built, the count is exact.
  const files          = backup?.files ?? [];
  const destSet        = new Set(backup?.destinations ?? []);
  const destCount      = Math.max(1, destSet.size);
  const components     = backup?.components ?? [];

  const buildDone      = files.length;
  // While building, we don't yet know how many parts each category will produce.
  // Use the larger of (components count) and (files we've already built) as an
  // estimate of build_total — keeps the bar from going backward as files appear.
  const buildEstimate  = Math.max( files.length, components.length );

  const uploadDone     = files.reduce(
    (sum, f) => sum + f.uploaded_to.filter((d) => destSet.has(d)).length,
    0,
  );
  const uploadTotal    = files.length * destCount;

  const doneUnits      = buildDone + uploadDone;
  const totalUnits     = buildEstimate + Math.max( uploadTotal, components.length * destCount );
  const hasMeaningful  = totalUnits > 0;
  const realPct        = hasMeaningful ? ( doneUnits / totalUnits ) * 100 : null;

  // Restore-pivot overrides the build/upload-based math entirely. Restore's
  // own progress doesn't fit "files × destinations"; estimate from log count
  // until "Restore complete" arrives, at which point we're definitively 100%.
  const pct =
    isRestoring && restoreDone ? 100 :
    isRestoring                ? Math.min(95, 10 + logs.length * 5) :
    status === 'success'       ? 100 :
    status === 'failed'        ? 100 :
    status === 'cancelled'     ? 100 :
    status === 'running'       ? (
      realPct !== null
        ? Math.max(5, Math.min(99, Math.round(realPct)))
        : Math.min(95, 5 + logs.length * 4)
    ) :
    5;

  // Step counter shown beneath the bar. Reads as "Step 3 of 14" while
  // running, "8 of 8 parts uploaded" once everything is in transit.
  const stepLabel = (() => {
    if (isRestoring) {
      // The build/upload step counter doesn't apply during restore — surface
      // the log line count instead so the user sees motion without confusion.
      return restoreDone ? null : `Restore step ${logs.length}`;
    }
    if (isTerminal && files.length > 0) {
      return `${uploadDone} of ${uploadTotal} parts uploaded`;
    }
    if (status === 'running' && totalUnits > 0) {
      return `Step ${doneUnits} of ${totalUnits}`;
    }
    return null;
  })();

  const statusIcon =
    isRestoring && restoreDone   ? <CheckCircle2 className="h-4 w-4 text-emerald-500" /> :
    status === 'success'         ? <CheckCircle2 className="h-4 w-4 text-emerald-500" /> :
    status === 'failed'          ? <AlertCircle  className="h-4 w-4 text-danger" /> :
    status === 'cancelled'       ? <Ban          className="h-4 w-4 text-ink-500" /> :
    status === 'cancelling'      ? <Loader2      className="h-4 w-4 text-amber-500 animate-spin" /> :
                                   <Loader2      className="h-4 w-4 text-brand-500 animate-spin" />;

  const title =
    isRestoring && restoreDone   ? 'Restore complete' :
    isRestoring                  ? 'Restoring from this backup' :
    status === 'success'         ? 'Backup complete' :
    status === 'failed'          ? 'Backup failed' :
    status === 'cancelled'       ? 'Backup cancelled' :
    status === 'cancelling'      ? 'Stopping…' :
                                   'Backup in progress';

  const doCancel = async () => {
    const ok = await confirm({
      title: 'Stop this backup?',
      description: 'The engine will abort at its next checkpoint. Any files already uploaded will remain on the destination.',
      confirmLabel: 'Stop backup',
      tone: 'danger',
    });
    if (!ok) return;
    setCancelling(true);
    try { await api.backups.cancel(id); }
    catch { /* the engine may already have finished — polling will reflect the real state */ }
    finally { setCancelling(false); }
  };

  /**
   * Dismiss the panel. Always tells the parent to unmount us — `activeBackupId`
   * has already been cleared on terminal transition via onTerminal(), so the
   * Backup-now button is already correct regardless of when the user closes.
   */
  const dismiss = () => {
    onDismiss();
  };

  // Minimized form: a compact pill showing the icon, title, and progress %.
  // Click anywhere on the body to restore. X stays available for full dismiss.
  if (minimized) {
    return (
      <div className="fixed bottom-4 right-4 left-4 sm:bottom-6 sm:right-6 sm:left-auto sm:w-[280px] z-[100000]">
        <Card className="shadow-pop">
          <CardBody className="!p-2.5">
            <div className="flex items-center gap-2">
              <button
                onClick={() => setMinimized(false)}
                title="Expand"
                className="flex-1 min-w-0 flex items-center gap-2 text-left rounded-md px-1.5 py-0.5 hover:bg-ink-50"
              >
                {statusIcon}
                <span className="flex-1 min-w-0">
                  <span className="block text-xs font-semibold text-ink-900 truncate">{title}</span>
                  <span className="block text-[10px] text-ink-500">
                    {pct}%{stepLabel ? ` · ${stepLabel}` : ''}
                  </span>
                </span>
                <Maximize2 className="h-3.5 w-3.5 text-ink-400 shrink-0" />
              </button>
              <button
                onClick={dismiss}
                title="Dismiss"
                className="h-7 w-7 grid place-items-center rounded-md text-ink-400 hover:text-ink-900 hover:bg-ink-100"
              >
                <X className="h-4 w-4" />
              </button>
            </div>
            {/* Slim progress bar so the user still sees motion while minimized */}
            <div className="mt-2">
              <Progress value={pct} />
            </div>
          </CardBody>
        </Card>
      </div>
    );
  }

  return (
    <div className="fixed bottom-4 right-4 left-4 sm:bottom-6 sm:right-6 sm:left-auto sm:w-[420px] z-[100000]">
      <Card className="shadow-pop">
        <CardBody className="space-y-3">
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-2">
              {statusIcon}
              <div>
                <p className="text-sm font-semibold text-ink-900">{title}</p>
                <p className="text-[11px] text-ink-500 font-mono">{backupSlug(backup?.date, id)}</p>
              </div>
            </div>
            <div className="flex items-center gap-0.5">
              <button onClick={() => setMinimized(true)} title="Minimize" aria-label="Minimize backup progress"
                className="h-7 w-7 grid place-items-center rounded-md text-ink-400 hover:text-ink-900 hover:bg-ink-100">
                <Minus className="h-4 w-4" />
              </button>
              <button onClick={dismiss} title="Dismiss" aria-label="Dismiss backup progress panel"
                className="h-7 w-7 grid place-items-center rounded-md text-ink-400 hover:text-ink-900 hover:bg-ink-100">
                <X className="h-4 w-4" />
              </button>
            </div>
          </div>

          <div>
            <div className="flex justify-between text-[11px] text-ink-500 mb-1.5">
              <span>{lastLine?.message ?? 'Starting…'}</span>
              <span title={stepLabel ?? undefined}>{pct}%</span>
            </div>
            <Progress value={pct} />
            {stepLabel && (
              <p className="mt-1 text-[10px] text-ink-400">{stepLabel}</p>
            )}
          </div>

          {/* Stop button (only while a BACKUP is in flight — never during
              restore. The backup cancel endpoint targets the backup engine's
              tick loop; restore runs synchronously and is uncancellable mid-
              flight, so a "Stop backup" button against a restore would be a
              no-op that misleads the user). */}
          {!isTerminal && status !== 'cancelling' && !isRestoring && (
            <Button
              size="sm"
              variant="secondary"
              leftIcon={<StopCircle className="h-3.5 w-3.5" />}
              onClick={doCancel}
              disabled={cancelling}
              className="text-danger border-red-200 hover:bg-red-50"
            >
              {cancelling ? 'Stopping…' : 'Stop backup'}
            </Button>
          )}
          {status === 'cancelling' && (
            <p className="text-[11px] text-amber-700">Stopping at the next checkpoint…</p>
          )}

          {backup && status === 'success' && (
            <div className="flex items-center gap-3 text-xs text-ink-600">
              <span>{formatBytes(backup.size)}</span>
              <span>·</span>
              <span>{formatDuration(backup.duration)}</span>
              <span>·</span>
              <span>{backup.destinations.length} destinations</span>
            </div>
          )}

          {backup && status === 'failed' && backup.error && (
            <p className="text-xs text-danger">{backup.error}</p>
          )}

          <div
            ref={logBoxRef}
            aria-live="polite"
            aria-label="Backup activity log"
            role="log"
            className={cn(
              'bg-ink-900 text-ink-100 rounded-lg p-2.5 font-mono text-[11px] leading-5 max-h-40 overflow-auto',
              logs.length === 0 && 'opacity-50',
            )}
          >
            {logs.length === 0 && <div className="text-ink-500">Waiting for first log line…</div>}
            {logs.slice().reverse().map((l) => (
              <div key={l.id} className="flex gap-2">
                <span className={cn('uppercase text-[9px] tracking-wider w-12 shrink-0', levelClass[l.level])}>{l.level}</span>
                <span className="break-all">{l.message}</span>
              </div>
            ))}
          </div>
        </CardBody>
      </Card>
    </div>
  );
}
