import { useCallback, useEffect, useRef, useState } from 'react';
import { ArrowLeft, Pencil, Plus, RefreshCw, Search, Trash2, TriangleAlert } from 'lucide-react';
import { navigateToPath } from '../routing';
import { usePrimaryAction } from '../primaryAction';
import { Badge, Button, Card, ConfirmDialog, Dialog, IconButton, Input, Spinner, Switch } from '../components';
import {
  addTraktList,
  deleteTraktList,
  getTraktListById,
  getTraktLists,
  getTraktStatus,
  matchTraktList,
  messageFromTraktError,
  updateTraktList,
  type TraktList
} from '../api';

const TRAKT_BASE_PATH = '/app/trakt-lists';

function viewMatchedItems(traktId: number) {
  navigateToPath(`/app/search?query=${encodeURIComponent(`trakt_list:${traktId}`)}`);
}

function traktListIdFromPathname(pathname: string): number | null {
  const normalized = pathname.replace(/\/+$/, '');

  if (!normalized.startsWith(`${TRAKT_BASE_PATH}/`)) {
    return null;
  }

  const id = Number(normalized.slice(TRAKT_BASE_PATH.length + 1).split('/')[0]);
  return Number.isInteger(id) && id > 0 ? id : null;
}

// The status endpoint is a poll-only substitute for the Blazor page's live
// IEntityLocker.OnTraktChanged event (no push channel exists for the REST API). Poll while
// busy; stop once idle; the caller re-fetches its own data on the busy -> idle transition.
function useTraktBusyPoll(onIdleTransition: () => void) {
  const [busy, setBusy] = useState(false);
  const wasBusyRef = useRef(false);
  const timerRef = useRef<number | undefined>(undefined);
  const onIdleTransitionRef = useRef(onIdleTransition);
  // Indirection so the recursive setTimeout call always reaches the latest poll closure
  // without the callback needing to reference its own (not-yet-assigned) binding.
  const pollRef = useRef<() => void>(() => {});

  useEffect(() => {
    onIdleTransitionRef.current = onIdleTransition;
  });

  useEffect(() => {
    pollRef.current = () => {
      getTraktStatus()
        .then((status) => {
          setBusy(status.busy);

          if (wasBusyRef.current && !status.busy) {
            onIdleTransitionRef.current();
          }

          wasBusyRef.current = status.busy;

          if (status.busy) {
            timerRef.current = window.setTimeout(() => pollRef.current(), 2500);
          }
        })
        .catch(() => {
          // Give up silently; the next dispatched action restarts polling via markBusy().
        });
    };
  });

  useEffect(() => {
    pollRef.current();
    return () => window.clearTimeout(timerRef.current);
  }, []);

  const markBusy = useCallback(() => {
    setBusy(true);
    wasBusyRef.current = true;
    window.clearTimeout(timerRef.current);
    timerRef.current = window.setTimeout(() => pollRef.current(), 2500);
  }, []);

  return { busy, markBusy };
}

/* ---------- add dialog ---------- */

function AddTraktListDialog({
  busy,
  error,
  onCancel,
  onSubmit,
  open
}: {
  busy: boolean;
  error: string | null;
  onCancel: () => void;
  onSubmit: (url: string) => void;
  open: boolean;
}) {
  const [url, setUrl] = useState('');
  const trimmed = url.trim();

  return (
    <Dialog
      footer={
        <>
          <Button disabled={busy} onClick={onCancel} variant="secondary">
            Cancel
          </Button>
          <Button disabled={busy || trimmed.length === 0} loading={busy} onClick={() => onSubmit(trimmed)} variant="primary">
            Add
          </Button>
        </>
      }
      onClose={onCancel}
      open={open}
      title="Add Trakt list"
      width={480}
    >
      <Input
        label="Trakt list URL"
        onChange={(event) => setUrl(event.target.value)}
        placeholder="https://trakt.tv/users/username/lists/list-name"
        value={url}
      />
      <p className="ctv-collections-picker-note">
        Fetching, saving and matching happen in the background — this dialog closes right away and the list appears
        once the server finishes.
      </p>
      {error && (
        <span className="ctv-field-error" role="alert">
          {error}
        </span>
      )}
    </Dialog>
  );
}

/* ---------- sub-path editor ---------- */

function TraktListEditor({
  id,
  onBack,
  onBackgroundMatch,
}: {
  id: number;
  onBack: () => void;
  onBackgroundMatch: () => void;
}) {
  const [list, setList] = useState<TraktList | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [autoRefresh, setAutoRefresh] = useState(false);
  const [generatePlaylist, setGeneratePlaylist] = useState(false);
  const [saving, setSaving] = useState(false);
  const [saveError, setSaveError] = useState<string | null>(null);
  const [saved, setSaved] = useState(false);

  useEffect(() => {
    let active = true;

    getTraktListById(id)
      .then((fetched) => {
        if (!active) {
          return;
        }

        setList(fetched);
        setAutoRefresh(fetched.autoRefresh);
        setGeneratePlaylist(fetched.generatePlaylist);
      })
      .catch((fetchError: unknown) => {
        if (active) {
          setError(messageFromTraktError(fetchError, 'Unable to load Trakt list'));
        }
      })
      .finally(() => {
        if (active) {
          setLoading(false);
        }
      });

    return () => {
      active = false;
    };
  }, [id]);

  const save = async () => {
    setSaving(true);
    setSaveError(null);
    setSaved(false);

    try {
      const updated = await updateTraktList(id, { autoRefresh, generatePlaylist });
      setList(updated);
      setSaved(true);
      if (generatePlaylist) {
        // saving with generatePlaylist enqueues a background match server-side
        onBackgroundMatch();
      }
    } catch (updateError) {
      setSaveError(messageFromTraktError(updateError, 'Unable to save Trakt list'));
    } finally {
      setSaving(false);
    }
  };

  return (
    <div className="ctv-collections">
      <div className="ctv-channels-actionbar">
        <Button onClick={onBack} size="sm" startIcon={<ArrowLeft aria-hidden="true" size={14} />} variant="ghost">
          All Trakt lists
        </Button>
        <span className="ctv-collections-detail-title">{list?.name ?? `Trakt list ${id}`}</span>
      </div>

      {error && (
        <div className="ctv-channels-error" role="alert">
          <TriangleAlert aria-hidden="true" size={15} />
          <span>{error}</span>
        </div>
      )}

      <Card>
        {loading ? (
          <div className="ctv-collections-loading">
            <Spinner size={18} />
            <span>Loading Trakt list…</span>
          </div>
        ) : list ? (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
            <Input disabled label="Slug" value={list.slug} />
            <label className="ctv-collections-order-toggle" title="Automatically refresh this list's items">
              <Switch checked={autoRefresh} onChange={setAutoRefresh} size="sm" />
              <span>Auto refresh</span>
            </label>
            <label className="ctv-collections-order-toggle" title="Generate a system playlist from this list">
              <Switch checked={generatePlaylist} onChange={setGeneratePlaylist} size="sm" />
              <span>Generate playlist</span>
            </label>
            {saveError && (
              <span className="ctv-field-error" role="alert">
                {saveError}
              </span>
            )}
            {saved && !saveError && <Badge tone="ok">Saved</Badge>}
            <div>
              <Button disabled={saving} loading={saving} onClick={() => void save()} variant="primary">
                Save changes
              </Button>
            </div>
          </div>
        ) : null}
      </Card>
    </div>
  );
}

/* ---------- screen ---------- */

export function TraktListsScreen() {
  const editingId = traktListIdFromPathname(window.location.pathname);

  const [lists, setLists] = useState<TraktList[]>([]);
  const [totalCount, setTotalCount] = useState(0);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [addOpen, setAddOpen] = useState(false);
  const [addBusy, setAddBusy] = useState(false);
  const [addError, setAddError] = useState<string | null>(null);
  const [deleteTarget, setDeleteTarget] = useState<TraktList | null>(null);
  const [deleteBusy, setDeleteBusy] = useState(false);
  const [deleteError, setDeleteError] = useState<string | null>(null);
  const [rowError, setRowError] = useState<string | null>(null);
  const activeRef = useRef(true);

  // TopBar "Add Trakt List" primary action. On the list, open the add dialog. On the
  // detail sub-route (/app/trakt-lists/{id}) the add dialog isn't mounted and this component
  // is keyed by pathname (so state wouldn't survive a navigate), so route back to the list
  // instead of flipping a state nobody renders — avoids a silent no-op button (#238 review).
  usePrimaryAction('traktLists', () => {
    if (editingId != null) {
      navigateToPath(TRAKT_BASE_PATH);
    } else {
      setAddOpen(true);
    }
  });

  // Fetch only; state updates happen in the async callbacks (never synchronously in the
  // effect body). `loading` starts true and flips false in `finally`, so the mount effect
  // shows the spinner; later quiet reloads (e.g. after a busy -> idle transition) never
  // touch `loading`, so the table stays visible instead of flashing back to a spinner.
  const load = useCallback(() => {
    getTraktLists({ pageSize: 100 })
      .then((paged) => {
        if (activeRef.current) {
          setLists(paged.page ?? []);
          setTotalCount(paged.totalCount ?? 0);
          setError(null);
        }
      })
      .catch((loadError: unknown) => {
        if (activeRef.current) {
          setError(messageFromTraktError(loadError));
        }
      })
      .finally(() => {
        if (activeRef.current) {
          setLoading(false);
        }
      });
  }, []);

  const refresh = useCallback(() => {
    setLoading(true);
    load();
  }, [load]);

  useEffect(() => {
    activeRef.current = true;
    load();

    return () => {
      activeRef.current = false;
    };
  }, [load]);

  const { busy, markBusy } = useTraktBusyPoll(load);

  if (editingId !== null) {
    return (
      <TraktListEditor
        id={editingId}
        onBack={() => navigateToPath(TRAKT_BASE_PATH)}
        onBackgroundMatch={markBusy}
      />
    );
  }

  const submitAdd = async (url: string) => {
    setAddBusy(true);
    setAddError(null);

    try {
      await addTraktList(url);
      markBusy();
      setAddOpen(false);
    } catch (submitError) {
      setAddError(messageFromTraktError(submitError, 'Unable to add Trakt list'));
    } finally {
      setAddBusy(false);
    }
  };

  const match = async (list: TraktList) => {
    setRowError(null);

    try {
      await matchTraktList(list.id);
      markBusy();
    } catch (matchError) {
      setRowError(messageFromTraktError(matchError, 'Unable to match Trakt list items'));
    }
  };

  const confirmDelete = async () => {
    if (!deleteTarget) {
      return;
    }

    setDeleteBusy(true);
    setDeleteError(null);

    try {
      await deleteTraktList(deleteTarget.id);
      markBusy();
      setDeleteTarget(null);
    } catch (removeError) {
      setDeleteError(messageFromTraktError(removeError, 'Unable to delete Trakt list'));
    } finally {
      setDeleteBusy(false);
    }
  };

  return (
    <div className="ctv-collections">
      <div className="ctv-channels-actionbar">
        <span className="ctv-collections-detail-title">Trakt Lists</span>
        {busy && (
          <Badge tone="accent">
            <Spinner size={12} /> Busy
          </Badge>
        )}
        <span className="ctv-channels-spacer" />
        <Button
          disabled={busy}
          onClick={() => {
            setAddError(null);
            setAddOpen(true);
          }}
          size="sm"
          startIcon={<Plus aria-hidden="true" size={14} />}
        >
          Add Trakt list
        </Button>
      </div>

      {error && (
        <div className="ctv-channels-error" role="alert">
          <TriangleAlert aria-hidden="true" size={15} />
          <span>{error}</span>
          <span className="ctv-channels-spacer" />
          <Button onClick={refresh} size="sm" variant="secondary">
            Retry
          </Button>
        </div>
      )}

      {rowError && (
        <div className="ctv-channels-error" role="alert">
          <TriangleAlert aria-hidden="true" size={15} />
          <span>{rowError}</span>
        </div>
      )}

      <Card padded={false}>
        {loading ? (
          <div className="ctv-collections-loading" role="status">
            <Spinner size={18} />
            <span>Loading Trakt lists…</span>
          </div>
        ) : lists.length === 0 ? (
          <div className="ctv-collections-empty">No Trakt lists yet.</div>
        ) : (
          <div className="ctv-channels-table-frame">
            <div className="ctv-channels-table-scroll">
              <table aria-label="Trakt lists" className="ctv-channels-table">
                <thead>
                  <tr>
                    <th>Slug</th>
                    <th>Name</th>
                    <th>Match status</th>
                    <th />
                  </tr>
                </thead>
                <tbody>
                  {lists.map((list) => (
                    <tr key={list.id}>
                      <td style={{ fontFamily: 'var(--font-mono)' }}>{list.slug}</td>
                      <td>{list.name}</td>
                      <td>
                        {list.matchCount} of {list.itemCount}
                      </td>
                      <td>
                        <div style={{ alignItems: 'center', display: 'flex', gap: 4 }}>
                          <IconButton
                            disabled={busy}
                            onClick={() => navigateToPath(`${TRAKT_BASE_PATH}/${list.id}`)}
                            size="sm"
                            title="Edit Trakt list properties"
                          >
                            <Pencil aria-hidden="true" size={14} />
                          </IconButton>
                          <IconButton
                            disabled={busy}
                            onClick={() => void match(list)}
                            size="sm"
                            title="Match Trakt list items"
                          >
                            <RefreshCw aria-hidden="true" size={14} />
                          </IconButton>
                          <IconButton
                            disabled={busy}
                            onClick={() => viewMatchedItems(list.traktId)}
                            size="sm"
                            title="View matched items"
                          >
                            <Search aria-hidden="true" size={14} />
                          </IconButton>
                          <IconButton
                            disabled={busy}
                            onClick={() => setDeleteTarget(list)}
                            size="sm"
                            title="Delete Trakt list"
                          >
                            <Trash2 aria-hidden="true" size={14} />
                          </IconButton>
                        </div>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
            <div className="ctv-channels-footer">
              <span>
                {totalCount} list{totalCount === 1 ? '' : 's'}
              </span>
            </div>
          </div>
        )}
      </Card>

      <div className="ctv-settings-callout">
        <Search aria-hidden="true" size={14} />
        <span>
          "View matched items" opens the SPA search screen, filtered to this list's matched items.
        </span>
      </div>

      <AddTraktListDialog
        busy={addBusy}
        error={addError}
        key={`add-${addOpen}`}
        onCancel={() => setAddOpen(false)}
        onSubmit={(url) => void submitAdd(url)}
        open={addOpen}
      />

      <ConfirmDialog
        busy={deleteBusy}
        confirmLabel="Delete"
        message={
          deleteTarget ? (
            <>
              <span>{`Delete "${deleteTarget.name}"? This cannot be undone.`}</span>
              {deleteError && (
                <span className="ctv-field-error" role="alert">
                  {deleteError}
                </span>
              )}
            </>
          ) : (
            ''
          )
        }
        onCancel={() => {
          setDeleteTarget(null);
          setDeleteError(null);
        }}
        onConfirm={() => void confirmDelete()}
        open={deleteTarget !== null}
        title="Delete Trakt list"
        tone="danger"
      />
    </div>
  );
}
