"use client";

import { useEffect, useOptimistic, useRef, useState, useTransition } from "react";
import type { Acc } from "@/lib/types";
import {
  deleteAccount,
  loadMoreAccounts,
  refreshAccounts,
  updateAccount,
} from "@/app/actions";
import EditableCell from "./editable-cell";
import ConfirmDialog from "./confirm-dialog";
import CreateAccountDialog from "./create-account-dialog";
import Toast, { type ToastMessage } from "./toast";
import { copyToClipboard } from "@/lib/clipboard";

type Props = {
  initial: Acc[];
  initialHasMore: boolean;
  initialTotal: number;
  prefixPattern: string;
};
type SortDir = "asc" | "desc";
type SortKey = "username" | "status";
type OptimisticPatch = { username: string; field: keyof Acc; value: string };

function StatusBadge({ status }: { status: string }) {
  const map: Record<string, { bg: string; fg: string; label: string }> = {
    "": { bg: "bg-zinc-100", fg: "text-zinc-600", label: "available" },
    wait: { bg: "bg-amber-100", fg: "text-amber-700", label: "wait" },
    done: { bg: "bg-emerald-100", fg: "text-emerald-700", label: "done" },
  };
  const v = map[status] ?? { bg: "bg-zinc-100", fg: "text-zinc-600", label: status || "—" };
  return (
    <span
      className={`inline-flex items-center rounded-full ${v.bg} ${v.fg} px-2 py-0.5 text-[11px] font-medium`}
    >
      {v.label}
    </span>
  );
}

function DeleteButton({
  label,
  onClick,
}: {
  label: string;
  onClick: () => void;
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      aria-label={`Delete ${label}`}
      className="inline-flex h-7 w-7 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-red-50 hover:text-red-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400"
    >
      <span aria-hidden="true" className="text-base leading-none">
        ×
      </span>
    </button>
  );
}

function CopyButton({
  label,
  onClick,
}: {
  label: string;
  onClick: () => void;
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      aria-label={`Copy credentials for ${label}`}
      title="Copy username and password"
      className="inline-flex h-7 w-7 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400"
    >
      <svg
        aria-hidden="true"
        viewBox="0 0 16 16"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.5"
        strokeLinecap="round"
        strokeLinejoin="round"
        className="h-4 w-4"
      >
        <rect x="5.5" y="5.5" width="8" height="8" rx="1.25" />
        <path d="M3 10.5V3.5A1 1 0 0 1 4 2.5h7" />
      </svg>
    </button>
  );
}

export default function AccountsTable({
  initial,
  initialHasMore,
  initialTotal,
  prefixPattern,
}: Props) {
  const [sortKey, setSortKey] = useState<SortKey>("username");
  const [sortDir, setSortDir] = useState<SortDir>("desc");
  const [editingKey, setEditingKey] = useState<string | null>(null);
  const [, startTransition] = useTransition();
  const [loadingMore, setLoadingMore] = useState(false);
  const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
  const [deleting, setDeleting] = useState(false);
  const [deleteError, setDeleteError] = useState<string | null>(null);
  const [createOpen, setCreateOpen] = useState(false);
  const [toast, setToast] = useState<ToastMessage | null>(null);

  // iOS Safari (and sometimes Chrome on Android) keeps the document
  // scroll position across SPA route changes, so tab-switching from
  // /users back to / leaves the user halfway down the page. Force
  // scroll-to-top on mount; route transitions remount the table.
  useEffect(() => {
    window.scrollTo({ top: 0, left: 0, behavior: "instant" });
  }, []);

  // Accumulated rows from initial server-side fetch + every loadMore.
  const [rows, setRows] = useState<Acc[]>(initial);
  const [hasMore, setHasMore] = useState<boolean>(initialHasMore);
  // Total row count in the DB; updated on every page fetch so it stays
  // fresh as external writes (the cm99 monitor) add rows. Mutations
  // adjust it locally so the header doesn't flash a stale count between
  // the optimistic update and the next page-fresh count.
  const [total, setTotal] = useState<number>(initialTotal);
  const sentinelRef = useRef<HTMLDivElement | null>(null);

  // `searchInput` is what the user is typing; `appliedQuery` is what
  // the server actually filtered on. Decoupled so typing doesn't fire a
  // request per keystroke — only on Enter / Find / Clear. `appliedQuery`
  // flows into refresh / changeSort / loadMore so sort + infinite-scroll
  // respect the active search.
  const [searchInput, setSearchInput] = useState("");
  const [appliedQuery, setAppliedQuery] = useState("");
  const [searching, setSearching] = useState(false);

  const [optimistic, applyOptimistic] = useOptimistic<Acc[], OptimisticPatch>(
    rows,
    (state, patch) =>
      state.map((row) =>
        row.username === patch.username ? { ...row, [patch.field]: patch.value } : row,
      ),
  );

  function saveCell(username: string, field: keyof Acc, value: string) {
    return new Promise<{ ok: boolean; error?: string }>((resolve) => {
      startTransition(async () => {
        applyOptimistic({ username, field, value });
        const row = rows.find((r) => r.username === username);
        if (!row) return resolve({ ok: false, error: "row not found" });
        const next: Acc = { ...row, [field]: value };
        const result = await updateAccount(next);
        if (result.ok) {
          setRows((prev) => prev.map((r) => (r.username === username ? next : r)));
          resolve({ ok: true });
        } else {
          resolve({ ok: false, error: result.error });
        }
      });
    });
  }

  // Force-evict the cache and re-fetch page 1. Used by the create-success
  // path so a freshly added row appears in its sorted position. Keeping
  // the function (instead of inlining at the call site) means a future
  // 'pull to refresh' gesture has a single hook.
  async function refresh() {
    const page = await refreshAccounts({
      prefix: prefixPattern,
      sort: sortKey,
      dir: sortDir,
      q: appliedQuery,
    });
    setRows(page.rows);
    setHasMore(page.hasMore);
    setTotal(page.total);
  }

  async function applySearch(nextQuery: string) {
    setSearching(true);
    setAppliedQuery(nextQuery);
    try {
      const page = await refreshAccounts({
        prefix: prefixPattern,
        sort: sortKey,
        dir: sortDir,
        q: nextQuery,
      });
      setRows(page.rows);
      setHasMore(page.hasMore);
      setTotal(page.total);
    } finally {
      setSearching(false);
    }
  }

  async function changeSort(nextKey: SortKey) {
    let nextDir: SortDir;
    if (nextKey === sortKey) {
      nextDir = sortDir === "asc" ? "desc" : "asc";
    } else {
      nextDir = "desc";
    }
    setSortKey(nextKey);
    setSortDir(nextDir);
    setLoadingMore(true);
    try {
      const page = await loadMoreAccounts({
        offset: 0,
        prefix: prefixPattern,
        sort: nextKey,
        dir: nextDir,
        q: appliedQuery,
      });
      setRows(page.rows);
      setHasMore(page.hasMore);
      setTotal(page.total);
    } finally {
      setLoadingMore(false);
    }
  }

  // Infinite scroll: when sentinel enters viewport, fetch the next page.
  // 300px rootMargin so the next page starts loading before the user
  // hits the bottom — feels seamless when scrolling fast.
  useEffect(() => {
    if (!hasMore || loadingMore) return;
    const sentinel = sentinelRef.current;
    if (!sentinel) return;

    const observer = new IntersectionObserver(
      (entries) => {
        if (!entries.some((e) => e.isIntersecting)) return;
        setLoadingMore(true);
        loadMoreAccounts({
          offset: rows.length,
          prefix: prefixPattern,
          sort: sortKey,
          dir: sortDir,
          q: appliedQuery,
        })
          .then((page) => {
            setRows((prev) => [...prev, ...page.rows]);
            setHasMore(page.hasMore);
            setTotal(page.total);
          })
          .catch((err) => console.error("loadMoreAccounts failed:", err))
          .finally(() => setLoadingMore(false));
      },
      { rootMargin: "300px 0px" },
    );
    observer.observe(sentinel);
    return () => observer.disconnect();
  }, [hasMore, loadingMore, rows.length, prefixPattern, sortKey, sortDir, appliedQuery]);

  async function handleCopy(row: Acc) {
    // Build the message line-by-line so an empty `link` (which the
    // monitor sometimes leaves blank for new accounts) drops out
    // entirely instead of producing a dangling "Link:" with nothing
    // after it.
    const lines = [
      `Username: ${row.username}`,
      `Password: ${row.password}`,
    ];
    if (row.link) lines.push(`Link: ${row.link}`);
    const text = lines.join("\n");
    const ok = await copyToClipboard(text);
    setToast(
      ok
        ? { type: "success", message: `Copied credentials for ${row.username}` }
        : {
            type: "error",
            message: `Could not copy — clipboard access blocked. Select text manually.`,
          },
    );
  }

  async function confirmDelete() {
    if (!deleteTarget) return;
    setDeleting(true);
    setDeleteError(null);
    const result = await deleteAccount(deleteTarget);
    setDeleting(false);
    if (result.ok) {
      const deleted = deleteTarget;
      setRows((prev) => prev.filter((r) => r.username !== deleted));
      setTotal((t) => Math.max(0, t - 1));
      setDeleteTarget(null);
      setToast({ type: "success", message: `Account ${deleted} deleted` });
    } else {
      setDeleteError(result.error);
    }
  }

  const onSearchSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    void applySearch(searchInput.trim());
  };
  const onSearchClear = () => {
    setSearchInput("");
    void applySearch("");
  };

  function HeaderTh({ k, label }: { k: SortKey; label: string }) {
    const active = sortKey === k;
    return (
      <button
        type="button"
        onClick={() => changeSort(k)}
        className={`inline-flex items-center gap-1 text-[11px] font-medium uppercase tracking-wider transition-colors ${
          active ? "text-zinc-900" : "text-zinc-500 hover:text-zinc-900"
        }`}
      >
        {label}
        <span aria-hidden="true">{active && sortDir === "asc" ? "↑" : "↓"}</span>
      </button>
    );
  }

  if (rows.length === 0 && !appliedQuery) {
    return (
      <div>
        <PageHead
          total={total}
          loaded={0}
          onAdd={() => setCreateOpen(true)}
        />
        <SearchBar
          value={searchInput}
          onChange={setSearchInput}
          onSubmit={onSearchSubmit}
          onClear={onSearchClear}
          appliedQuery={appliedQuery}
          searching={searching}
        />
        <div className="mt-6 rounded-2xl bg-white px-8 py-16 text-center ring-1 ring-zinc-200/60">
          <p className="text-sm text-zinc-500">
            No accounts yet. Click <span className="font-medium text-zinc-700">Add</span> above to create one manually, or wait for the monitor.
          </p>
        </div>
        <CreateAccountDialog
          open={createOpen}
          onClose={() => setCreateOpen(false)}
          onSuccess={async (name) => {
            setToast({ type: "success", message: `Account ${name} created` });
            // Force-refresh from page 1 so the new row appears in its
            // sorted position. (We don't know where it ranks otherwise.)
            await refresh();
          }}
          prefixPattern={prefixPattern}
        />
      </div>
    );
  }

  return (
    <div>
      <PageHead
        total={total}
        loaded={optimistic.length}
        onAdd={() => setCreateOpen(true)}
      />
      <SearchBar
        value={searchInput}
        onChange={setSearchInput}
        onSubmit={onSearchSubmit}
        onClear={onSearchClear}
        appliedQuery={appliedQuery}
        searching={searching}
      />
      {rows.length === 0 && appliedQuery && (
        <div className="mt-6 rounded-2xl bg-white px-8 py-16 text-center ring-1 ring-zinc-200/60">
          <p className="text-sm text-zinc-500">
            No accounts match <span className="font-medium text-zinc-700">{appliedQuery}</span>.
            {" "}
            <button
              type="button"
              onClick={onSearchClear}
              className="font-medium text-zinc-700 underline-offset-2 hover:underline"
            >
              Clear search
            </button>
            .
          </p>
        </div>
      )}

      {rows.length > 0 && (
      <>
      {/* Desktop / tablet table */}
      <div className="mt-6 hidden overflow-hidden rounded-2xl bg-white ring-1 ring-zinc-200/60 sm:block">
        <table className="w-full table-fixed border-collapse">
          <thead>
            <tr className="bg-zinc-50/60">
              <th className="w-[18%] px-5 py-3 text-left">
                <HeaderTh k="username" label="Username" />
              </th>
              <Th>Password</Th>
              <th className="w-[16%] px-5 py-3 text-left">
                <HeaderTh k="status" label="Status" />
              </th>
              <Th>Link</Th>
              <th className="w-20 px-3 py-3" aria-hidden="true" />
            </tr>
          </thead>
          <tbody className="divide-y divide-zinc-100">
            {optimistic.map((row) => {
              const k = (f: string) => `${row.username}::${f}`;
              return (
                <tr key={row.username} className="transition-colors hover:bg-zinc-50/60">
                  <td className="px-5 py-3 align-middle font-mono text-[13px] font-semibold text-zinc-900">
                    {row.username}
                  </td>
                  <td className="px-5 py-3 align-middle">
                    <EditableCell
                      value={row.password}
                      label={`password for ${row.username}`}
                      isCurrentlyEditing={editingKey === k("password")}
                      onEditStart={() => setEditingKey(k("password"))}
                      onEditEnd={() => setEditingKey(null)}
                      onSave={(v) => saveCell(row.username, "password", v)}
                    />
                  </td>
                  <td className="px-5 py-3 align-middle">
                    <EditableCell
                      value={row.status}
                      label={`status for ${row.username}`}
                      isCurrentlyEditing={editingKey === k("status")}
                      onEditStart={() => setEditingKey(k("status"))}
                      onEditEnd={() => setEditingKey(null)}
                      onSave={(v) => saveCell(row.username, "status", v)}
                      renderView={(v) => <StatusBadge status={v} />}
                    />
                  </td>
                  <td className="px-5 py-3 align-middle">
                    <EditableCell
                      value={row.link}
                      label={`link for ${row.username}`}
                      isCurrentlyEditing={editingKey === k("link")}
                      onEditStart={() => setEditingKey(k("link"))}
                      onEditEnd={() => setEditingKey(null)}
                      onSave={(v) => saveCell(row.username, "link", v)}
                    />
                  </td>
                  <td className="px-3 py-3 text-right align-middle">
                    <div className="inline-flex items-center gap-1">
                      <CopyButton
                        label={row.username}
                        onClick={() => handleCopy(row)}
                      />
                      <DeleteButton
                        label={row.username}
                        onClick={() => {
                          setDeleteError(null);
                          setDeleteTarget(row.username);
                        }}
                      />
                    </div>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>

      {/* Mobile cards */}
      <div className="mt-6 space-y-3 sm:hidden">
        {optimistic.map((row) => {
          const k = (f: string) => `${row.username}::${f}`;
          return (
            <div key={row.username} className="rounded-2xl bg-white p-5 ring-1 ring-zinc-200/60">
              <div className="flex items-center justify-between gap-3">
                <div className="flex min-w-0 items-center gap-3">
                  <span className="shrink-0 font-mono text-base font-semibold text-zinc-900">
                    {row.username}
                  </span>
                  <div className="min-w-0 flex-1">
                    <EditableCell
                      value={row.status}
                      label={`status for ${row.username}`}
                      isCurrentlyEditing={editingKey === k("status")}
                      onEditStart={() => setEditingKey(k("status"))}
                      onEditEnd={() => setEditingKey(null)}
                      onSave={(v) => saveCell(row.username, "status", v)}
                      renderView={(v) => <StatusBadge status={v} />}
                    />
                  </div>
                </div>
                <div className="flex items-center gap-1">
                  <CopyButton
                    label={row.username}
                    onClick={() => handleCopy(row)}
                  />
                  <DeleteButton
                    label={row.username}
                    onClick={() => {
                      setDeleteError(null);
                      setDeleteTarget(row.username);
                    }}
                  />
                </div>
              </div>
              <dl className="mt-4 space-y-3 border-t border-zinc-100 pt-4">
                <CardRow label="Password">
                  <EditableCell
                    value={row.password}
                    label={`password for ${row.username}`}
                    isCurrentlyEditing={editingKey === k("password")}
                    onEditStart={() => setEditingKey(k("password"))}
                    onEditEnd={() => setEditingKey(null)}
                    onSave={(v) => saveCell(row.username, "password", v)}
                  />
                </CardRow>
                <CardRow label="Link">
                  <EditableCell
                    value={row.link}
                    label={`link for ${row.username}`}
                    isCurrentlyEditing={editingKey === k("link")}
                    onEditStart={() => setEditingKey(k("link"))}
                    onEditEnd={() => setEditingKey(null)}
                    onSave={(v) => saveCell(row.username, "link", v)}
                  />
                </CardRow>
              </dl>
            </div>
          );
        })}
      </div>

      {/* Sentinel for infinite scroll. Hidden visually unless we're at
          the bottom; the IntersectionObserver triggers loadMore as it
          comes into view. */}
      <div ref={sentinelRef} className="h-10" aria-hidden="true" />
      {loadingMore && (
        <p className="mt-4 text-center text-[11px] font-medium uppercase tracking-wider text-zinc-400">
          Loading more…
        </p>
      )}
      {!hasMore && rows.length > 0 && (
        <p className="mt-6 text-center text-[11px] text-zinc-400">
          End of list — {rows.length} accounts loaded
        </p>
      )}
      </>
      )}

      <CreateAccountDialog
        open={createOpen}
        onClose={() => setCreateOpen(false)}
        onSuccess={async (name) => {
          setToast({ type: "success", message: `Account ${name} created` });
          await refresh();
        }}
        prefixPattern={prefixPattern}
      />

      <Toast toast={toast} onDismiss={() => setToast(null)} />

      <ConfirmDialog
        open={!!deleteTarget}
        onCancel={() => {
          if (!deleting) setDeleteTarget(null);
        }}
        onConfirm={confirmDelete}
        title={`Delete ${deleteTarget ?? ""}?`}
        message={
          <>
            <p>
              Permanently remove{" "}
              <code className="rounded bg-zinc-100 px-1.5 py-0.5 font-mono text-xs text-zinc-700">
                {deleteTarget}
              </code>{" "}
              from the accounts table. This action cannot be undone.
            </p>
            {deleteError && (
              <p className="mt-3 font-mono text-[11px] text-red-600" role="alert">
                {deleteError}
              </p>
            )}
          </>
        }
        confirmLabel="Delete"
        destructive
        pending={deleting}
      />
    </div>
  );
}

function SearchBar({
  value,
  onChange,
  onSubmit,
  onClear,
  appliedQuery,
  searching,
}: {
  value: string;
  onChange: (v: string) => void;
  onSubmit: (e: React.FormEvent) => void;
  onClear: () => void;
  appliedQuery: string;
  searching: boolean;
}) {
  return (
    <form onSubmit={onSubmit} role="search" className="mt-4 flex flex-wrap items-center gap-2">
      <label className="sr-only" htmlFor="accounts-search">Search by username</label>
      <input
        id="accounts-search"
        type="search"
        autoComplete="off"
        spellCheck={false}
        placeholder="Search username (e.g. 13c4511)"
        value={value}
        onChange={(e) => onChange(e.target.value)}
        className="min-w-0 flex-1 rounded-full bg-white px-4 py-1.5 text-sm text-zinc-900 ring-1 ring-zinc-200/60 placeholder:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-zinc-900"
      />
      <button
        type="submit"
        disabled={searching}
        className="inline-flex items-center rounded-full bg-zinc-900 px-3 py-1.5 text-xs font-medium text-white shadow-sm transition-colors hover:bg-zinc-700 disabled:cursor-not-allowed disabled:opacity-50"
      >
        {searching ? "Finding…" : "Find"}
      </button>
      {appliedQuery && (
        <button
          type="button"
          onClick={onClear}
          disabled={searching}
          className="inline-flex items-center rounded-full bg-white px-3 py-1.5 text-xs font-medium text-zinc-700 ring-1 ring-zinc-200/60 transition-colors hover:bg-zinc-50 disabled:cursor-not-allowed disabled:opacity-50"
        >
          Clear
        </button>
      )}
      {appliedQuery && (
        <span className="text-[11px] text-zinc-500">
          Filtered by <span className="font-mono text-zinc-700">{appliedQuery}</span>
        </span>
      )}
    </form>
  );
}

function Th({ children, className = "" }: { children: React.ReactNode; className?: string }) {
  return (
    <th
      className={`px-5 py-3 text-left text-[11px] font-medium uppercase tracking-wider text-zinc-500 ${className}`}
    >
      {children}
    </th>
  );
}

function CardRow({ label, children }: { label: string; children: React.ReactNode }) {
  return (
    <div className="grid grid-cols-[max-content_1fr] items-baseline gap-x-4">
      <dt className="text-[11px] font-medium uppercase tracking-wider text-zinc-500">
        {label}
      </dt>
      <dd className="min-w-0">{children}</dd>
    </div>
  );
}

function PageHead({
  total,
  loaded,
  onAdd,
}: {
  total: number;
  loaded: number;
  onAdd: () => void;
}) {
  // total = COUNT(*) from the API; loaded = how many rows the user has
  // scrolled in so far. Show the partial count only while it differs
  // from the total, so a fully-scrolled list reads cleanly.
  const showLoaded = loaded > 0 && loaded < total;
  return (
    <div>
      <p className="text-[11px] font-medium uppercase tracking-wider text-zinc-500">
        Table
      </p>
      <div className="mt-1 flex flex-wrap items-center justify-between gap-x-3 gap-y-2">
        <h1 className="text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl">
          Accounts
          <span className="ml-2 align-middle text-base font-medium text-zinc-400">
            {total}
          </span>
        </h1>
        <button
          type="button"
          onClick={onAdd}
          aria-label="Add account"
          className="inline-flex items-center gap-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-xs font-medium text-white shadow-sm transition-colors hover:bg-zinc-700"
        >
          <span aria-hidden="true">+</span>
          Add
        </button>
      </div>
      {showLoaded && (
        <p className="mt-1 text-[11px] text-zinc-400">
          Showing {loaded} of {total}
        </p>
      )}
    </div>
  );
}
