"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import {
  DndContext,
  PointerSensor,
  closestCenter,
  useSensor,
  useSensors,
  type DragEndEvent,
} from "@dnd-kit/core";
import {
  SortableContext,
  arrayMove,
  rectSortingStrategy,
  useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import type { SourceState } from "@/lib/types";
import { useAction } from "@/hooks/useAction";
import { spanStyle, GRID_COL_MIN, GRID_ROW_PX, GRID_GAP, MAX_ROWS } from "@/lib/layout";
import { SourceCard } from "./cards/SourceCard";
import { ThingsCard } from "./cards/ThingsCard";

export function Dashboard() {
  const router = useRouter();
  const [items, setItems] = useState<SourceState[]>([]);
  const [loading, setLoading] = useState(true);
  const [refreshingAll, setRefreshingAll] = useState(false);
  const [refreshingId, setRefreshingId] = useState<string | null>(null);
  const [generating, setGenerating] = useState(false);
  const [editing, setEditing] = useState(false);

  // Initial paint from cache, then refresh stale sources in the background.
  useEffect(() => {
    let alive = true;
    (async () => {
      const cached = await fetch("/api/sources").then((r) => r.json());
      if (!alive) return;
      setItems(cached.sources);
      setLoading(false);
      const fresh = await fetch("/api/sources?fresh=1").then((r) => r.json());
      if (alive) setItems(fresh.sources);
    })();
    return () => {
      alive = false;
    };
  }, []);

  const refreshAll = useCallback(async () => {
    setRefreshingAll(true);
    try {
      const fresh = await fetch("/api/sources?force=1").then((r) => r.json());
      setItems(fresh.sources);
    } finally {
      setRefreshingAll(false);
    }
  }, []);

  const refreshOne = useCallback(async (id: string) => {
    setRefreshingId(id);
    try {
      const res = await fetch(`/api/sources/${id}/refresh`, { method: "POST" });
      if (res.ok) {
        const { source } = await res.json();
        setItems((prev) => prev.map((it) => (it.source.id === id ? source : it)));
      }
    } finally {
      setRefreshingId(null);
    }
  }, []);

  const generatePaper = useCallback(async () => {
    setGenerating(true);
    try {
      const res = await fetch("/api/edition", { method: "POST" });
      if (res.ok) router.push("/editions");
    } finally {
      setGenerating(false);
    }
  }, [router]);

  // Persist a layout patch (position / cols / rows) for one source.
  const patchSource = useCallback((id: string, patch: Record<string, number>) => {
    void fetch(`/api/sources/${id}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(patch),
    });
  }, []);

  // Drag-reorder: renumber enabled cards into the same set of position slots they
  // already occupy, leaving disabled (hidden) sources' positions untouched.
  const onDragEnd = useCallback(
    (e: DragEndEvent) => {
      const { active, over } = e;
      if (!over || active.id === over.id) return;
      setItems((prev) => {
        const enabled = prev.filter((it) => it.source.enabled);
        const oldIndex = enabled.findIndex((it) => it.source.id === active.id);
        const newIndex = enabled.findIndex((it) => it.source.id === over.id);
        if (oldIndex < 0 || newIndex < 0) return prev;
        const slots = enabled.map((it) => it.source.position).sort((a, b) => a - b);
        const reordered = arrayMove(enabled, oldIndex, newIndex);
        const posById = new Map<string, number>();
        reordered.forEach((it, i) => {
          const pos = slots[i];
          if (pos !== it.source.position) patchSource(it.source.id, { position: pos });
          posById.set(it.source.id, pos);
        });
        return prev
          .map((it) =>
            posById.has(it.source.id)
              ? { ...it, source: { ...it.source, position: posById.get(it.source.id)! } }
              : it,
          )
          .sort((a, b) => a.source.position - b.source.position);
      });
    },
    [patchSource],
  );

  // Live local resize, persisted on release.
  const resize = useCallback(
    (id: string, cols: number, rows: number, commit: boolean) => {
      setItems((prev) =>
        prev.map((it) =>
          it.source.id === id ? { ...it, source: { ...it.source, cols, rows } } : it,
        ),
      );
      if (commit) patchSource(id, { cols, rows });
    },
    [patchSource],
  );

  useAction(
    {
      id: "refresh-all",
      label: "Refresh all sources",
      combo: "mod+shift+r",
      group: "Actions",
      run: refreshAll,
    },
    [refreshAll],
  );
  useAction(
    {
      id: "generate-paper",
      label: "Generate today's paper",
      combo: "mod+shift+g",
      group: "Actions",
      run: generatePaper,
    },
    [generatePaper],
  );

  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
  );

  const enabled = items.filter((it) => it.source.enabled);
  const gridStyle: React.CSSProperties = {
    display: "grid",
    gridTemplateColumns: `repeat(auto-fill, minmax(${GRID_COL_MIN}px, 1fr))`,
    gridAutoRows: `${GRID_ROW_PX}px`,
    gridAutoFlow: "dense",
    gap: GRID_GAP,
  };

  return (
    <main className="w-full px-6 py-8">
      <div className="flex items-center justify-between mb-6">
        <p className="text-sm" style={{ color: "var(--text-muted)" }}>
          {new Date().toLocaleDateString(undefined, {
            weekday: "long",
            month: "long",
            day: "numeric",
          })}
        </p>
        <div className="flex gap-2">
          <Btn onClick={() => setEditing((v) => !v)} primary={editing}>
            {editing ? "Done" : "Edit layout"}
          </Btn>
          <Btn onClick={generatePaper} disabled={generating}>
            {generating ? "Generating…" : "Generate paper"}
          </Btn>
          <Btn onClick={refreshAll} disabled={refreshingAll} primary={!editing}>
            {refreshingAll ? "Refreshing…" : "Refresh all"}
          </Btn>
        </div>
      </div>

      {editing && (
        <p className="text-xs mb-3" style={{ color: "var(--text-faint)" }}>
          Drag cards to reorder · drag a card&rsquo;s bottom-right corner to resize · click
          Done when finished.
        </p>
      )}

      {loading ? (
        <p style={{ color: "var(--text-faint)" }}>Loading…</p>
      ) : editing ? (
        <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
          <SortableContext items={enabled.map((it) => it.source.id)} strategy={rectSortingStrategy}>
            <div style={gridStyle}>
              {enabled.map((it) => (
                <SortableTile key={it.source.id} state={it} onResize={resize}>
                  <CardFor
                    state={it}
                    onRefresh={() => refreshOne(it.source.id)}
                    refreshing={refreshingId === it.source.id}
                  />
                </SortableTile>
              ))}
            </div>
          </SortableContext>
        </DndContext>
      ) : (
        <div style={gridStyle}>
          {enabled.map((it) => (
            <div
              key={it.source.id}
              style={{ ...spanStyle(it.source.cols, it.source.rows), minHeight: 0 }}
            >
              <CardFor
                state={it}
                onRefresh={() => refreshOne(it.source.id)}
                refreshing={refreshingId === it.source.id}
              />
            </div>
          ))}
        </div>
      )}
    </main>
  );
}

function CardFor({
  state,
  onRefresh,
  refreshing,
}: {
  state: SourceState;
  onRefresh: () => void;
  refreshing: boolean;
}) {
  return state.source.kind === "todos" ? (
    <ThingsCard state={state} onRefresh={onRefresh} refreshing={refreshing} />
  ) : (
    <SourceCard state={state} onRefresh={onRefresh} refreshing={refreshing} />
  );
}

// A draggable, corner-resizable grid tile (edit mode only).
function SortableTile({
  state,
  onResize,
  children,
}: {
  state: SourceState;
  onResize: (id: string, cols: number, rows: number, commit: boolean) => void;
  children: React.ReactNode;
}) {
  const { source } = state;
  const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
    id: source.id,
  });
  const nodeRef = useRef<HTMLDivElement | null>(null);

  // Corner-resize: translate pointer movement into whole grid-cell span changes.
  const onResizeStart = (e: React.PointerEvent) => {
    e.preventDefault();
    e.stopPropagation();
    const grid = nodeRef.current?.parentElement;
    if (!grid) return;
    const cols = getComputedStyle(grid).gridTemplateColumns.split(" ").filter(Boolean).length || 1;
    const cellW = (grid.clientWidth - GRID_GAP * (cols - 1)) / cols;
    const stepX = cellW + GRID_GAP;
    const stepY = GRID_ROW_PX + GRID_GAP;
    const startX = e.clientX;
    const startY = e.clientY;
    const startCols = source.cols;
    const startRows = source.rows;
    (e.target as Element).setPointerCapture(e.pointerId);

    const clamp = (n: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, n));
    const move = (ev: PointerEvent) => {
      const nextCols = clamp(startCols + Math.round((ev.clientX - startX) / stepX), 1, cols);
      const nextRows = clamp(startRows + Math.round((ev.clientY - startY) / stepY), 1, MAX_ROWS);
      onResize(source.id, nextCols, nextRows, false);
    };
    const up = (ev: PointerEvent) => {
      const nextCols = clamp(startCols + Math.round((ev.clientX - startX) / stepX), 1, cols);
      const nextRows = clamp(startRows + Math.round((ev.clientY - startY) / stepY), 1, MAX_ROWS);
      onResize(source.id, nextCols, nextRows, true);
      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", up);
    };
    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
  };

  return (
    <div
      ref={(el) => {
        setNodeRef(el);
        nodeRef.current = el;
      }}
      style={{
        ...spanStyle(source.cols, source.rows),
        minHeight: 0,
        position: "relative",
        transform: CSS.Transform.toString(transform),
        transition,
        opacity: isDragging ? 0.4 : 1,
        zIndex: isDragging ? 10 : undefined,
        cursor: "grab",
        outline: "2px dashed var(--border-strong)",
        outlineOffset: -2,
        borderRadius: 8,
      }}
      {...attributes}
      {...listeners}
    >
      {/* Block inner clicks/scroll while editing; this overlay is the drag surface. */}
      <div style={{ pointerEvents: "none", height: "100%" }}>{children}</div>

      {/* Resize handle — bottom-right corner. */}
      <div
        onPointerDown={onResizeStart}
        title="Drag to resize"
        style={{
          position: "absolute",
          right: 2,
          bottom: 2,
          width: 16,
          height: 16,
          cursor: "nwse-resize",
          color: "var(--text-on-accent)",
          background: "var(--accent)",
          borderRadius: 4,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          fontSize: 10,
          lineHeight: 1,
          zIndex: 5,
        }}
      >
        ⤡
      </div>
    </div>
  );
}

function Btn({
  children,
  onClick,
  disabled,
  primary,
}: {
  children: React.ReactNode;
  onClick: () => void;
  disabled?: boolean;
  primary?: boolean;
}) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className="px-3 py-2 text-sm font-medium"
      style={{
        background: primary ? "var(--accent)" : "var(--bg-elevated)",
        color: primary ? "var(--text-on-accent)" : "var(--text)",
        border: `1px solid ${primary ? "var(--accent)" : "var(--border-strong)"}`,
        borderRadius: 6,
        opacity: disabled ? 0.6 : 1,
      }}
    >
      {children}
    </button>
  );
}
