/**
 * Snippet Library — the left pane (spec §02).
 *
 * The always-visible list with a pinned "Create New Snippet" item,
 * selection/highlight, and delete. Each row carries a secondary metadata line —
 * draft status indicator, relative date, and size (spec §02). Below the list, the
 * Selected-Snippet Metadata Panel exposes the active snippet's editable Name and
 * Comment (auto-saved), its timestamps and linked datasets, and the Duplicate /
 * Delete operations. A live Search box and a Sort disclosure sit above the list
 * (spec §02), and a Storage Monitor pins to the bottom of the pane.
 */

import { useEffect, useMemo, useRef, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { formatDate } from '@core/date-format';
import {
  formatSnippetSize,
  hasUnpublishedChanges,
  snippetSizeBytes,
  type Snippet,
} from '@core/snippet';
import { filterAndSortSnippets } from '@core/snippet-sort';
import { openModal } from '../modals/ModalCoordinator';
import { confirm } from '../stores/ConfirmStore';
import { notify } from '../stores/NotificationStore';
import { selectActiveSnippet, useSnippetStore } from '../stores/SnippetStore';
import { useUserSettingsStore } from '../stores/UserSettingsStore';
import { Button } from './Button';
import { Icon } from './Icon';
import { IconButton } from './IconButton';
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
import { SettingRow, SettingsPopover, TextControl } from './SettingsPopover';
import { SortControl } from './SortControl';
import StorageMonitor from './StorageMonitor';
import styles from './SnippetLibrary.module.css';

/** Date display modes (spec §07 → Formatting). */
const DATE_FORMAT_OPTIONS: ReadonlyArray<SegmentedOption<'smart' | 'iso' | 'custom'>> = [
  { value: 'smart', label: 'Smart' },
  { value: 'iso', label: 'ISO' },
  { value: 'custom', label: 'Custom' },
];

/**
 * Library settings cluster (spec §07 → Formatting), disclosed from the library
 * toolbar — it controls the timestamps the list and metadata panel render. Opens
 * rightward (align left) so the wide panel clears the narrow left pane.
 */
function LibrarySettings() {
  const formatting = useUserSettingsStore((s) => s.saved.formatting);
  const setFormatting = useUserSettingsStore((s) => s.setFormatting);
  return (
    <SettingsPopover id="library-settings" label="Date format settings" title="Dates" align="left">
      <SettingRow label="Date format">
        <SegmentedControl
          label="Date format"
          options={DATE_FORMAT_OPTIONS}
          value={formatting.dateFormat}
          onChange={(dateFormat) => setFormatting({ dateFormat })}
        />
      </SettingRow>
      {formatting.dateFormat === 'custom' && (
        <SettingRow label="Custom" htmlFor="set-customdate">
          <TextControl
            id="set-customdate"
            value={formatting.customDateFormat}
            placeholder="yyyy-MM-dd HH:mm"
            onChange={(customDateFormat) => setFormatting({ customDateFormat })}
          />
        </SettingRow>
      )}
    </SettingsPopover>
  );
}

/** Auto-save settle time for the metadata panel's Name/Comment fields, mirroring
 *  the editor's draft auto-save (spec §02 → "edits save automatically"). */
const META_AUTOSAVE_MS = 400;

/**
 * Selected-Snippet Metadata Panel (spec §02). Keyed by snippet id by its caller,
 * so switching the active snippet remounts it and the local field state re-seeds
 * cleanly. Name/Comment edits auto-save: debounced while typing and flushed on
 * blur (so clicking away to another snippet commits before this unmounts). Both
 * advance the snippet's modified time via the store (§02 → Sort).
 */
function SnippetMeta({
  snippet,
  onDuplicate,
  onDelete,
}: {
  snippet: Snippet;
  onDuplicate: () => void;
  onDelete: () => void;
}) {
  const renameSnippet = useSnippetStore((s) => s.renameSnippet);
  const setComment = useSnippetStore((s) => s.setComment);
  const formatting = useUserSettingsStore((s) => s.saved.formatting);
  const [name, setName] = useState(snippet.name);
  const [comment, setCommentLocal] = useState(snippet.comment);

  // The store can rename underneath this panel (publish's content-derived
  // naming, spec §03D). Adopt the new store name unless the local field has
  // diverged — i.e. the user is mid-edit, and their text wins. Without this,
  // the stale local value differs from the store and the auto-save below
  // writes the old name right back, silently undoing the publish rename.
  const lastStoreName = useRef(snippet.name);
  useEffect(() => {
    if (snippet.name === lastStoreName.current) return;
    const previous = lastStoreName.current;
    lastStoreName.current = snippet.name;
    setName((local) => (local === previous ? snippet.name : local));
  }, [snippet.name]);

  useEffect(() => {
    if (name === snippet.name) return;
    const t = setTimeout(() => renameSnippet(snippet.id, name), META_AUTOSAVE_MS);
    return () => clearTimeout(t);
  }, [name, snippet.id, snippet.name, renameSnippet]);

  useEffect(() => {
    if (comment === snippet.comment) return;
    const t = setTimeout(() => setComment(snippet.id, comment), META_AUTOSAVE_MS);
    return () => clearTimeout(t);
  }, [comment, snippet.id, snippet.comment, setComment]);

  return (
    <section className={styles.meta} aria-label="Snippet details">
      <label className={styles.metaField}>
        <span className={styles.metaLabel}>Name</span>
        <input
          className={styles.metaName}
          value={name}
          onChange={(e) => setName(e.target.value)}
          onBlur={() => renameSnippet(snippet.id, name)}
          spellCheck={false}
        />
      </label>

      <label className={styles.metaField}>
        <span className={styles.metaLabel}>Comment</span>
        <textarea
          className={styles.metaComment}
          value={comment}
          onChange={(e) => setCommentLocal(e.target.value)}
          onBlur={() => setComment(snippet.id, comment)}
          rows={2}
          placeholder="Add a note…"
        />
      </label>

      <dl className={styles.metaTimes}>
        <div>
          <dt>Created</dt>
          <dd>{formatDate(snippet.created, formatting.dateFormat, formatting.customDateFormat)}</dd>
        </div>
        <div>
          <dt>Modified</dt>
          <dd>
            {formatDate(snippet.modified, formatting.dateFormat, formatting.customDateFormat)}
          </dd>
        </div>
      </dl>

      {snippet.datasetRefs.length > 0 && (
        <div className={styles.metaLinked}>
          <span className={styles.metaLabel}>Linked datasets</span>
          <ul className={styles.linkedList}>
            {snippet.datasetRefs.map((ref) => (
              <li key={ref} className={styles.linkedItem}>
                <Icon name="dataset" />
                <span className={styles.linkedName}>{ref}</span>
              </li>
            ))}
          </ul>
        </div>
      )}

      <div className={styles.metaActions}>
        <Button className={styles.metaAction} onClick={onDuplicate}>
          Duplicate
        </Button>
        <Button variant="danger-outline" className={styles.metaAction} onClick={onDelete}>
          Delete
        </Button>
      </div>
    </section>
  );
}

export function SnippetLibrary() {
  const snippets = useSnippetStore(useShallow((s) => s.snippets));
  const activeId = useSnippetStore((s) => s.activeSnippetId);
  const activeSnippet = useSnippetStore(selectActiveSnippet);
  const createSnippet = useSnippetStore((s) => s.createSnippet);
  const selectSnippet = useSnippetStore((s) => s.selectSnippet);
  const removeSnippet = useSnippetStore((s) => s.removeSnippet);
  const duplicateActiveSnippet = useSnippetStore((s) => s.duplicateActiveSnippet);
  const searchQuery = useSnippetStore((s) => s.searchQuery);
  const sortBy = useSnippetStore((s) => s.sortBy);
  const sortOrder = useSnippetStore((s) => s.sortOrder);
  const setSearch = useSnippetStore((s) => s.setSearch);
  const formatting = useUserSettingsStore((s) => s.saved.formatting);
  const searchRef = useRef<HTMLInputElement>(null);

  // Derive the visible list with useMemo over PRIMITIVE store values — never
  // inside a selector that builds a fresh array, which would loop the app
  // (MEMORY → "Zustand stable selectors"; the pure pipeline lives in core).
  const ordered = useMemo(
    () => filterAndSortSnippets(snippets, searchQuery, { sortBy, sortOrder }),
    [snippets, searchQuery, sortBy, sortOrder],
  );

  // The list owes a single empty state: a search that matched nothing (council
  // EMPTY STATES). A genuinely empty library is handled one level up — App replaces
  // the whole pane chrome with the onboarding canvas (spec §02 → First-Run & Empty
  // Workspace), so this component never renders with zero snippets.
  const searching = searchQuery.trim() !== '';
  const noMatches = ordered.length === 0;

  const clearSearch = () => {
    setSearch('');
    searchRef.current?.focus(); // clear returns focus to the input (council SEARCH)
  };

  const handleDelete = async (id: string, name: string) => {
    // In-app confirmation (docs/architecture/03 → confirmation dialogs).
    const ok = await confirm({
      title: 'Delete snippet',
      message: `Delete "${name}"? This cannot be undone.`,
      confirmLabel: 'Delete',
      danger: true,
    });
    if (!ok) return;
    removeSnippet(id);
    // Confirm the deletion (spec §02). The message names which snippet went —
    // useful when toasts stack (council toast-copy rule, docs/architecture/10).
    notify({
      kind: 'success',
      title: 'Snippet deleted',
      message: `"${name}" was permanently removed from your library.`,
    });
  };

  const handleDuplicate = () => {
    const id = duplicateActiveSnippet();
    if (!id) return;
    // The copy isn't visibly distinct from its source at a glance, so the outcome
    // needs a toast (spec §02 → Duplicate; contract 10 §1 — toast what isn't
    // already self-evident, unlike Create which opens visibly in the editor).
    notify({
      kind: 'success',
      title: 'Snippet duplicated',
      message: 'An independent copy was added and is now active.',
    });
  };

  return (
    <div className={styles.library}>
      {/* Library toolbar: heading + the date-format settings gear. */}
      <div className={styles.toolbar}>
        <span className={styles.heading}>Snippets</span>
        <LibrarySettings />
      </div>

      {/* The creation surface (spec §02): the guided path is the primary — Build
          Chart opens the Chart Builder — and raw JSON stays one visible click away
          as a ghost action (NN/g #6 recognition / #7 expert accelerator; one
          primary per surface — Carbon). Neither raises a toast: both outcomes
          open on-screen (the builder, or the new snippet in the editor). When the
          pane is dragged narrow the labels collapse to icons (a @container
          query); `aria-label` keeps each accessible name. */}
      <div className={styles.createRow}>
        <Button
          variant="primary"
          size="lg"
          className={styles.createNew}
          onClick={() => openModal('chartBuilder')}
          aria-label="Build Chart"
          title="Build Chart"
        >
          <Icon name="chart" />
          <span className={styles.createNewLabel}>Build Chart</span>
        </Button>
        <Button
          size="lg"
          className={styles.createGhost}
          onClick={() => createSnippet()}
          aria-label="New JSON snippet"
          title="New JSON snippet"
        >
          <Icon name="add" />
          <span className={styles.createNewLabel}>New JSON snippet</span>
        </Button>
      </div>

      {/* Search + Sort controls, pinned above the list (spec §02; council
          SEARCH + SORT). Search is an unlabelled Carbon active-search: a search
          input with a leading magnifier and a clear (✕) that appears when
          non-empty and returns focus to the input. Sort is a disclosure whose
          trigger shows the current order for recognition. */}
      <div className={styles.controls}>
        <div className={styles.search}>
          <Icon name="search" className={styles.searchIcon} />
          <input
            ref={searchRef}
            type="search"
            className={styles.searchInput}
            // No visible label — the magnifier + placeholder name it (council SEARCH).
            aria-label="Search snippets"
            placeholder="Search snippets"
            value={searchQuery}
            onChange={(e) => setSearch(e.target.value)}
            spellCheck={false}
          />
          {searching && (
            <IconButton
              size="sm"
              className={styles.searchClear}
              label="Clear search"
              onClick={clearSearch}
            >
              <Icon name="close" />
            </IconButton>
          )}
        </div>
        <SortControl />
      </div>

      {/* Polite result-count announcement (council SEARCH; Carbon "always
          include the number of results, including no results"). Only announces
          while searching, so it stays quiet for the default full list. */}
      <div className={styles.srOnly} role="status" aria-live="polite">
        {searching ? `${ordered.length} ${ordered.length === 1 ? 'result' : 'results'}` : ''}
      </div>

      <ul className={styles.list}>
        {noMatches && searching && (
          // The list's one empty state: a search that matched nothing (council
          // EMPTY STATES). A genuinely empty library never reaches here — App shows
          // the onboarding canvas in place of this pane (spec §02).
          <li className={styles.empty}>
            <span className={styles.emptyTitle}>No snippets match your search</span>
            <span className={styles.emptyHint}>Try a different term.</span>
          </li>
        )}
        {ordered.map((s) => {
          // Size is omitted under ~1 KB per spec §02; null collapses the suffix.
          const size = formatSnippetSize(snippetSizeBytes(s));
          // When sorting by Created, the row shows the created date instead of
          // modified, so the visible date matches the ordering key (spec §02 →
          // List Item).
          const stamp = sortBy === 'created' ? s.created : s.modified;
          const date = formatDate(stamp, formatting.dateFormat, formatting.customDateFormat);
          return (
            <li key={s.id} className={`${styles.item} ${s.id === activeId ? styles.active : ''}`}>
              {/* The row's selectable area is a real <button> so it's keyboard
                  operable (Enter/Space) and focusable — not a click-only <li>.
                  Not a listbox option: APG forbids interactive children, and each
                  row carries a delete button. */}
              <button
                type="button"
                className={styles.itemMain}
                aria-current={s.id === activeId ? 'true' : undefined}
                onClick={() => selectSnippet(s.id)}
              >
                <span className={styles.name}>{s.name}</span>
                {/* Secondary metadata line (spec §02): status · date · size. The
                    indicator sits in a fixed-width leading slot so the date stays
                    aligned across rows whether or not a snippet is dirty. Draft is
                    flagged by presence + label, never colour alone (arch/10 §6). */}
                <span className={styles.sub}>
                  <span className={styles.indicator}>
                    {hasUnpublishedChanges(s) && (
                      <span
                        className={styles.draftDot}
                        title="Has unpublished draft changes"
                        aria-label="Has unpublished draft changes"
                      />
                    )}
                  </span>
                  <span className={styles.date}>{size ? `${date} · ${size}` : date}</span>
                  {/* Linked datasets (spec §02): the snippet side of the
                      bidirectional link, maintained on publish via datasetRefs. */}
                  {s.datasetRefs.length > 0 && (
                    <span
                      className={styles.datasets}
                      title={`Linked datasets: ${s.datasetRefs.join(', ')}`}
                      aria-label={`${s.datasetRefs.length} linked dataset${s.datasetRefs.length === 1 ? '' : 's'}`}
                    >
                      <Icon name="dataset" />
                      {s.datasetRefs.length}
                    </span>
                  )}
                </span>
              </button>
              <IconButton
                size="sm"
                className={styles.delete}
                label={`Delete ${s.name}`}
                title="Delete snippet"
                onClick={() => void handleDelete(s.id, s.name)}
              >
                <Icon name="delete" />
              </IconButton>
            </li>
          );
        })}
      </ul>

      {/* Selected-Snippet Metadata Panel (spec §02). Keyed by id so it re-seeds
          its local field state when the active snippet changes. */}
      {activeSnippet && (
        <SnippetMeta
          key={activeSnippet.id}
          snippet={activeSnippet}
          onDuplicate={handleDuplicate}
          onDelete={() => void handleDelete(activeSnippet.id, activeSnippet.name)}
        />
      )}

      {/* Storage Monitor (spec §02 → Storage Monitor) — pinned at the bottom of
          the library pane, below the metadata panel. Built by a parallel agent. */}
      <StorageMonitor />
    </div>
  );
}
