<script lang="ts">
  import { pageTitle } from '$lib/branding';
  import { Copy, Trash2, CheckCircle2 } from 'lucide-svelte';
  import { useQueryClient } from '@tanstack/svelte-query';
  import RowActionsMenu, { type RowAction } from '$lib/components/RowActionsMenu.svelte';
  import {
    createAdminPlaybackErrorsQuery,
    resolvePlaybackError,
    deleteQuarantineFile
  } from '$lib/api/admin';
  import { qk } from '$lib/api/queries';
  import { errMessage } from '$lib/api/errors';
  import { pushToast } from '$lib/stores/toast.svelte';
  import Modal from '$lib/components/Modal.svelte';
  import type { AdminPlaybackError, PlaybackErrorResolution } from '$lib/api/types';

  // Client-reported playback errors inbox. Two tabs — Unresolved
  // (default) / Resolved. Per-row: copy details to clipboard, delete
  // the source file (auto-resolves as 'deleted'), or mark resolved
  // manually with a fixed/ignored reason. Other resolutions (hidden,
  // requested) get stamped by their respective workflows in follow-up
  // slices.

  const client = useQueryClient();

  // Tab state — observed by the query factory so swapping tabs
  // re-fetches the corresponding list.
  let resolved = $state(false);

  const queryStore = $derived(createAdminPlaybackErrorsQuery(resolved));
  const query = $derived($queryStore);
  const rows = $derived((query.data ?? []) as AdminPlaybackError[]);

  function relativeTime(iso: string): string {
    const ms = Date.now() - new Date(iso).getTime();
    const days = Math.floor(ms / (24 * 3_600_000));
    if (days >= 1) return `${days}d ago`;
    const hours = Math.floor(ms / 3_600_000);
    if (hours >= 1) return `${hours}h ago`;
    const minutes = Math.floor(ms / 60_000);
    if (minutes >= 1) return `${minutes}m ago`;
    return 'just now';
  }

  // Maps the kind enum to a short readable badge label.
  function kindLabel(kind: string): string {
    switch (kind) {
      case 'zero_duration': return 'Zero duration';
      case 'load_failed':   return 'Load failed';
      case 'stalled':       return 'Stalled';
      default: return kind;
    }
  }

  // Maps the resolution enum to a short label for the Resolved tab.
  function resolutionLabel(r: string | null | undefined): string {
    if (!r) return '';
    switch (r) {
      case 'hidden':    return 'Hidden';
      case 'deleted':   return 'Deleted';
      case 'requested': return 'Re-requested';
      case 'fixed':     return 'Fixed';
      case 'ignored':   return 'Ignored';
      default: return r;
    }
  }

  async function invalidate() {
    await client.invalidateQueries({ queryKey: qk.adminPlaybackErrors(resolved) });
  }

  // Copy a single-row JSON payload to the clipboard. Includes the
  // server-side file_path so the operator can grep their library
  // mount without round-tripping back through the UI.
  async function onCopy(r: AdminPlaybackError) {
    const payload = {
      id: r.id,
      track_id: r.track_id,
      track_title: r.track_title,
      artist_name: r.artist_name,
      album_title: r.album_title,
      file_path: r.track_file_path,
      kind: r.kind,
      detail: r.detail,
      reported_by: r.username,
      client_id: r.client_id,
      occurred_at: r.occurred_at
    };
    try {
      await navigator.clipboard.writeText(JSON.stringify(payload, null, 2));
      pushToast('Copied error details to clipboard');
    } catch (e: unknown) {
      pushToast(`Copy failed: ${errMessage(e)}`, 'error');
    }
  }

  // Delete the source file via the existing quarantine admin endpoint;
  // on success stamp this row as resolved='deleted' so the inbox
  // reflects the action immediately.
  let deleteFileOpen = $state<AdminPlaybackError | null>(null);

  async function confirmDeleteFile() {
    const row = deleteFileOpen;
    if (!row) return;
    deleteFileOpen = null;
    try {
      await deleteQuarantineFile(row.track_id);
      await resolvePlaybackError(row.id, 'deleted');
      pushToast(`Deleted "${row.track_title}"`);
      await invalidate();
    } catch (e: unknown) {
      pushToast(`Delete failed: ${errMessage(e)}`, 'error');
    }
  }

  // Manual mark-resolved modal. Resolution dropdown limited to the two
  // "no further action taken" cases (fixed / ignored). Delete already
  // stamps 'deleted'; hide/request land in follow-up slices.
  let resolveOpen = $state<AdminPlaybackError | null>(null);
  let resolveChoice = $state<PlaybackErrorResolution>('fixed');

  function openResolve(row: AdminPlaybackError) {
    resolveOpen = row;
    resolveChoice = 'fixed';
  }

  async function confirmResolve() {
    const row = resolveOpen;
    if (!row) return;
    resolveOpen = null;
    try {
      await resolvePlaybackError(row.id, resolveChoice);
      pushToast(`Marked "${row.track_title}" ${resolutionLabel(resolveChoice).toLowerCase()}`);
      await invalidate();
    } catch (e: unknown) {
      pushToast(`Resolve failed: ${errMessage(e)}`, 'error');
    }
  }

  function actionsFor(r: AdminPlaybackError): { primary: RowAction; secondary: RowAction[] } {
    return {
      primary: {
        icon: CheckCircle2,
        label: 'Resolve',
        ariaLabel: `Resolve ${r.track_title}`,
        onclick: () => openResolve(r)
      },
      secondary: [
        {
          icon: Copy,
          label: 'Copy',
          ariaLabel: `Copy details for ${r.track_title}`,
          onclick: () => onCopy(r)
        },
        {
          icon: Trash2,
          label: 'Delete file',
          ariaLabel: `Delete file for ${r.track_title}`,
          danger: true,
          onclick: () => { deleteFileOpen = r; }
        }
      ]
    };
  }
</script>

<svelte:head><title>{pageTitle('Admin · Playback errors')}</title></svelte:head>

<div class="space-y-4">
  <header class="flex items-end justify-between gap-3">
    <div>
      <h1 class="font-display text-2xl font-medium text-text-primary">Playback errors</h1>
      <p class="text-sm text-text-secondary">
        Tracks that failed to play on a client. Reported automatically by
        the Android player when a track loads with zero duration or
        decode-fails — the player skips fast and logs here for triage.
      </p>
    </div>
    {#if !query.isPending && !query.isError && !resolved}
      <span class="rounded-full bg-accent/15 px-3 py-1 text-sm text-accent">
        {rows.length} unresolved
      </span>
    {/if}
  </header>

  <!-- Tab toggle. Resolved / unresolved are two separate query keys so
       the cache holds both lists without re-fetching when you flip. -->
  <div role="tablist" aria-label="Resolution status" class="flex gap-2 border-b border-border">
    <button
      type="button"
      role="tab"
      aria-selected={!resolved}
      onclick={() => { resolved = false; }}
      class="border-b-2 px-3 py-2 text-sm transition-colors {!resolved
        ? 'border-accent text-text-primary'
        : 'border-transparent text-text-secondary hover:text-text-primary'}"
    >Unresolved</button>
    <button
      type="button"
      role="tab"
      aria-selected={resolved}
      onclick={() => { resolved = true; }}
      class="border-b-2 px-3 py-2 text-sm transition-colors {resolved
        ? 'border-accent text-text-primary'
        : 'border-transparent text-text-secondary hover:text-text-primary'}"
    >Resolved</button>
  </div>

  {#if query.isError}
    <p class="text-error">Couldn't load: {errMessage(query.error)}</p>
  {:else if query.isPending}
    <p class="text-text-secondary">Loading…</p>
  {:else if rows.length === 0}
    <p class="text-text-secondary">
      {resolved ? 'No resolved errors yet.' : 'No unresolved errors.'}
    </p>
  {:else}
    <ul class="divide-y divide-border rounded-md border border-border">
      {#each rows as r (r.id)}
        <li class="flex items-start gap-3 px-3 py-3">
          <div class="min-w-0 flex-1">
            <div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
              <span class="truncate text-sm font-medium text-text-primary">
                {r.track_title}
              </span>
              <span class="truncate text-xs text-text-secondary">
                {r.artist_name} · {r.album_title}
              </span>
              <span class="rounded bg-surface-hover px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-text-secondary">
                {kindLabel(r.kind)}
              </span>
              {#if resolved && r.resolution}
                <span class="rounded bg-accent/15 px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-accent">
                  {resolutionLabel(r.resolution)}
                </span>
              {/if}
            </div>
            <div class="mt-0.5 text-xs text-text-muted">
              by {r.username} · {relativeTime(r.occurred_at)}
              {#if r.detail}<span class="text-text-secondary"> · {r.detail}</span>{/if}
            </div>
            <div class="mt-1 truncate text-[11px] font-mono text-text-muted" title={r.track_file_path}>
              {r.track_file_path}
            </div>
          </div>
          {#if !resolved}
            {@const a = actionsFor(r)}
            <RowActionsMenu primary={a.primary} secondary={a.secondary} />
          {/if}
        </li>
      {/each}
    </ul>
  {/if}
</div>

<Modal
  title="Delete file?"
  open={deleteFileOpen !== null}
  onClose={() => { deleteFileOpen = null; }}
>
  {#if deleteFileOpen}
    <p class="text-sm text-text-secondary">
      This removes <span class="font-medium text-text-primary">{deleteFileOpen.track_title}</span>
      from the library and deletes the underlying file. The error row
      will be marked resolved.
    </p>
    <p class="mt-2 text-xs text-text-muted font-mono">{deleteFileOpen.track_file_path}</p>
    <div class="mt-4 flex justify-end gap-2">
      <button
        type="button"
        onclick={() => { deleteFileOpen = null; }}
        class="rounded px-3 py-2 text-sm text-text-secondary hover:text-text-primary"
      >Cancel</button>
      <button
        type="button"
        onclick={confirmDeleteFile}
        class="rounded bg-action-destructive px-3 py-2 text-sm text-action-fg hover:opacity-90"
      >Delete file</button>
    </div>
  {/if}
</Modal>

<Modal
  title="Mark resolved"
  open={resolveOpen !== null}
  onClose={() => { resolveOpen = null; }}
>
  {#if resolveOpen}
    <p class="text-sm text-text-secondary">
      Marking <span class="font-medium text-text-primary">{resolveOpen.track_title}</span> resolved.
      Pick how you handled it:
    </p>
    <label class="mt-3 block text-xs text-text-secondary">
      Resolution
      <select
        bind:value={resolveChoice}
        class="mt-1 block w-full rounded border border-border bg-surface px-2 py-1.5 text-sm text-text-primary"
      >
        <option value="fixed">Fixed — track now plays</option>
        <option value="ignored">Ignored — no action needed</option>
      </select>
    </label>
    <div class="mt-4 flex justify-end gap-2">
      <button
        type="button"
        onclick={() => { resolveOpen = null; }}
        class="rounded px-3 py-2 text-sm text-text-secondary hover:text-text-primary"
      >Cancel</button>
      <button
        type="button"
        onclick={confirmResolve}
        class="rounded bg-action-primary px-3 py-2 text-sm text-action-fg hover:opacity-90"
      >Mark resolved</button>
    </div>
  {/if}
</Modal>
