// 리스트(표) 보기 — 고정 행 높이 가상 스크롤. 이름·크기·날짜를 나란히 비교하고
// 열 머리글로 바로 정렬한다. 선택·정렬·필터·파일 작업은 격자 보기와 완전히 공유한다.
import { IconArrowDown, IconArrowUp, IconTrash } from "../../ui/icons";
import {
  createEffect,
  createMemo,
  createSignal,
  For,
  on,
  onCleanup,
  onMount,
  Show,
  type Component,
} from "solid-js";
import { createStore, produce, reconcile } from "solid-js/store";
import {
  anchorIndex,
  info,
  markedIds,
  openLightbox,
  registerItemViewFocus,
  selected,
  selectRange,
  selectSingle,
  setSort,
  snapshot,
  sortDesc,
  sortKey,
  tagVersion,
  thumbVersions,
  toggleSelect,
  trashTag,
} from "../../state/store";
import {
  fileDetails,
  fileTagsBulk,
  setThumbPriority,
  thumbUrl,
  type FileDetails,
  type Snapshot,
  type SortKey,
} from "../../ipc/commands";
import { ko } from "../../i18n/ko";
import { openContextMenu } from "../fileops/ContextMenu";
import { handleItemKeys } from "./itemKeys";

const ROW_H = 38;
/** 화면 위·아래로 더 그려 두는 행 수 (스크롤 시 빈칸 방지) */
const OVERSCAN = 10;
/** 한 번에 메타/태그를 채워 넣는 최대 행 수 (백엔드 file_details 상한 500 이내) */
const BATCH_MAX = 400;
/**
 * 머리글과 행이 같은 열 정의를 공유한다.
 * 부가 정보(태그·해상도·길이)는 minmax(0,…)로 두어 창이 좁아지면 그 열부터 줄어든다 —
 * 이름·크기·날짜를 지키면서 가로 스크롤 없이 한 줄에 들어가게 하기 위함이다.
 */
const COLS =
  "2.75rem minmax(5rem,1fr) minmax(0,11rem) 3rem 4.75rem minmax(0,5.5rem) minmax(0,3.75rem) 8.5rem";

function fmtSize(bytes: number): string {
  if (bytes >= 1 << 30) return `${(bytes / (1 << 30)).toFixed(2)} GB`;
  if (bytes >= 1 << 20) return `${(bytes / (1 << 20)).toFixed(1)} MB`;
  if (bytes >= 1 << 10) return `${(bytes / (1 << 10)).toFixed(0)} KB`;
  return `${bytes} B`;
}

function fmtDate(ms: number | null | undefined): string {
  if (!ms) return "—";
  const d = new Date(ms);
  const p = (n: number) => String(n).padStart(2, "0");
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
}

function fmtDuration(ms: number | null | undefined): string {
  if (!ms) return "";
  const s = Math.round(ms / 1000);
  const m = Math.floor(s / 60);
  const h = Math.floor(m / 60);
  return h > 0
    ? `${h}:${String(m % 60).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`
    : `${m}:${String(s % 60).padStart(2, "0")}`;
}

export const ListView: Component = () => {
  let containerRef: HTMLDivElement | undefined;
  const [scrollTop, setScrollTop] = createSignal(0);
  const [viewportH, setViewportH] = createSignal(600);

  const count = () => snapshot()?.count ?? 0;

  // ── 가상 스크롤 ──
  const visibleRange = createMemo<[number, number]>(() => {
    const n = count();
    if (n === 0) return [0, -1];
    const first = Math.max(0, Math.floor(scrollTop() / ROW_H) - OVERSCAN);
    const last = Math.min(n - 1, Math.ceil((scrollTop() + viewportH()) / ROW_H) + OVERSCAN);
    return [first, last];
  });

  const indices = createMemo(() => {
    const [first, last] = visibleRange();
    const out: number[] = [];
    for (let i = first; i <= last; i++) out.push(i);
    return out;
  });

  let rafPending = false;
  const onScroll = () => {
    if (rafPending) return;
    rafPending = true;
    requestAnimationFrame(() => {
      rafPending = false;
      if (containerRef) setScrollTop(containerRef.scrollTop);
    });
  };

  /** 행을 화면 안으로 (키보드 이동·보기 전환용) */
  const revealRow = (index: number) => {
    const el = containerRef;
    if (!el) return;
    const top = index * ROW_H;
    if (top < el.scrollTop) el.scrollTop = top;
    else if (top + ROW_H > el.scrollTop + el.clientHeight)
      el.scrollTop = top + ROW_H - el.clientHeight;
  };

  onMount(() => {
    const el = containerRef!;
    // 크롬(툴바·브레드크럼·폴더 칩·사이드바)이 클릭 뒤 방향키를 여기로 되돌린다
    onCleanup(registerItemViewFocus(() => containerRef?.focus({ preventScroll: true })));
    // 높이는 폴링을 단일 진실원으로 — 값이 같으면 시그널이 갱신되지 않으므로 비용은 없다
    const tick = () => setViewportH(el.clientHeight);
    tick();
    const ro = new ResizeObserver(tick);
    ro.observe(el);
    const poll = setInterval(tick, 250);
    // 격자에서 넘어왔을 때 보고 있던 항목을 그대로 보여준다
    const anchor = anchorIndex();
    if (anchor >= 0) requestAnimationFrame(() => revealRow(anchor));
    onCleanup(() => {
      ro.disconnect();
      clearInterval(poll);
    });
  });

  // ── 지연 로드: 파일 메타 (이름·크기·날짜·길이) ──
  // 이름/경로는 이름변경·이동으로 바뀌므로 스냅샷이 갈릴 때 캐시를 버린다.
  const [detailsCache, setDetailsCache] = createStore<Record<number, FileDetails>>({});
  let detTimer: ReturnType<typeof setTimeout> | undefined;
  createEffect(on(snapshot, () => setDetailsCache(reconcile({})), { defer: true }));
  createEffect(() => {
    const [first, last] = visibleRange();
    const snap = snapshot();
    if (!snap || last < first) return;
    clearTimeout(detTimer);
    detTimer = setTimeout(() => {
      const need: number[] = [];
      for (let i = first; i <= last && need.length < BATCH_MAX; i++) {
        const id = snap.id(i);
        if (detailsCache[id] === undefined) need.push(id);
      }
      if (need.length === 0) return;
      void fileDetails(need)
        .then((ds) =>
          setDetailsCache(
            produce((s) => {
              for (const d of ds) s[d.id] = d;
            }),
          ),
        )
        .catch(() => {});
    }, 120);
  });
  onCleanup(() => clearTimeout(detTimer));

  // ── 지연 로드: 태그 (격자 오버레이와 같은 배치 조회) ──
  const [tagCache, setTagCache] = createStore<Record<number, string[]>>({});
  let tagTimer: ReturnType<typeof setTimeout> | undefined;
  createEffect(on(tagVersion, () => setTagCache(reconcile({})), { defer: true }));
  createEffect(() => {
    const [first, last] = visibleRange();
    const snap = snapshot();
    tagVersion(); // 태그가 바뀌면 다시 읽는다
    if (!snap || last < first) return;
    clearTimeout(tagTimer);
    tagTimer = setTimeout(() => {
      const need: number[] = [];
      for (let i = first; i <= last && need.length < BATCH_MAX; i++) {
        const id = snap.id(i);
        if (tagCache[id] === undefined) need.push(id);
      }
      if (need.length === 0) return;
      void fileTagsBulk(need)
        .then((links) =>
          setTagCache(
            produce((s) => {
              for (const id of need) s[id] = [];
              for (const l of links) {
                if (l.tagId === trashTag()) continue; // 삭제 예정은 🗑 배지가 담당
                (s[l.fileId] ??= []).push(l.name);
              }
            }),
          ),
        )
        .catch(() => {});
    }, 150);
  });
  onCleanup(() => clearTimeout(tagTimer));

  // 화면에 보이는데 썸네일이 없는 항목 우선 생성 요청
  let prioTimer: ReturnType<typeof setTimeout> | undefined;
  createEffect(() => {
    const [first, last] = visibleRange();
    const snap = snapshot();
    if (!snap || last < first) return;
    clearTimeout(prioTimer);
    prioTimer = setTimeout(() => {
      const ids: number[] = [];
      for (let i = first; i <= last && ids.length < BATCH_MAX; i++) {
        if (!snap.hasThumb(i)) ids.push(snap.id(i));
      }
      if (ids.length > 0) void setThumbPriority(ids);
    }, 200);
  });
  onCleanup(() => clearTimeout(prioTimer));

  // ── 상호작용 ──
  const onRowClick = (e: MouseEvent, snap: Snapshot, index: number) => {
    const id = snap.id(index);
    if (e.shiftKey) selectRange(snap, index);
    else if (e.ctrlKey || e.metaKey) toggleSelect(id, index);
    else selectSingle(id, index);
  };

  const onRowContextMenu = (e: MouseEvent, snap: Snapshot, index: number) => {
    e.preventDefault();
    const id = snap.id(index);
    if (!selected().has(id)) selectSingle(id, index);
    void fileDetails([id]).then((d) => {
      openContextMenu({ x: e.clientX, y: e.clientY, fileId: id, fileName: d[0]?.name });
    });
  };

  const onRowDragStart = (e: DragEvent, snap: Snapshot, index: number) => {
    const id = snap.id(index);
    if (!selected().has(id)) selectSingle(id, index);
    e.dataTransfer?.setData("application/x-archive-move", "1");
    if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
  };

  const onKeyDown = (e: KeyboardEvent) => {
    const snap = snapshot();
    if (!snap) return;
    handleItemKeys(e, snap, {
      neighbor: (from, key) =>
        key === "ArrowUp" || key === "ArrowLeft"
          ? from > 0
            ? from - 1
            : undefined
          : from < snap.count - 1
            ? from + 1
            : undefined,
      onMove: revealRow,
    });
  };

  // ── 날짜 열: 정렬이 '수정일'이면 수정일을, 그 밖에는 촬영일을 보여준다 ──
  const dateKey = (): SortKey => (sortKey() === "mtime" ? "mtime" : "taken");
  const dateLabel = () => (sortKey() === "mtime" ? ko.list.modified : ko.list.taken);
  const dateOf = (d: FileDetails | undefined) =>
    d ? (sortKey() === "mtime" ? d.mtimeMs : (d.takenAt ?? d.mtimeMs)) : undefined;

  /** 열 머리글 — 누르면 그 열로 정렬, 다시 누르면 방향 반전 */
  const SortHead: Component<{ label: string; col: SortKey; right?: boolean }> = (props) => (
    <button
      class="flex min-w-0 items-center gap-1 hover:text-[var(--text-primary)]"
      classList={{
        "justify-end": props.right,
        "text-[var(--text-primary)]": sortKey() === props.col,
      }}
      onClick={() => setSort(props.col, sortKey() === props.col ? !sortDesc() : true)}
      title={ko.list.sortHint(props.label)}
    >
      <span class="truncate">{props.label}</span>
      <Show when={sortKey() === props.col}>
        <span class="shrink-0">
          {sortDesc() ? <IconArrowDown size={12} /> : <IconArrowUp size={12} />}
        </span>
      </Show>
    </button>
  );

  return (
    <div class="flex min-h-0 flex-1 flex-col">
      {/* 머리글 — 열 폭은 행과 같은 정의를 쓴다 */}
      <div
        class="grid shrink-0 items-center gap-2 border-b border-[var(--border)] px-2 py-1.5 text-[11px] text-[var(--text-muted)]"
        style={{ "grid-template-columns": COLS }}
      >
        <span />
        <SortHead label={ko.list.name} col="name" />
        <span class="truncate">{ko.list.tags}</span>
        <span class="truncate">{ko.list.kind}</span>
        <SortHead label={ko.list.size} col="size" right />
        <span class="truncate text-right">{ko.list.dims}</span>
        <span class="truncate text-right">{ko.list.duration}</span>
        <SortHead label={dateLabel()} col={dateKey()} right />
      </div>

      <div
        ref={containerRef}
        onScroll={onScroll}
        onKeyDown={onKeyDown}
        tabindex={0}
        class="relative min-h-0 flex-1 overflow-y-auto overflow-x-hidden outline-none"
      >
        <Show
          when={count() > 0}
          fallback={
            <div class="grid h-full place-items-center text-sm text-[var(--text-muted)]">
              {ko.grid.empty}
            </div>
          }
        >
          <div class="relative" style={{ height: `${count() * ROW_H}px` }}>
            <For each={indices()}>
              {(i) => {
                // 모든 파생값은 반응형으로 — For가 같은 인덱스의 DOM을 재사용해도
                // 뷰 전환으로 스냅샷이 바뀌면 즉시 갱신되도록 한다.
                const id = () => snapshot()?.id(i) ?? -1;
                const d = () => detailsCache[id()];
                const isSel = () => selected().has(id());
                const marked = () => markedIds().has(id());
                const isVideo = () => snapshot()?.kind(i) === 1;
                const w = () => snapshot()?.width(i) ?? 0;
                const h = () => snapshot()?.height(i) ?? 0;
                const version = () => thumbVersions[id()] ?? 0;
                const showImg = () => {
                  const s = snapshot();
                  return !!s && (s.hasThumb(i) || version() > 0);
                };
                const rowTags = () => tagCache[id()] ?? [];
                return (
                  <div
                    class="list-row absolute inset-x-0 grid cursor-pointer items-center gap-2 px-2 text-xs"
                    classList={{ "is-sel": isSel() }}
                    style={{
                      transform: `translateY(${i * ROW_H}px)`,
                      height: `${ROW_H}px`,
                      "grid-template-columns": COLS,
                    }}
                    onClick={(e) => {
                      const s = snapshot();
                      if (s) onRowClick(e, s, i);
                    }}
                    onDblClick={() => openLightbox(i)}
                    onContextMenu={(e) => {
                      const s = snapshot();
                      if (s) onRowContextMenu(e, s, i);
                    }}
                    draggable={true}
                    onDragStart={(e) => {
                      const s = snapshot();
                      if (s) onRowDragStart(e, s, i);
                    }}
                  >
                    {/* 썸네일 — 미디어 면이 아니라 28×36 크롬 슬롯이다.
                        --grid-cell-bg(고정 다크)를 쓰면 흰 카드 안에 검은 사각형이 박힌다. */}
                    <div class="h-7 w-9 shrink-0 overflow-hidden rounded-[var(--r-xs)] bg-[var(--bg-panel-raised)]">
                      <Show when={showImg() && info()}>
                        <img
                          src={`${thumbUrl(info()!, id())}&v=${version()}`}
                          class="h-full w-full object-cover"
                          decoding="async"
                          draggable={false}
                          onError={(e) => {
                            (e.currentTarget as HTMLImageElement).style.display = "none";
                          }}
                        />
                      </Show>
                    </div>

                    {/* 이름 (+ 삭제 예정 배지) */}
                    <div class="flex min-w-0 items-center gap-1.5">
                      <Show when={marked()}>
                        <span class="chip chip--danger chip--xs shrink-0" title={ko.trash.badge}>
                          <IconTrash size={11} />
                        </span>
                      </Show>
                      <span
                        class="truncate text-[var(--text-primary)]"
                        title={d() ? `${d()!.dir}\\${d()!.name}` : undefined}
                      >
                        {d()?.name ?? "…"}
                      </span>
                    </div>

                    {/* 태그 */}
                    <div class="flex min-w-0 items-center gap-1 overflow-hidden">
                      <For each={rowTags().slice(0, 3)}>
                        {(name) => (
                          <span class="chip chip--accent chip--xs min-w-0" title={name}>
                            <span>{name}</span>
                          </span>
                        )}
                      </For>
                      <Show when={rowTags().length > 3}>
                        <span class="shrink-0 text-[10px] text-[var(--text-muted)]">
                          +{rowTags().length - 3}
                        </span>
                      </Show>
                    </div>

                    <span class="truncate text-[var(--text-muted)]">
                      {isVideo() ? ko.filter.video : ko.filter.image}
                    </span>
                    <span class="text-right tabular-nums text-[var(--text-secondary)]">
                      {d() ? fmtSize(d()!.size) : ""}
                    </span>
                    {/* 좁아지면 0폭까지 줄어드는 열 — 넘친 글자가 옆 열을 침범하지 않게 자른다 */}
                    <span class="truncate text-right tabular-nums text-[var(--text-muted)]">
                      {w() > 0 && h() > 0 ? `${w()}×${h()}` : "—"}
                    </span>
                    <span class="truncate text-right tabular-nums text-[var(--text-muted)]">
                      {fmtDuration(d()?.durationMs)}
                    </span>
                    <span class="text-right tabular-nums text-[var(--text-muted)]">
                      {fmtDate(dateOf(d()))}
                    </span>
                  </div>
                );
              }}
            </For>
          </div>
        </Show>
      </div>
    </div>
  );
};
