import { useEffect, useRef, useState } from "react";
import type { Editor as TiptapEditor } from "@tiptap/core";
import type { HocuspocusProvider } from "@hocuspocus/provider";
import { Cloud, CloudOff, AlertTriangle, Loader2 } from "lucide-react";
import { Tooltip } from "./Tooltip";

/**
 * Server-side persistence pipeline state, as returned by
 * `GET /api/storage/status`. Mirrors the shape of `StorageStatus`
 * in `backend/src/hf-storage.ts` - if you add a field there, add
 * it here too.
 */
interface StorageStatus {
  enabled: boolean;
  datasetId: string;
  datasetReady: boolean;
  lastLocalSaveAt: number | null;
  lastCloudPushAt: number | null;
  pendingPush: boolean;
  lastError: {
    stage: "dataset-create" | "local-save" | "cloud-push";
    message: string;
    statusCode?: number;
    at: number;
    docName?: string;
  } | null;
}

/**
 * What the user sees in the top bar, derived from BOTH the WS
 * connection state AND the server-side persistence pipeline.
 *
 * Severity ordering (worst first): error > offline > pending > saved.
 * The displayed status is always the worst applicable signal, so
 * a green "Saved" never wins over a red "Sync failed".
 */
type DisplayStatus =
  | "saved"        // WS connected + dataset ready + no error + recent push
  | "pending"      // edit in flight or push timer armed
  | "offline"      // WS disconnected (network or container restart)
  | "error";       // backend reports lastError in the pipeline

interface Props {
  editorInstance: TiptapEditor | null;
  providerRef: { current: HocuspocusProvider | null };
}

const POLL_MS = 5_000;

export function SyncIndicator({ editorInstance: _editorInstance, providerRef }: Props) {
  const [wsConnected, setWsConnected] = useState(false);
  const [hasLocalEdit, setHasLocalEdit] = useState(false);
  const [serverStatus, setServerStatus] = useState<StorageStatus | null>(null);
  const editTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  // ---- WS layer: connection + edit activity ------------------------------
  // Same lazy-provider polling pattern as the previous version: the
  // provider is created by <Editor> AFTER this component mounts, so a
  // useEffect([providerRef]) alone would never re-fire when it lands.
  useEffect(() => {
    let pollId: ReturnType<typeof setInterval> | null = null;

    const attach = (p: HocuspocusProvider) => {
      const onConnect = () => setWsConnected(true);
      const onDisconnect = () => setWsConnected(false);
      const onSynced = () => setWsConnected(true);
      p.on("connect", onConnect);
      p.on("disconnect", onDisconnect);
      p.on("synced", onSynced);

      // Seed + 1s reconcile loop (some HF proxies eat the first
      // `connect` event; without this we'd stay "Offline" forever).
      const seed = () => {
        const ws = (p as any).configuration?.websocketProvider;
        setWsConnected(ws?.status === "connected");
      };
      seed();
      const reconcile = setInterval(seed, 1000);

      // Listen at the Yjs layer so we see EVERY change (TipTap's
      // own `update` event misses hero/settings/citation edits
      // because those bypass prosemirror).
      const ydoc = (p as any).document as
        | { on: Function; off: Function }
        | undefined;
      const onYUpdate = (_u: Uint8Array, origin: unknown) => {
        if (origin === p) return; // remote update, not ours
        setHasLocalEdit(true);
        if (editTimerRef.current) clearTimeout(editTimerRef.current);
        // Local edit "settles" 1.5s after the last keystroke; this
        // is also roughly when the backend's `debouncedSave` fires
        // (2s), so the indicator briefly flashes pending then
        // recovers to saved/error based on the next poll.
        editTimerRef.current = setTimeout(() => setHasLocalEdit(false), 1500);
      };
      ydoc?.on?.("update", onYUpdate);

      return () => {
        p.off("connect", onConnect);
        p.off("disconnect", onDisconnect);
        p.off("synced", onSynced);
        ydoc?.off?.("update", onYUpdate);
        clearInterval(reconcile);
      };
    };

    let cleanup: (() => void) | null = null;
    if (providerRef.current) {
      cleanup = attach(providerRef.current);
    } else {
      pollId = setInterval(() => {
        const p = providerRef.current;
        if (!p) return;
        if (pollId) {
          clearInterval(pollId);
          pollId = null;
        }
        cleanup = attach(p);
      }, 100);
    }

    return () => {
      if (pollId) clearInterval(pollId);
      if (cleanup) cleanup();
    };
  }, [providerRef]);

  // ---- Server pipeline polling -------------------------------------------
  // Cheap GET every 5s. The backend tracker updates in-process on
  // every save/push/error so the worst-case latency for surfacing
  // a problem is one poll interval. We don't use SSE/WS for this
  // because the data is tiny, the polling interval is generous,
  // and adding another long-lived connection is more failure modes
  // than it's worth.
  useEffect(() => {
    let cancelled = false;
    let timer: ReturnType<typeof setTimeout> | null = null;

    const poll = async () => {
      try {
        const res = await fetch("/api/storage/status", {
          credentials: "include",
        });
        if (cancelled) return;
        if (res.ok) {
          const data = (await res.json()) as StorageStatus;
          setServerStatus(data);
        } else if (res.status === 403) {
          // Viewer (not an editor) - storage status isn't relevant
          // to them. Stop polling.
          return;
        }
      } catch {
        // Network blip - keep trying. The WS disconnection will
        // dominate the UI anyway in that case.
      }
      if (!cancelled) {
        timer = setTimeout(poll, POLL_MS);
      }
    };
    poll();

    return () => {
      cancelled = true;
      if (timer) clearTimeout(timer);
    };
  }, []);

  // ---- Derive the displayed status ---------------------------------------
  // Worst-applicable wins (see DisplayStatus jsdoc).
  const status: DisplayStatus = (() => {
    if (serverStatus?.lastError) return "error";
    if (!wsConnected) return "offline";
    if (hasLocalEdit || serverStatus?.pendingPush) return "pending";
    return "saved";
  })();

  // ---- beforeunload guard ------------------------------------------------
  // If there's an unsynced local edit OR a pending push OR a known
  // sync error, browsers should pop the standard "Leave site?"
  // confirmation. The exact message is ignored by modern browsers
  // (Chrome/Safari/Firefox show their own generic copy) but
  // setting `returnValue` is what triggers the prompt.
  useEffect(() => {
    const needsGuard = status === "pending" || status === "error" || status === "offline";
    if (!needsGuard) return;

    const handler = (e: BeforeUnloadEvent) => {
      e.preventDefault();
      // Legacy browsers (and TS types still hold this) want a string.
      e.returnValue = "";
      return "";
    };
    window.addEventListener("beforeunload", handler);
    return () => window.removeEventListener("beforeunload", handler);
  }, [status]);

  // ---- Render ------------------------------------------------------------
  const { icon, label, tooltip } = renderState(status, serverStatus);

  return (
    <Tooltip title={tooltip}>
      <span
        className={`sync-indicator sync-indicator--${status}`}
        role="status"
        aria-live="polite"
      >
        {icon}
        <span className="sync-indicator__label">{label}</span>
      </span>
    </Tooltip>
  );
}

function renderState(status: DisplayStatus, server: StorageStatus | null) {
  switch (status) {
    case "error": {
      const err = server?.lastError;
      const stageLabel: Record<string, string> = {
        "dataset-create": "Cloud storage setup failed",
        "local-save": "Local save failed",
        "cloud-push": "Cloud sync failed",
      };
      const label = err ? stageLabel[err.stage] ?? "Storage error" : "Storage error";
      const hint = err?.statusCode === 403
        ? " - your OAuth grant may be missing the `manage-repos` scope. Sign out and back in."
        : "";
      const tooltip = err
        ? `${label}: ${err.message}${hint}`
        : "Storage error";
      return {
        icon: <AlertTriangle size={14} />,
        label,
        tooltip,
      };
    }
    case "offline":
      return {
        icon: <CloudOff size={14} />,
        label: "Offline",
        tooltip: "Disconnected - reconnecting...",
      };
    case "pending":
      return {
        icon: <Loader2 size={14} className="spin" />,
        label: "Saving...",
        tooltip: server?.datasetReady === false
          ? "Saving locally - cloud sync starts after first successful dataset creation"
          : "Saving to cloud...",
      };
    case "saved":
    default: {
      const last = server?.lastCloudPushAt;
      const tooltip = last
        ? `All changes saved · last cloud sync ${formatRelative(last)}`
        : server?.datasetReady
        ? "All changes saved"
        : "Saved locally - cloud sync will start on first change";
      return {
        icon: <Cloud size={14} />,
        label: "Saved",
        tooltip,
      };
    }
  }
}

function formatRelative(ts: number): string {
  const diff = Date.now() - ts;
  if (diff < 5_000) return "just now";
  if (diff < 60_000) return `${Math.round(diff / 1000)}s ago`;
  if (diff < 3_600_000) return `${Math.round(diff / 60_000)}min ago`;
  return new Date(ts).toLocaleTimeString();
}
