import { useCallback, useEffect, useRef, useState } from 'react';
import { ArrowLeft, Check, Layers, Plus, Trash2, TriangleAlert } from 'lucide-react';
import { Badge, Button, Card, ConfirmDialog, IconButton, Input, Select, Spinner, Switch } from '../components';
import { usePrimaryAction } from '../primaryAction';
import {
  ApiError,
  createMultiCollection,
  deleteMultiCollection,
  getCollections,
  getMultiCollectionWithMeta,
  getMultiCollections,
  getSmartCollections,
  messageFromMultiCollectionError,
  updateMultiCollection,
  type MediaCollection,
  type MultiCollection,
  type MultiCollectionItemRequest,
  type SmartCollection
} from '../api';

/* ---------- data hook ---------- */

type ListState =
  | { data: MultiCollection[]; error: null; status: 'success' }
  | { data: null; error: string; status: 'error' }
  | { data: null; error: null; status: 'loading' };

function useMultiCollectionsData() {
  const [state, setState] = useState<ListState>({ data: null, error: null, status: 'loading' });
  const activeRef = useRef(true);

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

  const load = useCallback(() => {
    getMultiCollections({ pageSize: 1000 })
      .then((result) => {
        if (activeRef.current) {
          setState({ data: result.page ?? [], error: null, status: 'success' });
        }
      })
      .catch((error: unknown) => {
        if (activeRef.current) {
          setState({ data: null, error: messageFromMultiCollectionError(error), status: 'error' });
        }
      });
  }, []);

  useEffect(() => {
    load();
  }, [load]);

  const refresh = useCallback(
    (quiet = false) => {
      if (!quiet) {
        setState({ data: null, error: null, status: 'loading' });
      }

      load();
    },
    [load]
  );

  return { refresh, state };
}

/* ---------- editor ---------- */

// Per-source weight bounds. Must mirror the API's MultiCollectionItemWeight validator (1..1000) so a
// client-side value never provokes a raw 400 (docs/decisions.md 2026-07-17, #404).
const WEIGHT_MIN = 1;
const WEIGHT_MAX = 1000;

// Coerce the editor's free-text weight to a valid integer share. Empty / non-numeric / out-of-range
// all snap to the [1,1000] bound — 1 (the floor, = fair share) when the field is blank or invalid.
function clampWeight(raw: string): number {
  const parsed = Math.trunc(Number(raw));
  if (!Number.isFinite(parsed)) {
    return WEIGHT_MIN;
  }
  return Math.min(WEIGHT_MAX, Math.max(WEIGHT_MIN, parsed));
}

// One row in the editor: a manual collection or a smart collection, plus its schedule-as-group flag
// and per-source weight. `weight` is held as a string so the number field edits smoothly (transient
// empty / partial values are allowed); it is clamped to the API bounds on blur and again at save.
// Names are resolved from the fetched collection lists.
interface DraftItem {
  id: number;
  kind: 'manual' | 'smart';
  name: string;
  scheduleAsGroup: boolean;
  weight: string;
}

function itemsFromMultiCollection(mc: MultiCollection): DraftItem[] {
  return mc.items.map((item) =>
    item.collectionId != null
      ? {
          id: item.collectionId,
          kind: 'manual',
          name: item.name,
          scheduleAsGroup: item.scheduleAsGroup,
          // Round-trip the persisted weight — the update REPLACES the item list, so dropping it here
          // would silently reset every weight to 1 on save (the API returns it for exactly this
          // reason). Default 1 for rows created before weights existed (docs/decisions.md, #404).
          weight: String(item.weight ?? WEIGHT_MIN)
        }
      : {
          id: item.smartCollectionId ?? 0,
          kind: 'smart',
          name: item.name,
          scheduleAsGroup: item.scheduleAsGroup,
          weight: String(item.weight ?? WEIGHT_MIN)
        }
  );
}

function toItemRequest(item: DraftItem): MultiCollectionItemRequest {
  return {
    collectionId: item.kind === 'manual' ? item.id : null,
    playbackOrder: 'Chronological',
    scheduleAsGroup: item.scheduleAsGroup,
    smartCollectionId: item.kind === 'smart' ? item.id : null,
    weight: clampWeight(item.weight)
  };
}

function MultiCollectionEditor({
  initial,
  onBack,
  onSaved
}: {
  initial: MultiCollection | null;
  onBack: () => void;
  onSaved: () => void;
}) {
  const [name, setName] = useState(initial?.name ?? '');
  const [items, setItems] = useState<DraftItem[]>(initial ? itemsFromMultiCollection(initial) : []);
  const [collections, setCollections] = useState<MediaCollection[]>([]);
  const [smartCollections, setSmartCollections] = useState<SmartCollection[]>([]);
  const [selectedCollection, setSelectedCollection] = useState('');
  const [selectedSmart, setSelectedSmart] = useState('');
  const [loadError, setLoadError] = useState<string | null>(null);
  const [saveError, setSaveError] = useState<string | null>(null);
  const [saving, setSaving] = useState(false);
  const [conflictOpen, setConflictOpen] = useState(false);
  const [reloadKey, setReloadKey] = useState(0);
  const activeRef = useRef(true);
  // Concurrency ETag (issue #253): captured from the single-record GET when editing an existing
  // multi-collection, sent as If-Match on save. There's no rotation-on-save because the editor
  // always returns to the list after a successful save (see onSaved below).
  const etagRef = useRef<string | null>(null);

  useEffect(() => {
    activeRef.current = true;
    Promise.all([
      getCollections(),
      getSmartCollections(),
      initial ? getMultiCollectionWithMeta(initial.id) : Promise.resolve(null)
    ])
      .then(([manual, smart, mcMeta]) => {
        if (activeRef.current) {
          setCollections(manual);
          setSmartCollections(smart);
          if (mcMeta) {
            etagRef.current = mcMeta.etag;
            setName(mcMeta.data.name ?? '');
            setItems(itemsFromMultiCollection(mcMeta.data));
          }
          setLoadError(null);
        }
      })
      .catch((error: unknown) => {
        if (activeRef.current) {
          setLoadError(messageFromMultiCollectionError(error, 'Unable to load collections'));
        }
      });

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

  const hasManual = (id: number) => items.some((item) => item.kind === 'manual' && item.id === id);
  const hasSmart = (id: number) => items.some((item) => item.kind === 'smart' && item.id === id);

  const addCollection = () => {
    const id = Number(selectedCollection);
    if (!id || hasManual(id)) {
      return;
    }

    const collection = collections.find((entry) => entry.id === id);
    setItems((current) => [
      ...current,
      { id, kind: 'manual', name: collection?.name ?? `#${id}`, scheduleAsGroup: false, weight: String(WEIGHT_MIN) }
    ]);
    setSelectedCollection('');
  };

  const addSmart = () => {
    const id = Number(selectedSmart);
    if (!id || hasSmart(id)) {
      return;
    }

    const collection = smartCollections.find((entry) => entry.id === id);
    setItems((current) => [
      ...current,
      { id, kind: 'smart', name: collection?.name ?? `#${id}`, scheduleAsGroup: false, weight: String(WEIGHT_MIN) }
    ]);
    setSelectedSmart('');
  };

  const removeItem = (target: DraftItem) => {
    setItems((current) => current.filter((item) => !(item.kind === target.kind && item.id === target.id)));
  };

  const setScheduleAsGroup = (target: DraftItem, next: boolean) => {
    setItems((current) =>
      current.map((item) => (item.kind === target.kind && item.id === target.id ? { ...item, scheduleAsGroup: next } : item))
    );
  };

  // Free-text while typing (allow transient empty / partial); the value is clamped to the API bounds
  // on blur (normalizeWeight) and again at save (clampWeight in toItemRequest).
  const setWeight = (target: DraftItem, next: string) => {
    setItems((current) =>
      current.map((item) => (item.kind === target.kind && item.id === target.id ? { ...item, weight: next } : item))
    );
  };

  const normalizeWeight = (target: DraftItem) => {
    setItems((current) =>
      current.map((item) =>
        item.kind === target.kind && item.id === target.id ? { ...item, weight: String(clampWeight(item.weight)) } : item
      )
    );
  };

  // Fair share = every source at weight 1 (docs/decisions.md 2026-07-17: fair-share is not a separate
  // mode, just WeightedShuffle with equal weights — so this resets, it does not write a different order).
  const resetToFairShare = () => {
    setItems((current) => current.map((item) => ({ ...item, weight: String(WEIGHT_MIN) })));
  };

  const trimmedName = name.trim();

  const save = async () => {
    if (trimmedName.length === 0 || saving) {
      return;
    }

    setSaving(true);
    setSaveError(null);

    try {
      const body = { items: items.map(toItemRequest), name: trimmedName };
      if (initial) {
        await updateMultiCollection(initial.id, body, etagRef.current);
      } else {
        await createMultiCollection(body);
      }

      onSaved();
    } catch (error) {
      if (error instanceof ApiError && error.status === 412) {
        // Another edit landed since we loaded — force a reload rather than overwriting it (#253).
        setConflictOpen(true);
      } else {
        setSaveError(messageFromMultiCollectionError(error, 'Unable to save multi-collection'));
      }
    } finally {
      setSaving(false);
    }
  };

  const reloadAfterConflict = () => {
    setConflictOpen(false);
    setSaveError(null);
    setReloadKey((key) => key + 1);
  };

  const collectionOptions = [
    { label: 'Select a collection…', value: '' },
    ...collections
      .filter((entry) => !hasManual(entry.id))
      .map((entry) => ({ label: entry.name ?? `#${entry.id}`, value: String(entry.id) }))
  ];

  const smartOptions = [
    { label: 'Select a smart collection…', value: '' },
    ...smartCollections
      .filter((entry) => !hasSmart(entry.id))
      .map((entry) => ({ label: entry.name ?? `#${entry.id}`, value: String(entry.id) }))
  ];

  const sortedItems = [...items].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));

  // Percentages are a DISPLAY concern (the wire format is integer relative shares) — a 3:1 weight
  // shows as 75% / 25% (#404). Computed from clamped weights so a transient out-of-range edit never
  // skews the shares mid-type.
  const totalWeight = items.reduce((sum, item) => sum + clampWeight(item.weight), 0);
  const weightPercent = (item: DraftItem): number =>
    totalWeight > 0 ? Math.round((clampWeight(item.weight) / totalWeight) * 100) : 0;
  // Fair share = every source already at weight 1; nothing to reset in that state.
  const alreadyFairShare = items.length > 0 && items.every((item) => clampWeight(item.weight) === WEIGHT_MIN);

  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 multi-collections
        </Button>
        <span className="ctv-collections-detail-title">{initial ? 'Edit multi-collection' : 'New multi-collection'}</span>
        <span className="ctv-channels-spacer" />
        {saveError && <span className="ctv-settings-savebar-error">{saveError}</span>}
        <Button
          disabled={trimmedName.length === 0 || saving}
          loading={saving}
          onClick={() => void save()}
          size="sm"
          startIcon={<Check aria-hidden="true" size={14} />}
        >
          {initial ? 'Save multi-collection' : 'Add multi-collection'}
        </Button>
      </div>

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

      <Card title="Multi-collection">
        <Input
          label="Name"
          onChange={(event) => setName(event.target.value)}
          placeholder="Multi-collection name"
          value={name}
        />

        <div style={{ alignItems: 'flex-end', display: 'flex', gap: 8, marginTop: 16 }}>
          <Select
            label="Collections"
            onChange={(event) => setSelectedCollection(event.target.value)}
            options={collectionOptions}
            value={selectedCollection}
          />
          <Button
            disabled={selectedCollection === ''}
            onClick={addCollection}
            size="sm"
            startIcon={<Plus aria-hidden="true" size={14} />}
            variant="secondary"
          >
            Add Collection
          </Button>
        </div>

        <div style={{ alignItems: 'flex-end', display: 'flex', gap: 8, marginTop: 12 }}>
          <Select
            label="Smart Collections"
            onChange={(event) => setSelectedSmart(event.target.value)}
            options={smartOptions}
            value={selectedSmart}
          />
          <Button
            disabled={selectedSmart === ''}
            onClick={addSmart}
            size="sm"
            startIcon={<Plus aria-hidden="true" size={14} />}
            variant="secondary"
          >
            Add Smart Collection
          </Button>
        </div>
      </Card>

      <Card padded={false}>
        {sortedItems.length === 0 ? (
          <div className="ctv-collections-empty">No collections added yet.</div>
        ) : (
          <>
            {sortedItems.map((item, index) => (
              <div
                className="ctv-settings-flush-row"
                key={`${item.kind}:${item.id}`}
                style={index === 0 ? { borderTop: 'none' } : undefined}
              >
                <Layers aria-hidden="true" color="var(--ctv-accent)" size={15} />
                <span className="ctv-settings-flush-row-main ctv-settings-flush-row-title">{item.name}</span>
                <Badge tone="neutral">{item.kind === 'smart' ? 'Smart' : 'Manual'}</Badge>
                <div
                  className="ctv-collections-weight"
                  title="Relative share of airtime under the Weighted Shuffle playback order"
                >
                  <span className="ctv-collections-weight-label" aria-hidden="true">
                    Weight
                  </span>
                  <Input
                    ariaLabel={`Weight for ${item.name}`}
                    fullWidth={false}
                    inputMode="numeric"
                    max={WEIGHT_MAX}
                    min={WEIGHT_MIN}
                    onBlur={() => normalizeWeight(item)}
                    onChange={(event) => setWeight(item, event.target.value)}
                    size="sm"
                    style={{ width: 68 }}
                    type="number"
                    value={item.weight}
                  />
                  <span className="ctv-collections-weight-pct">{weightPercent(item)}%</span>
                </div>
                <label className="ctv-collections-order-toggle" title="Schedule as group">
                  <Switch
                    checked={item.scheduleAsGroup}
                    onChange={(next) => setScheduleAsGroup(item, next)}
                    size="sm"
                  />
                  <span>Schedule as group</span>
                </label>
                <IconButton
                  onClick={() => removeItem(item)}
                  size="sm"
                  title={`Remove ${item.name}`}
                  variant="ghost"
                >
                  <Trash2 aria-hidden="true" size={14} />
                </IconButton>
              </div>
            ))}
            <div className="ctv-settings-flush-footer ctv-collections-weight-footer">
              <span className="ctv-collections-weight-hint">
                Weights set each source&rsquo;s share of airtime under the <strong>Weighted Shuffle</strong> playback
                order (set on the schedule item). Equal weights = fair share.
              </span>
              <span className="ctv-channels-spacer" />
              <Button
                disabled={alreadyFairShare}
                onClick={resetToFairShare}
                size="sm"
                variant="ghost"
              >
                Reset to fair share
              </Button>
            </div>
          </>
        )}
      </Card>

      <ConfirmDialog
        cancelLabel="Keep editing"
        confirmLabel="Reload"
        message="This was changed elsewhere since you opened it. Reload to get the latest — your unsaved changes will be discarded."
        onCancel={() => setConflictOpen(false)}
        onConfirm={reloadAfterConflict}
        open={conflictOpen}
        title="Multi-collection changed elsewhere"
        tone="danger"
      />
    </div>
  );
}

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

export function MultiCollectionsScreen() {
  const { refresh, state } = useMultiCollectionsData();
  const [editing, setEditing] = useState<{ kind: 'new' } | { kind: 'edit'; item: MultiCollection } | null>(null);
  const [deleteTarget, setDeleteTarget] = useState<MultiCollection | null>(null);
  const [deleteBusy, setDeleteBusy] = useState(false);
  const [deleteError, setDeleteError] = useState<string | null>(null);

  // TopBar "Add Multi-Collection" primary action.
  usePrimaryAction('multiCollections', () => setEditing({ kind: 'new' }));

  if (editing) {
    return (
      <MultiCollectionEditor
        initial={editing.kind === 'edit' ? editing.item : null}
        onBack={() => setEditing(null)}
        onSaved={() => {
          setEditing(null);
          refresh(true);
        }}
      />
    );
  }

  if (state.status === 'loading') {
    return (
      <div className="ctv-collections-loading" role="status">
        <Spinner size={18} />
        <span>Loading multi-collections…</span>
      </div>
    );
  }

  if (state.status === 'error') {
    return (
      <div className="ctv-collections">
        <div className="ctv-channels-error" role="alert">
          <TriangleAlert aria-hidden="true" size={15} />
          <span>{state.error}</span>
          <span className="ctv-channels-spacer" />
          <Button onClick={() => refresh()} size="sm" variant="secondary">
            Retry
          </Button>
        </div>
      </div>
    );
  }

  const multiCollections = [...state.data].sort((a, b) =>
    (a.name ?? '').localeCompare(b.name ?? '', undefined, { sensitivity: 'base' })
  );

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

    setDeleteBusy(true);
    setDeleteError(null);

    try {
      await deleteMultiCollection(deleteTarget.id);
      setDeleteTarget(null);
      refresh(true);
    } catch (error) {
      setDeleteError(messageFromMultiCollectionError(error, 'Unable to delete multi-collection'));
    } finally {
      setDeleteBusy(false);
    }
  };

  return (
    <div className="ctv-collections">
      <div className="ctv-channels-actionbar">
        <Badge tone="neutral">{multiCollections.length} multi-collections</Badge>
        <span className="ctv-channels-spacer" />
        <Button onClick={() => setEditing({ kind: 'new' })} size="sm" startIcon={<Plus aria-hidden="true" size={14} />}>
          New multi-collection
        </Button>
      </div>

      <Card padded={false}>
        {multiCollections.length === 0 ? (
          <div className="ctv-collections-empty">No multi-collections yet.</div>
        ) : (
          multiCollections.map((collection, index) => (
            <div
              className="ctv-settings-flush-row"
              key={collection.id}
              style={index === 0 ? { borderTop: 'none' } : undefined}
            >
              <Layers aria-hidden="true" color="var(--ctv-accent)" size={15} />
              <button
                className="ctv-settings-flush-row-main ctv-settings-flush-row-title ctv-collections-linkbtn"
                onClick={() => setEditing({ item: collection, kind: 'edit' })}
                type="button"
              >
                {collection.name}
              </button>
              <Badge tone="neutral">
                {collection.items.length} item{collection.items.length === 1 ? '' : 's'}
              </Badge>
              <IconButton onClick={() => setDeleteTarget(collection)} size="sm" title="Delete" variant="ghost">
                <Trash2 aria-hidden="true" size={14} />
              </IconButton>
            </div>
          ))
        )}
      </Card>

      <ConfirmDialog
        busy={deleteBusy}
        confirmLabel="Delete"
        message={
          deleteTarget ? (
            <>
              <span>{`Delete "${deleteTarget.name ?? 'this multi-collection'}"? 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 multi-collection"
        tone="danger"
      />
    </div>
  );
}
