<script lang="ts">
import { type ScrapeJob, client } from "$lib/api";
import { FlexRender, createSvelteTable } from "$lib/components/ui/data-table/index.js";
import { formatAbsoluteDate } from "$lib/date";
import { formatDuration } from "$lib/time";
import { ArrowDown, ArrowUp, ArrowUpDown, TriangleAlert } from "@lucide/svelte";
import {
  type ColumnDef,
  type SortingState,
  type Updater,
  getCoreRowModel,
  getSortedRowModel,
} from "@tanstack/table-core";
import { onMount } from "svelte";
import { type ConnectionState, ScrapeJobsStore } from "$lib/ws";

let jobs = $state<ScrapeJob[]>([]);
let connectionState = $state<ConnectionState>("disconnected");
let initialized = $state(false);
let error = $state<string | null>(null);
let sorting: SortingState = $state([]);
let tick = $state(0);
let subjectMap = $state(new Map<string, string>());

let store: ScrapeJobsStore | undefined;

// Shared tooltip state — single tooltip for all timing cells via event delegation
let tooltipText = $state<string | null>(null);
let tooltipX = $state(0);
let tooltipY = $state(0);

function showTooltip(event: MouseEvent) {
  const target = (event.target as HTMLElement).closest<HTMLElement>("[data-timing-tooltip]");
  if (!target) return;
  tooltipText = target.dataset.timingTooltip ?? null;
  tooltipX = event.clientX;
  tooltipY = event.clientY;
}

function moveTooltip(event: MouseEvent) {
  if (tooltipText === null) return;
  const target = (event.target as HTMLElement).closest<HTMLElement>("[data-timing-tooltip]");
  if (!target) {
    tooltipText = null;
    return;
  }
  tooltipText = target.dataset.timingTooltip ?? null;
  tooltipX = event.clientX;
  tooltipY = event.clientY;
}

function hideTooltip() {
  tooltipText = null;
}

onMount(() => {
  // Tick every second for live time displays
  const tickInterval = setInterval(() => {
    tick++;
  }, 1000);

  // Load subject reference data
  client
    .getReference("subject")
    .then((entries) => {
      const map = new Map<string, string>();
      for (const entry of entries) {
        map.set(entry.code, entry.description);
      }
      subjectMap = map;
    })
    .catch(() => {
      // Subject lookup is best-effort
    });

  // Initialize WebSocket store
  store = new ScrapeJobsStore(() => {
    if (!store) return;
    connectionState = store.getConnectionState();
    initialized = store.isInitialized();
    // getJobs() returns a cached array when unchanged, so only reassign
    // when the reference differs to avoid triggering reactive table rebuilds.
    const next = store.getJobs();
    if (next !== jobs) jobs = next;
  });
  store.connect();

  return () => {
    clearInterval(tickInterval);
    store?.disconnect();
  };
});

function handleSortingChange(updater: Updater<SortingState>) {
  sorting = typeof updater === "function" ? updater(sorting) : updater;
}

// --- Helper functions ---

function formatJobDetails(job: ScrapeJob, subjects: Map<string, string>): string {
  const payload = job.targetPayload as Record<string, unknown>;
  switch (job.targetType) {
    case "Subject": {
      const code = payload.subject as string;
      const desc = subjects.get(code);
      return desc ? `${code} \u2014 ${desc}` : code;
    }
    case "CrnList": {
      const crns = payload.crns as string[];
      return `${crns.length} CRNs`;
    }
    case "SingleCrn":
      return `CRN ${payload.crn as string}`;
    case "CourseRange":
      return `${payload.subject as string} ${payload.low as number}\u2013${payload.high as number}`;
    default:
      return JSON.stringify(payload);
  }
}

function priorityColor(priority: string): string {
  const p = priority.toLowerCase();
  if (p === "urgent" || p === "critical") return "text-red-500";
  if (p === "high") return "text-orange-500";
  if (p === "low") return "text-muted-foreground";
  return "text-foreground";
}

function retryColor(retryCount: number, maxRetries: number): string {
  if (retryCount >= maxRetries && maxRetries > 0) return "text-red-500";
  if (retryCount > 0) return "text-amber-500";
  return "text-muted-foreground";
}

function statusColor(status: string): { text: string; dot: string } {
  switch (status) {
    case "processing":
      return { text: "text-blue-500", dot: "bg-blue-500" };
    case "pending":
      return { text: "text-green-500", dot: "bg-green-500" };
    case "scheduled":
      return { text: "text-muted-foreground", dot: "bg-muted-foreground" };
    case "staleLock":
      return { text: "text-red-500", dot: "bg-red-500" };
    case "exhausted":
      return { text: "text-red-500", dot: "bg-red-500" };
    default:
      return { text: "text-muted-foreground", dot: "bg-muted-foreground" };
  }
}

function formatStatusLabel(status: string): string {
  // Convert camelCase to separate words, capitalize first letter
  return status.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^\w/, (c) => c.toUpperCase());
}

function lockDurationColor(ms: number): string {
  const minutes = ms / 60_000;
  if (minutes >= 8) return "text-red-500";
  if (minutes >= 5) return "text-amber-500";
  return "text-foreground";
}

function overdueDurationColor(ms: number): string {
  const minutes = ms / 60_000;
  if (minutes >= 5) return "text-red-500";
  return "text-amber-500";
}

// --- Table columns ---

const columns: ColumnDef<ScrapeJob, unknown>[] = [
  {
    id: "id",
    accessorKey: "id",
    header: "ID",
    enableSorting: false,
  },
  {
    id: "status",
    accessorKey: "status",
    header: "Status",
    enableSorting: true,
    sortingFn: (rowA, rowB) => {
      const order: Record<string, number> = {
        processing: 0,
        staleLock: 1,
        pending: 2,
        scheduled: 3,
        exhausted: 4,
      };
      const a = order[rowA.original.status] ?? 3;
      const b = order[rowB.original.status] ?? 3;
      return a - b;
    },
  },
  {
    id: "targetType",
    accessorKey: "targetType",
    header: "Type",
    enableSorting: false,
  },
  {
    id: "details",
    accessorFn: () => "",
    header: "Details",
    enableSorting: false,
  },
  {
    id: "priority",
    accessorKey: "priority",
    header: "Priority",
    enableSorting: true,
    sortingFn: (rowA, rowB) => {
      const order: Record<string, number> = {
        critical: 0,
        urgent: 0,
        high: 1,
        normal: 2,
        medium: 2,
        low: 3,
      };
      const a = order[String(rowA.original.priority).toLowerCase()] ?? 2;
      const b = order[String(rowB.original.priority).toLowerCase()] ?? 2;
      return a - b;
    },
  },
  {
    id: "timing",
    accessorFn: (row) => {
      if (row.lockedAt) return Date.now() - new Date(row.lockedAt).getTime();
      return Date.now() - new Date(row.queuedAt).getTime();
    },
    header: "Timing",
    enableSorting: true,
  },
];

const table = createSvelteTable({
  get data() {
    return jobs;
  },
  getRowId: (row) => String(row.id),
  columns,
  state: {
    get sorting() {
      return sorting;
    },
  },
  onSortingChange: handleSortingChange,
  getCoreRowModel: getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(),
  enableSortingRemoval: true,
});

const skeletonWidths: Record<string, string> = {
  id: "w-6",
  status: "w-20",
  targetType: "w-16",
  details: "w-32",
  priority: "w-16",
  timing: "w-32",
};

// Unified timing display: shows the most relevant duration for the job's current state.
// Uses _tick dependency so Svelte re-evaluates every second.
function getTimingDisplay(
  job: ScrapeJob,
  _tick: number
): { text: string; colorClass: string; icon: "warning" | "none"; tooltip: string } {
  const now = Date.now();
  const queuedTime = new Date(job.queuedAt).getTime();
  const executeTime = new Date(job.executeAt).getTime();

  if (job.status === "processing" || job.status === "staleLock") {
    const lockedTime = job.lockedAt ? new Date(job.lockedAt).getTime() : now;
    const processingMs = now - lockedTime;
    const waitedMs = lockedTime - queuedTime;

    const prefix = job.status === "staleLock" ? "stale" : "processing";
    const colorClass =
      job.status === "staleLock" ? "text-red-500" : lockDurationColor(processingMs);

    const tooltipLines = [
      `Queued: ${formatAbsoluteDate(job.queuedAt)}`,
      `Waited: ${formatDuration(Math.max(0, waitedMs))}`,
      `Locked: ${formatAbsoluteDate(job.lockedAt)}`,
      `${job.status === "staleLock" ? "Stale for" : "Processing"}: ${formatDuration(processingMs)}`,
    ];

    return {
      text: `${prefix} ${formatDuration(processingMs)}`,
      colorClass,
      icon: job.status === "staleLock" ? "warning" : "none",
      tooltip: tooltipLines.join("\n"),
    };
  }

  if (job.status === "exhausted") {
    const tooltipLines = [
      `Queued: ${formatAbsoluteDate(job.queuedAt)}`,
      `Retries: ${job.retryCount}/${job.maxRetries} exhausted`,
    ];
    return {
      text: "exhausted",
      colorClass: "text-red-500",
      icon: "warning",
      tooltip: tooltipLines.join("\n"),
    };
  }

  // Scheduled (future execute_at)
  const executeAtDiff = now - executeTime;
  if (job.status === "scheduled" || executeAtDiff < 0) {
    const tooltipLines = [
      `Queued: ${formatAbsoluteDate(job.queuedAt)}`,
      `Executes: ${formatAbsoluteDate(job.executeAt)}`,
    ];
    return {
      text: `in ${formatDuration(Math.abs(executeAtDiff))}`,
      colorClass: "text-muted-foreground",
      icon: "none",
      tooltip: tooltipLines.join("\n"),
    };
  }

  // Pending (overdue — execute_at is in the past, waiting to be picked up)
  const waitingMs = now - queuedTime;
  const tooltipLines = [
    `Queued: ${formatAbsoluteDate(job.queuedAt)}`,
    `Waiting: ${formatDuration(waitingMs)}`,
  ];
  return {
    text: `waiting ${formatDuration(waitingMs)}`,
    colorClass: overdueDurationColor(waitingMs),
    icon: "warning",
    tooltip: tooltipLines.join("\n"),
  };
}
</script>

<div class="flex items-center justify-between mb-4">
  <h1 class="text-lg font-semibold text-foreground">Scrape Jobs</h1>
  <div class="flex items-center gap-2 text-sm">
    {#if connectionState === "connected"}
      <span class="inline-flex items-center gap-1.5">
        <span class="size-2 shrink-0 rounded-full bg-green-500"></span>
        <span class="text-green-500">Live</span>
      </span>
    {:else if connectionState === "reconnecting"}
      <span class="inline-flex items-center gap-1.5">
        <span class="size-2 shrink-0 rounded-full bg-amber-500"></span>
        <span class="text-amber-500">Reconnecting...</span>
      </span>
    {:else}
      <span class="inline-flex items-center gap-2">
        <span class="inline-flex items-center gap-1.5">
          <span class="size-2 shrink-0 rounded-full bg-red-500"></span>
          <span class="text-red-500">Disconnected</span>
        </span>
        <button
          class="rounded-md bg-muted px-2 py-0.5 text-xs font-medium text-foreground hover:bg-muted/80 transition-colors"
          onclick={() => store?.retry()}
        >
          Retry
        </button>
      </span>
    {/if}
  </div>
</div>

{#if error}
  <p class="text-destructive">{error}</p>
{:else}
  <div class="bg-card border-border overflow-hidden rounded-lg border">
    <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
    <table
      class="w-full border-collapse text-xs"
      onmouseenter={showTooltip}
      onmousemove={moveTooltip}
      onmouseleave={hideTooltip}
    >
      <thead>
        {#each table.getHeaderGroups() as headerGroup}
          <tr class="border-b border-border text-left text-muted-foreground">
            {#each headerGroup.headers as header}
              <th
                class="px-3 py-2.5 font-medium whitespace-nowrap"
                class:cursor-pointer={header.column.getCanSort()}
                class:select-none={header.column.getCanSort()}
                onclick={header.column.getToggleSortingHandler()}
              >
                {#if header.column.getCanSort()}
                  <span class="inline-flex items-center gap-1">
                    {#if typeof header.column.columnDef.header === "string"}
                      {header.column.columnDef.header}
                    {:else}
                      <FlexRender
                        content={header.column.columnDef.header}
                        context={header.getContext()}
                      />
                    {/if}
                    {#if header.column.getIsSorted() === "asc"}
                      <ArrowUp class="size-3.5" />
                    {:else if header.column.getIsSorted() === "desc"}
                      <ArrowDown class="size-3.5" />
                    {:else}
                      <ArrowUpDown class="size-3.5 text-muted-foreground/40" />
                    {/if}
                  </span>
                {:else if typeof header.column.columnDef.header === "string"}
                  {header.column.columnDef.header}
                {:else}
                  <FlexRender
                    content={header.column.columnDef.header}
                    context={header.getContext()}
                  />
                {/if}
              </th>
            {/each}
          </tr>
        {/each}
      </thead>
      {#if !initialized}
        <tbody>
          {#each Array(5) as _}
            <tr class="border-b border-border">
              {#each columns as col}
                <td class="px-3 py-2.5">
                  <div
                    class="h-3.5 rounded bg-muted animate-pulse {skeletonWidths[col.id ?? ''] ?? 'w-20'}"
                  ></div>
                </td>
              {/each}
            </tr>
          {/each}
        </tbody>
      {:else if jobs.length === 0}
        <tbody>
          <tr>
            <td colspan={columns.length} class="py-12 text-center text-muted-foreground">
              No scrape jobs found.
            </td>
          </tr>
        </tbody>
      {:else}
        <tbody>
          {#each table.getRowModel().rows as row (row.id)}
            {@const job = row.original}
            {@const sc = statusColor(job.status)}
            {@const timingDisplay = getTimingDisplay(job, tick)}
            <tr
              class="border-b border-border last:border-b-0 hover:bg-muted/50 transition-colors"
            >
              {#each row.getVisibleCells() as cell (cell.id)}
                {@const colId = cell.column.id}
                {#if colId === "id"}
                  <td class="px-3 py-2.5 tabular-nums text-muted-foreground/70 w-12">{job.id}</td>
                {:else if colId === "status"}
                  <td class="px-3 py-2.5 whitespace-nowrap">
                    <span class="inline-flex items-center gap-1.5">
                      <span class="size-1.5 shrink-0 rounded-full {sc.dot}"></span>
                      <span class="flex flex-col leading-tight">
                        <span class={sc.text}>{formatStatusLabel(job.status)}</span>
                        {#if job.maxRetries > 0}
                          <span class="text-[10px] {retryColor(job.retryCount, job.maxRetries)}">
                            {job.retryCount}/{job.maxRetries} retries
                          </span>
                        {/if}
                      </span>
                    </span>
                  </td>
                {:else if colId === "targetType"}
                  <td class="px-3 py-2.5 whitespace-nowrap">
                    <span
                      class="inline-flex items-center rounded-md bg-muted/60 px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground"
                    >
                      {job.targetType}
                    </span>
                  </td>
                {:else if colId === "details"}
                  <td class="px-3 py-2.5 max-w-48 truncate text-muted-foreground" title={formatJobDetails(job, subjectMap)}>
                    {formatJobDetails(job, subjectMap)}
                  </td>
                {:else if colId === "priority"}
                  <td class="px-3 py-2.5 whitespace-nowrap">
                    <span class="font-medium capitalize {priorityColor(job.priority)}">
                      {job.priority}
                    </span>
                  </td>
                {:else if colId === "timing"}
                  <td class="px-3 py-2.5 whitespace-nowrap">
                    <span
                      class="inline-flex items-center gap-1.5 tabular-nums text-foreground"
                      data-timing-tooltip={timingDisplay.tooltip}
                    >
                      <span class="size-3.5 shrink-0 inline-flex items-center justify-center {timingDisplay.colorClass}">
                        {#if timingDisplay.icon === "warning"}
                          <TriangleAlert class="size-3.5" />
                        {/if}
                      </span>
                      {timingDisplay.text}
                    </span>
                  </td>
                {/if}
              {/each}
            </tr>
          {/each}
        </tbody>
      {/if}
    </table>
  </div>

  {#if tooltipText !== null}
    <div
      class="pointer-events-none fixed z-50 bg-card text-card-foreground text-xs border border-border rounded-md px-2.5 py-1.5 shadow-sm whitespace-pre-line max-w-max text-left"
      style="left: {tooltipX + 12}px; top: {tooltipY + 12}px;"
    >
      {tooltipText}
    </div>
  {/if}
{/if}
