import { useEffect, useMemo, useReducer, useRef, useState } from "react";
import type { KeyboardEvent, MouseEvent, RefObject } from "react";

import clsx from "clsx";
import { Hex as HexColor } from "colorlib";
import {
  Check,
  CheckCheck,
  ChevronsLeft,
  ChevronsRight,
  CircleDashed,
  Copy,
  Crosshair,
  Dot,
  GripVertical,
  Pencil,
  Plus,
  Redo2,
  RefreshCw,
  RefreshCwOff,
  Trash2,
  Undo2,
  X,
} from "lucide-react";
import { motion } from "motion/react";

import type { ContrastToken } from "@/hooks/contrast";
import { luminanceFromHex, useContrastToken } from "@/hooks/contrast";
import { useDragAndDrop } from "@/hooks/dragAndDrop";
import { extractHexValue, formatHexString } from "@/hooks/hex";
import {
  createPaletteCardActions,
  paletteCardReducer,
} from "@/hooks/paletteCard";
import type {
  PaletteCard,
  PaletteCardActions,
  PaletteCardState,
  PaletteColor,
  PaletteMode,
} from "@/hooks/paletteCard";
import {
  loadCards,
  saveActiveCardId,
  saveCards,
  serializeCard,
} from "@/hooks/storage";
import { randomId } from "@/util";

import styles from "./PaletteEditor.module.css";

type Timeout = ReturnType<typeof setTimeout>;

const SYNC_DELAY = 2000;
const DEFAULT_BG = HexColor.from_code("f6f6f6");

function defaultPaletteCard(): PaletteCardState {
  const defaultCard = {
    id: randomId(),
    name: "New Palette",
    colors: [],
    selectedColorIds: [],
  };

  return {
    present: defaultCard,
    history: [],
    future: [],
  };
}

function PaletteEditor({
  pickerColor,
  setPickerColor,
  initialCardState,
}: {
  pickerColor: HexColor;
  setPickerColor: (hex: HexColor) => void;
  initialCardState?: PaletteCardState;
}) {
  const [cardState, dispatch] = useReducer(
    paletteCardReducer,
    initialCardState || defaultPaletteCard(),
  );
  const actions = useMemo(() => createPaletteCardActions(dispatch), [dispatch]);
  const [historyCounter, setHistoryCounter] = useState(0);
  const [mode, setMode] = useState<PaletteMode>("normal");
  const [isSynced, setIsSynced] = useState(false);
  const snapshotRef = useRef<PaletteCard | null>(null);
  const timerRef = useRef<Timeout | null>(null);

  const incrementHistoryCounter = () => {
    setHistoryCounter((prev) => prev + 1);
  };

  useEffect(() => {
    saveActiveCardId(cardState.present.id);
  }, []);

  useEffect(() => {
    const cards = loadCards();
    cards[cardState.present.id] = serializeCard(cardState.present);
    saveCards(cards);
  }, [cardState.present]);

  return (
    <div className={styles.paletteEditor} data-cy="palette-editor">
      <ActionBar
        mode={mode}
        setMode={setMode}
        actions={actions}
        hasSelection={cardState.present.selectedColorIds.length > 0}
        canUndo={cardState.history.length > 0}
        canRedo={cardState.future.length > 0}
        isSynced={isSynced}
        incrementHistoryCounter={incrementHistoryCounter}
        snapshotRef={snapshotRef}
        syncTimerRef={timerRef}
      />
      <PaletteCard
        pickerColor={pickerColor}
        setPickerColor={setPickerColor}
        cardState={cardState.present}
        actions={actions}
        mode={mode}
        isSynced={isSynced}
        setIsSynced={setIsSynced}
        snapshotRef={snapshotRef}
        syncTimerRef={timerRef}
        historyCounter={historyCounter}
      />
    </div>
  );
}

function ActionBar({
  mode,
  setMode,
  actions,
  hasSelection,
  canUndo,
  canRedo,
  isSynced,
  incrementHistoryCounter,
  snapshotRef,
  syncTimerRef,
}: {
  mode: PaletteMode;
  setMode: (mode: PaletteMode) => void;
  actions: PaletteCardActions;
  hasSelection: boolean;
  canUndo: boolean;
  canRedo: boolean;
  isSynced: boolean;
  incrementHistoryCounter: () => void;
  snapshotRef: RefObject<PaletteCard | null>;
  syncTimerRef: RefObject<Timeout | null>;
}) {
  const clearSyncTimeout = () => {
    if (syncTimerRef.current) clearTimeout(syncTimerRef.current);
    snapshotRef.current = null;
  };

  const handleUndo = () => {
    if (isSynced) clearSyncTimeout();
    incrementHistoryCounter();
    actions.undo();
  };

  const handleRedo = () => {
    if (isSynced) clearSyncTimeout();
    incrementHistoryCounter();
    actions.redo();
  };

  const handleModeChange = (next: PaletteMode) => {
    if (mode === "normal") {
      if (isSynced && syncTimerRef.current) {
        clearTimeout(syncTimerRef.current);
        if (snapshotRef.current) {
          actions.commitToHistory(snapshotRef.current);
          snapshotRef.current = null;
        }
      }
    }
    actions.clearSelection();
    setMode(next);
  };

  return (
    <div className={styles.actionBar} data-cy="action-bar">
      <button
        className={clsx(styles.actionButton, styles.iconButton)}
        data-cy="undo"
        disabled={!canUndo}
        onClick={handleUndo}
        title="Undo"
      >
        <Undo2 size={16} />
      </button>
      <button
        className={clsx(styles.actionButton, styles.iconButton)}
        data-cy="redo"
        disabled={!canRedo}
        onClick={handleRedo}
        title="Redo"
      >
        <Redo2 size={16} />
      </button>
      <button
        className={clsx(styles.actionButton, styles.iconButton)}
        data-cy="add"
        onClick={actions.addColor}
        title="Add Color"
      >
        <Plus size={16} />
      </button>
      <button
        className={clsx(styles.actionButton, styles.iconButton)}
        data-cy="delete"
        disabled={!hasSelection}
        onClick={actions.deleteSelectedColors}
        title="Delete Selected"
      >
        <Trash2 size={16} />
      </button>
      <button
        className={clsx(styles.actionButton, styles.iconButton)}
        data-cy="duplicate"
        disabled={!hasSelection}
        onClick={actions.duplicateSelectedColors}
        title="Duplicate Selected"
      >
        <Copy size={16} />
      </button>
      <button
        className={clsx(styles.actionButton, styles.wordButton, {
          [styles.activeButton]: mode === "reorder",
        })}
        data-cy="reorder"
        aria-pressed={mode === "reorder"}
        onClick={() =>
          handleModeChange(mode === "reorder" ? "normal" : "reorder")
        }
        title="Reorder Colors"
      >
        Reorder
      </button>
      <button
        className={clsx(styles.actionButton, styles.wordButton, {
          [styles.activeButton]: mode === "select",
        })}
        data-cy="select"
        aria-pressed={mode === "select"}
        onClick={() =>
          handleModeChange(mode === "select" ? "normal" : "select")
        }
        title="Select Multiple"
      >
        Select
      </button>
      {mode === "select" && (
        <>
          <button
            className={clsx(styles.actionButton, styles.iconButton)}
            data-cy="select-all"
            onClick={actions.selectAll}
            title="Select All"
          >
            <CheckCheck size={16} />
          </button>
          <button
            className={clsx(styles.actionButton, styles.iconButton)}
            data-cy="clear"
            onClick={actions.clearSelection}
            title="Clear Selections"
          >
            <X size={16} />
          </button>
        </>
      )}
    </div>
  );
}

function PaletteCard({
  pickerColor,
  setPickerColor,
  cardState,
  actions,
  mode,
  isSynced,
  setIsSynced,
  snapshotRef,
  syncTimerRef,
  historyCounter,
}: {
  pickerColor: HexColor;
  setPickerColor: (hex: HexColor) => void;
  cardState: PaletteCard;
  actions: PaletteCardActions;
  mode: PaletteMode;
  isSynced: boolean;
  setIsSynced: (v: boolean) => void;
  snapshotRef: RefObject<PaletteCard | null>;
  syncTimerRef: RefObject<Timeout | null>;
  historyCounter: number;
}) {
  const selectedColor =
    mode === "select"
      ? null
      : cardState.selectedColorIds.length === 1
        ? cardState.colors.find((c) => c.id === cardState.selectedColorIds[0])
        : null;
  const wasSyncedRef = useRef(false);
  const pickerColorValue = pickerColor.to_code();

  // when sync toggles on, set picker <- palette
  useEffect(() => {
    if (!isSynced) {
      wasSyncedRef.current = false;
      return;
    }

    // handle toggle on
    if (!wasSyncedRef.current) {
      wasSyncedRef.current = true;

      if (syncTimerRef.current) {
        clearTimeout(syncTimerRef.current);
        syncTimerRef.current = null;
        if (snapshotRef.current) {
          actions.commitToHistory(snapshotRef.current);
          snapshotRef.current = null;
        }
      }

      if (selectedColor) setPickerColor(selectedColor.hex);
    }
  }, [selectedColor?.id, isSynced]);

  // during sync, set picker -> palette
  useEffect(() => {
    if (!isSynced || !selectedColor || !wasSyncedRef.current) return;

    if (!snapshotRef.current) {
      snapshotRef.current = cardState; // capture pre-change state once
    }

    actions.setColorValueSilent(selectedColor.id, pickerColor);

    if (syncTimerRef.current) clearTimeout(syncTimerRef.current);
    syncTimerRef.current = setTimeout(() => {
      actions.commitToHistory(snapshotRef.current!);
      snapshotRef.current = null;
    }, SYNC_DELAY);
  }, [pickerColorValue]);

  // when selection changes, set picker <- palette
  useEffect(() => {
    if (!isSynced || !selectedColor) return;

    if (syncTimerRef.current) {
      clearTimeout(syncTimerRef.current);
      syncTimerRef.current = null;
      if (snapshotRef.current) {
        snapshotRef.current = null;
      }
    }

    setPickerColor(selectedColor.hex);
  }, [selectedColor?.id]);

  // undo/redo, set picker <- palette
  useEffect(() => {
    if (!isSynced || !selectedColor || !wasSyncedRef.current) return;

    setPickerColor(selectedColor.hex);
  }, [historyCounter]);

  return (
    <div className={styles.cardWrapper} data-cy="palette-card">
      <SyncButton isSynced={isSynced} setIsSynced={setIsSynced} />
      <CardHeader name={cardState.name} onNameChange={actions.setCardName} />
      <PickerColor
        pickerColor={pickerColor}
        paletteColorId={selectedColor?.id || null}
        setPaletteColor={actions.setColorValue}
        isSynced={isSynced}
      />
      <PaletteColor
        selectedColor={selectedColor?.hex || null}
        setPickerColor={setPickerColor}
        isSynced={isSynced}
      />
      <Palette cardState={cardState} actions={actions} mode={mode} />
    </div>
  );
}

function SyncButton({
  isSynced,
  setIsSynced,
}: {
  isSynced: boolean;
  setIsSynced: (v: boolean) => void;
}) {
  return (
    <div
      className={styles.sync}
      data-cy="sync"
      onClick={() => setIsSynced(!isSynced)}
      aria-pressed={isSynced}
      style={{
        color: isSynced ? "#292929" : "#7a7a7a",
      }}
      title={isSynced ? "Unsync Picker & Palette" : "Sync Picker and Palette"}
    >
      <span className={styles.leftSpan}>Picker</span>
      <span className={styles.middleSpan}>
        {isSynced ? <RefreshCw size={22} /> : <RefreshCwOff size={22} />}
      </span>
      <span className={styles.rightSpan}>Palette</span>
    </div>
  );
}

function CardHeader({
  name,
  onNameChange,
}: {
  name: string;
  onNameChange: (name: string) => void;
}) {
  return (
    <div className={styles.cardHeader} data-cy="card-header">
      <EditableField
        testID="card-name"
        value={name}
        setValue={onNameChange}
        buttonSize={12}
      />
    </div>
  );
}

function PickerColor({
  pickerColor,
  paletteColorId,
  setPaletteColor,
  isSynced,
}: {
  pickerColor: HexColor;
  paletteColorId: string | null;
  setPaletteColor: (id: string, hex: HexColor) => void;
  isSynced: boolean;
}) {
  const arrowToken = useContrastToken(() => luminanceFromHex(pickerColor));

  const handleClick = () => {
    if (!isSynced && paletteColorId) {
      setPaletteColor(paletteColorId, pickerColor);
    }
  };

  return (
    <div
      className={clsx(styles.previewPane, styles.pickerColor)}
      data-cy="picker-preview"
      style={{
        cursor: isSynced ? "unset" : "pointer",
        backgroundColor: formatHexString(pickerColor),
      }}
      onClick={handleClick}
      title={!isSynced ? "Send to Picker" : ""}
    >
      {!isSynced && (
        <div
          data-cy="picker-color-arrow"
          className={clsx(styles.arrowIndicator, {
            [styles.arrowIndicatorDark]: arrowToken === "dark",
            [styles.arrowIndicatorLight]: arrowToken === "light",
          })}
        >
          <ChevronsRight size={32} />
        </div>
      )}
    </div>
  );
}

function PaletteColor({
  selectedColor,
  setPickerColor,
  isSynced,
}: {
  selectedColor: HexColor | null;
  setPickerColor: (hex: HexColor) => void;
  isSynced: boolean;
}) {
  const bgColor = selectedColor || DEFAULT_BG;
  const arrowToken = useContrastToken(() => luminanceFromHex(bgColor));

  const handleClick = () => {
    if (!isSynced && selectedColor) {
      setPickerColor(selectedColor);
    }
  };

  return (
    <div
      className={clsx(styles.previewPane, styles.paletteColor)}
      data-cy="selected-preview"
      style={{
        cursor: isSynced ? "unset" : "pointer",
        backgroundColor: formatHexString(bgColor),
      }}
      onClick={handleClick}
      title={!isSynced ? "Send to Palette" : ""}
    >
      {!isSynced && (
        <div
          data-cy="palette-color-arrow"
          className={clsx(styles.arrowIndicator, {
            [styles.arrowIndicatorDark]: arrowToken === "dark",
            [styles.arrowIndicatorLight]: arrowToken === "light",
          })}
        >
          <ChevronsLeft size={32} />
        </div>
      )}
    </div>
  );
}

function Palette({
  cardState,
  actions,
  mode,
}: {
  cardState: PaletteCard;
  actions: PaletteCardActions;
  mode: PaletteMode;
}) {
  const {
    containerRef,
    setItemRef,
    isDragging,
    // sourceIndex,
    targetIndex,
    previewItems,
  } = useDragAndDrop({
    items: cardState.colors,
    handleReorder: actions.reorderColors,
    disabled: mode !== "reorder",
  });

  const handleNormalClick = (color: PaletteColor) => {
    const ids = cardState.selectedColorIds;
    const isSelected = ids.includes(color.id);
    console.log(color.id, isSelected);
    if (isSelected) {
      actions.setSelectedColors([]);
    } else {
      actions.setSelectedColors([color.id]);
    }
  };

  const handleSelectClick = (color: PaletteColor) => {
    const ids = cardState.selectedColorIds;
    const next = ids.includes(color.id)
      ? ids.filter((id) => id !== color.id)
      : [...ids, color.id];
    actions.setSelectedColors(next);
  };

  const onRowClick =
    mode === "normal"
      ? handleNormalClick
      : mode === "select"
        ? handleSelectClick
        : undefined;

  const displayColors = isDragging ? previewItems : cardState.colors;

  return (
    <div className={styles.palette} data-cy="palette" ref={containerRef}>
      {displayColors.length > 0 ? (
        displayColors.map((color, index) => (
          <PaletteRow
            key={color.id}
            color={color}
            index={index}
            isSelected={cardState.selectedColorIds.includes(color.id)}
            isEditable={
              mode === "normal" && cardState.selectedColorIds[0] === color.id
            }
            isDragging={isDragging}
            mode={mode}
            actions={actions}
            onRowClick={onRowClick}
            setItemRef={setItemRef}
            targetIndex={targetIndex}
          />
        ))
      ) : (
        <span style={{ margin: 8 }}>
          No colors in palette. Press{" "}
          <Plus size={16} style={{ transform: "translateY(2px)" }} /> in the
          toolbar above to add one.
        </span>
      )}
    </div>
  );
}

function PaletteRow({
  color,
  index,
  isSelected,
  isEditable,
  isDragging,
  mode,
  actions,
  onRowClick,
  setItemRef,
  targetIndex,
}: {
  color: PaletteColor;
  index: number;
  isSelected: boolean;
  isEditable: boolean;
  isDragging: boolean;
  mode: PaletteMode;
  actions: PaletteCardActions;
  onRowClick?: (color: PaletteColor, e: MouseEvent<HTMLDivElement>) => void;
  setItemRef: (el: HTMLElement | null, id: string) => void;
  targetIndex: number;
}) {
  const isNormalMode = mode === "normal";
  const isSelectMode = mode === "select";
  const isReorderMode = mode === "reorder";

  const Wrapper = isReorderMode ? motion.div : "div";
  const motionProps = isReorderMode
    ? { layout: true, transition: { duration: 0.25 } }
    : {};

  const token = useContrastToken(() => luminanceFromHex(color.hex));

  return (
    <Wrapper
      className={clsx(styles.paletteRowWrapper, {
        [styles.draggable]: isReorderMode,
        [styles.dragging]: isDragging && index === targetIndex,
        [styles.multiSelected]: isSelectMode && isSelected,
      })}
      ref={(el) => setItemRef(el, color.id)}
      data-cy={`palette-row-${index}-wrapper`}
      data-item-id={color.id}
      onClick={(e) => onRowClick?.(color, e)}
      {...motionProps}
    >
      <div
        className={styles.paletteRow}
        data-cy={`palette-row-${index}`}
        aria-selected={isSelected}
        style={{
          backgroundColor: formatHexString(color.hex),
        }}
      >
        {isSelectMode && (
          <span
            className={clsx(styles.modeDecorator, styles.checkDecorator, {
              [styles.checkDecoratorSelected]: isSelected,
              [styles.checkDecoratorDark]: token === "dark",
              [styles.checkDecoratorLight]: token === "light",
            })}
          >
            {isSelected && <Check size={18} strokeWidth={3} />}
          </span>
        )}
        {mode === "reorder" && (
          <span
            className={clsx(styles.modeDecorator, {
              [styles.gripDark]: token === "dark",
              [styles.gripLight]: token === "light",
            })}
          >
            <GripVertical size={24} />
          </span>
        )}
        {isNormalMode &&
          (isSelected ? (
            <Crosshair
              className={clsx(styles.selectedIndicator, {
                [styles.indicatorDark]: token === "dark",
                [styles.indicatorLight]: token === "light",
              })}
              size={27}
            >
              <Dot />
            </Crosshair>
          ) : (
            <CircleDashed
              className={clsx(styles.targettedIndicator, {
                [styles.indicatorDark]: token === "dark",
                [styles.indicatorLight]: token === "light",
              })}
              size={32}
            />
          ))}
        <div className={styles.paletteRowData}>
          <div className={styles.colorName}>
            {isEditable ? (
              <EditableField
                testID={`palette-row-name-${index}`}
                value={color.name}
                setValue={(newName: string) =>
                  actions.setColorName(color.id, newName)
                }
                buttonSize={12}
                contrastToken={token}
                reset={!isSelected}
              />
            ) : (
              <div>
                <span
                  data-cy={`palette-row-name-${index}`}
                  className={clsx(styles.field, {
                    [styles.fieldDark]: token === "dark",
                    [styles.fieldLight]: token === "light",
                  })}
                >
                  {color.name}
                </span>
              </div>
            )}
          </div>
          <div className={styles.colorHex}>
            {isEditable ? (
              <EditableField
                testID={`palette-row-hex-${index}`}
                value={color.hex.to_code()}
                setValue={(newHex: string) =>
                  actions.setColorValue(color.id, HexColor.from_code(newHex))
                }
                buttonSize={12}
                contrastToken={token}
                reset={!isSelected}
                validate={(raw: string) => extractHexValue(raw)}
                render={(raw: string) => `#${raw}`}
              />
            ) : (
              <div className={styles.fieldWrapper}>
                <span
                  data-cy={`palette-row-hex-${index}`}
                  className={clsx(styles.field, {
                    [styles.fieldDark]: token === "dark",
                    [styles.fieldLight]: token === "light",
                  })}
                >
                  {formatHexString(color.hex)}
                </span>
              </div>
            )}
          </div>
        </div>
      </div>
    </Wrapper>
  );
}

function EditableField({
  testID,
  value,
  setValue,
  buttonSize,
  contrastToken,
  reset,
  validate,
  render,
}: {
  testID: string;
  value: string;
  setValue: (v: string) => void;
  buttonSize: number;
  contrastToken?: ContrastToken;
  reset?: boolean;
  validate?: (raw: string) => string | null;
  render?: (raw: string) => string;
}) {
  const [isEditing, setIsEditing] = useState(false);
  const spanRef = useRef<HTMLSpanElement>(null);

  useEffect(() => {
    if (reset) setIsEditing(false);
  }, [reset]);

  useEffect(() => {
    // return if not editing or not rendered
    if (!isEditing || !spanRef.current) return;

    // set span content
    spanRef.current.textContent = render ? render(value) : value;

    // focus span
    spanRef.current.focus();

    // select contents
    const range = document.createRange();
    range.selectNodeContents(spanRef.current);
    window.getSelection()?.removeAllRanges();
    window.getSelection()?.addRange(range);
  }, [isEditing, value, render]);

  const onConfirm = () => {
    const raw = spanRef.current?.textContent ?? "";
    const validated = validate ? validate(raw) : raw;
    if (validated) setValue(validated);
    setIsEditing(false);
  };

  const onCancel = () => {
    window.getSelection()?.removeAllRanges();
    setIsEditing(false);
  };

  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === "Escape") {
      e.preventDefault();
      onCancel();
    } else if (e.key === "Enter") {
      e.preventDefault();
      onConfirm();
    }
  };

  return (
    <div className={styles.editableFieldWrapper}>
      <div className={styles.valueWrapper}>
        <span
          ref={spanRef}
          data-cy={isEditing ? `${testID}-input` : testID}
          className={clsx(styles.editableField, {
            [styles.editingField]: isEditing,
            [styles.editableFieldDark]: contrastToken === "dark",
            [styles.editableFieldLight]: contrastToken === "light",
          })}
          contentEditable={isEditing}
          suppressContentEditableWarning
          onClick={(e) => e.stopPropagation()}
          onKeyDown={isEditing ? handleKeyDown : undefined}
        >
          {!isEditing && (render ? render(value) : value)}
        </span>
      </div>
      {isEditing ? (
        <div>
          <button
            data-cy={`${testID}-cancel`}
            className={clsx(styles.editableFieldButton, {
              [styles.editableFieldButtonDark]: contrastToken === "dark",
              [styles.editableFieldButtonLight]: contrastToken === "light",
            })}
            onClick={(e) => {
              e.stopPropagation();
              onCancel();
            }}
            title="Cancel"
          >
            <X size={buttonSize} strokeWidth={3} />
          </button>
          <button
            data-cy={`${testID}-confirm`}
            className={clsx(styles.editableFieldButton, {
              [styles.editableFieldButtonDark]: contrastToken === "dark",
              [styles.editableFieldButtonLight]: contrastToken === "light",
            })}
            onClick={(e) => {
              e.stopPropagation();
              onConfirm();
            }}
            title="Confirm"
          >
            <Check size={buttonSize} strokeWidth={3} />
          </button>
        </div>
      ) : (
        <div>
          <button
            data-cy={`${testID}-edit`}
            className={clsx(styles.editableFieldButton, {
              [styles.editableFieldButtonDark]: contrastToken === "dark",
              [styles.editableFieldButtonLight]: contrastToken === "light",
            })}
            onClick={(e) => {
              e.stopPropagation();
              setIsEditing(true);
            }}
            title="Edit"
          >
            <Pencil size={buttonSize} strokeWidth={3} />
          </button>
        </div>
      )}
    </div>
  );
}

export default PaletteEditor;
