/**
 * Where the book actually is, plus the manual escape hatches.
 *
 * The bar is deliberately loud. A previous version showed a small grey pill
 * that could read "Saved" while nothing was being written; anything other than
 * a clean, saved state now takes up space and stays until it is resolved.
 */
import { useEffect, useRef, useState } from "react";
import { Check, CloudOff, Download, Loader2, TriangleAlert, Upload } from "lucide-react";
import { keepMine, saveNow, useSync, useTheirs, type SyncStatus } from "@/lib/book-sync";
import { exportBook, importBook, parseBackup } from "@/lib/backup";
import { cn } from "@/lib/cn";

function relative(iso: string | null): string {
  if (!iso) return "";
  const secs = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
  if (secs < 10) return "just now";
  if (secs < 60) return `${secs}s ago`;
  const mins = Math.round(secs / 60);
  if (mins < 60) return `${mins}m ago`;
  const hours = Math.round(mins / 60);
  if (hours < 24) return `${hours}h ago`;
  return new Date(iso).toLocaleDateString();
}

const LABEL: Record<SyncStatus, string> = {
  starting: "Loading",
  unsaved: "Unsaved",
  saving: "Saving",
  saved: "Saved",
  error: "Not saved",
  conflict: "Changed elsewhere",
  disabled: "This browser only",
};

/** The compact header indicator. */
export function SaveStatus() {
  const status = useSync((s) => s.status);
  const lastSavedAt = useSync((s) => s.lastSavedAt);
  const detail = useSync((s) => s.detail);
  const [, tick] = useState(0);

  useEffect(() => {
    const id = setInterval(() => tick((n) => n + 1), 15_000);
    return () => clearInterval(id);
  }, []);

  const bad = status === "error" || status === "disabled" || status === "conflict";
  const pending = status === "unsaved";
  const Icon =
    status === "saving" || status === "starting"
      ? Loader2
      : status === "error"
        ? CloudOff
        : bad || pending
          ? TriangleAlert
          : Check;

  return (
    <button
      type="button"
      onClick={() => saveNow()}
      title={detail ?? LABEL[status]}
      aria-live="polite"
      className={cn(
        "inline-flex items-center gap-1.5 text-[12px] rounded-md px-2 py-1 hover:bg-neutral",
        bad
          ? "text-error font-medium"
          : pending
            ? "text-error"
            : "text-ink-3",
      )}
    >
      <Icon
        className={cn("size-3.5", (status === "saving" || status === "starting") && "animate-spin")}
      />
      <span>{LABEL[status]}</span>
      {status === "saved" && lastSavedAt ? (
        <span className="hidden md:inline text-ink-3 tabular-nums">{relative(lastSavedAt)}</span>
      ) : null}
    </button>
  );
}

/**
 * Full-width bar under the header, shown only when something needs attention
 * or the user asks for the backup controls.
 */
export function SaveBanner() {
  const status = useSync((s) => s.status);
  const detail = useSync((s) => s.detail);
  const conflict = useSync((s) => s.conflict);
  const fileRef = useRef<HTMLInputElement>(null);
  const [note, setNote] = useState<string | null>(null);

  async function onFile(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    e.target.value = "";
    if (!file) return;
    try {
      const data = parseBackup(await file.text());
      const counts = `${data.tasks.length} tasks, ${data.notes.length} notes, ${data.projects.length} projects`;
      if (!window.confirm(`Replace the current book with this file?\n\n${counts}`)) return;
      importBook(data);
      setNote("Imported. It will save to the server in a moment.");
    } catch (err) {
      setNote(`Could not import: ${err instanceof Error ? err.message : "unreadable file"}`);
    }
  }

  const needsAttention = status === "error" || status === "disabled" || status === "conflict";
  if (!needsAttention && !note) return null;

  return (
    <div className="border-b border-line bg-error-muted px-3 md:px-6 py-2 text-[13px] text-ink">
      <div className="flex flex-wrap items-center gap-x-3 gap-y-2">
        <TriangleAlert className="size-4 text-error shrink-0" />
        <span className="font-medium">{LABEL[status]}.</span>
        <span className="text-ink-2">{note ?? detail}</span>

        {conflict ? (
          <span className="flex items-center gap-2">
            <button
              type="button"
              onClick={keepMine}
              className="rounded-md border border-line bg-surface px-2 py-1 hover:border-ink-2"
            >
              Keep this browser&rsquo;s version
            </button>
            <button
              type="button"
              onClick={useTheirs}
              className="rounded-md border border-line bg-surface px-2 py-1 hover:border-ink-2"
            >
              Use the other version
            </button>
          </span>
        ) : null}

        <span className="ml-auto flex items-center gap-2">
          <button
            type="button"
            onClick={exportBook}
            title="Download the whole book as a JSON file"
            className="inline-flex items-center gap-1.5 rounded-md border border-line bg-surface px-2 py-1 hover:border-ink-2"
          >
            <Download className="size-3.5" />
            Export
          </button>
          <button
            type="button"
            onClick={() => fileRef.current?.click()}
            className="inline-flex items-center gap-1.5 rounded-md border border-line bg-surface px-2 py-1 hover:border-ink-2"
          >
            <Upload className="size-3.5" />
            Import
          </button>
          <input
            ref={fileRef}
            type="file"
            accept="application/json,.json"
            onChange={onFile}
            className="hidden"
          />
        </span>
      </div>
    </div>
  );
}
