import { useState, useCallback, useRef, useEffect } from "react";
import { useLocalStorageBoolean } from "@/hooks/use-local-storage-boolean";
import type { RowTint } from "@/themes";

type CollapsiblePanelProps = {
  title: string;
  storageKey: string;
  defaultOpen?: boolean;
  headerRight?: React.ReactNode;
  /** Action element rendered at the right side of the header (e.g. "+" button). Click events are stopped from toggling the panel. */
  headerAction?: React.ReactNode;
  /** Override the default content padding classes. */
  contentClassName?: string;
  /** Called after the panel is toggled. */
  onToggle?: (isOpen: boolean) => void;
  /** Optional row tint for background color. */
  tint?: RowTint | null;
  /** When true, the tint is applied to the header only while the panel is collapsed. */
  tintOnlyWhenCollapsed?: boolean;
  /** When true, renders a drag handle at the bottom and persists user-set height to localStorage. */
  resizable?: boolean;
  /** Initial open height in pixels when no persisted value exists. Default 200 (matches legacy max-height). */
  defaultHeight?: number;
  /** Floor for drag-resize in pixels. Default 80. */
  minHeight?: number;
  /** Ceiling for drag-resize — number (pixels) or `calc(100vh - Npx)` string. Default `'calc(100vh - 120px)'`. */
  maxHeight?: number | string;
  /** Panel body height in pixels used on mobile single-row layouts. When set, the drag handle is hidden on coarse-pointer / narrow viewports and the content area uses this height. Default 56. */
  mobileHeight?: number;
  /** When set, renders a small corner element at the right edge of the drag-handle row.
   *  The corner's pointerdown invokes the internal horizontal-drag start, then calls this
   *  callback (which starts a vertical drag in the parent), then overrides the body cursor
   *  to `nwse-resize` so the diagonal cursor wins over the two axis cursors. Only rendered
   *  when the internal `showDragHandle` condition is true (resizable + open + !mobile). */
  onCornerPointerDown?: (e: React.PointerEvent<HTMLDivElement>) => void;
  children: React.ReactNode;
};

function readPersistedHeight(key: string): number | null {
  try {
    const stored = localStorage.getItem(key);
    if (stored == null) return null;
    const n = parseInt(stored, 10);
    if (!Number.isFinite(n)) return null;
    return n;
  } catch {
    return null;
  }
}

function writePersistedHeight(key: string, height: number): void {
  try {
    localStorage.setItem(key, String(Math.round(height)));
  } catch {
    // localStorage unavailable
  }
}

/** Resolve `maxHeight` (number | `calc(100vh - Npx)` | other) to a pixel value. */
function resolveMaxHeight(maxHeight: number | string): number {
  if (typeof maxHeight === "number") return maxHeight;
  const m = /^\s*calc\(\s*100vh\s*-\s*(\d+)px\s*\)\s*$/.exec(maxHeight);
  if (m) return Math.max(0, window.innerHeight - parseInt(m[1], 10));
  return Math.max(0, window.innerHeight - 120);
}

/** Subscribe to a media query; returns a stable boolean. */
function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(() => {
    if (typeof window === "undefined" || !window.matchMedia) return false;
    return window.matchMedia(query).matches;
  });
  useEffect(() => {
    if (typeof window === "undefined" || !window.matchMedia) return;
    const mq = window.matchMedia(query);
    const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
    if (mq.addEventListener) mq.addEventListener("change", handler);
    else mq.addListener(handler);
    setMatches(mq.matches);
    return () => {
      if (mq.removeEventListener) mq.removeEventListener("change", handler);
      else mq.removeListener(handler);
    };
  }, [query]);
  return matches;
}

export function CollapsiblePanel({
  title,
  storageKey,
  defaultOpen = true,
  headerRight,
  headerAction,
  contentClassName,
  onToggle,
  tint,
  resizable = false,
  defaultHeight = 200,
  minHeight = 80,
  maxHeight = "calc(100vh - 120px)",
  mobileHeight = 56,
  tintOnlyWhenCollapsed = false,
  onCornerPointerDown,
  children,
}: CollapsiblePanelProps) {
  const [isOpen, setIsOpen] = useLocalStorageBoolean(storageKey, defaultOpen);
  const contentRef = useRef<HTMLDivElement>(null);

  // Hide the drag handle + use fixed mobile height on coarse pointer or narrow viewport.
  const isMobile = useMediaQuery("(pointer: coarse), (max-width: 639px)");

  const heightStorageKey = `${storageKey}-height`;
  const [height, setHeight] = useState<number>(() => {
    if (!resizable) return defaultHeight;
    const persisted = readPersistedHeight(heightStorageKey);
    if (persisted == null) return defaultHeight;
    // Reject persisted values outside [minHeight, maxHeight] — fall back to defaultHeight.
    // `resolveMaxHeight` reads `window.innerHeight` for `calc(100vh - Npx)` strings.
    const ceiling = typeof window !== "undefined" ? resolveMaxHeight(maxHeight) : Number.MAX_SAFE_INTEGER;
    if (persisted < minHeight || persisted > ceiling) return defaultHeight;
    return persisted;
  });

  const toggle = useCallback(() => {
    const next = !isOpen;
    setIsOpen(next);
    onToggle?.(next);
  }, [isOpen, setIsOpen, onToggle]);

  // During transition, keep overflow hidden for smooth animation.
  // Only set transitioning=true on actual user toggles (not initial mount),
  // otherwise transitionend never fires and overflow stays hidden permanently.
  const [transitioning, setTransitioning] = useState(false);
  const hasMounted = useRef(false);

  useEffect(() => {
    if (!contentRef.current) return;
    // Skip initial mount — no transition occurs, so transitionend would never fire
    if (!hasMounted.current) {
      hasMounted.current = true;
      return;
    }
    setTransitioning(true);
    const el = contentRef.current;
    const handler = () => setTransitioning(false);
    el.addEventListener("transitionend", handler);
    return () => el.removeEventListener("transitionend", handler);
  }, [isOpen]);

  // ---- Drag-to-resize (desktop only) ----
  const dragStateRef = useRef<{ startY: number; startHeight: number } | null>(null);

  const clampHeight = useCallback(
    (h: number) => {
      const ceiling = resolveMaxHeight(maxHeight);
      return Math.max(minHeight, Math.min(ceiling, h));
    },
    [minHeight, maxHeight],
  );

  const onPointerMove = useCallback(
    (e: PointerEvent) => {
      const st = dragStateRef.current;
      if (!st || !contentRef.current) return;
      e.preventDefault();
      const next = clampHeight(st.startHeight + (e.clientY - st.startY));
      // Live update via direct style mutation (avoid setState-per-mousemove layout thrash).
      contentRef.current.style.height = `${next}px`;
    },
    [clampHeight],
  );

  const onPointerUp = useCallback(
    (e: PointerEvent) => {
      // Clear the document-level cursor override set in onHandlePointerDown. Done
      // first so an early return below (no active drag state) still releases the
      // cursor — though in practice onPointerUp only fires when a drag is live.
      document.body.style.cursor = "";
      const st = dragStateRef.current;
      if (!st || !contentRef.current) return;
      const final = clampHeight(st.startHeight + (e.clientY - st.startY));
      dragStateRef.current = null;
      document.removeEventListener("pointermove", onPointerMove);
      document.removeEventListener("pointerup", onPointerUp);
      // Restore the CSS transition (suppressed during drag to prevent the 150ms
      // height animation from chasing each pointermove — see onHandlePointerDown).
      contentRef.current.style.transition = "";
      setHeight(final);
      writePersistedHeight(heightStorageKey, final);
    },
    [clampHeight, heightStorageKey, onPointerMove],
  );

  const onHandlePointerDown = useCallback(
    (e: React.PointerEvent<HTMLDivElement>) => {
      if (!resizable || !contentRef.current) return;
      e.preventDefault();
      dragStateRef.current = {
        startY: e.clientY,
        startHeight: contentRef.current.getBoundingClientRect().height,
      };
      // Disable the height transition during drag so direct style mutations
      // in onPointerMove snap immediately instead of animating toward each value.
      contentRef.current.style.transition = "none";
      // Force the drag cursor at the document level for the duration of the drag.
      // Without this, the pointer can leave the 3px handle mid-drag and the browser
      // reverts to the default cursor (implicit pointer capture loss on thin handles).
      // Cleared in onPointerUp.
      document.body.style.cursor = "row-resize";
      document.addEventListener("pointermove", onPointerMove);
      document.addEventListener("pointerup", onPointerUp);
    },
    [resizable, onPointerMove, onPointerUp],
  );

  // Cleanup dangling listeners on unmount. Also clear the body cursor in case the
  // component unmounts mid-drag (navigation, hot-reload, error boundary) — otherwise
  // the `row-resize` / `nwse-resize` cursor would leak across the rest of the UI.
  useEffect(() => {
    return () => {
      document.removeEventListener("pointermove", onPointerMove);
      document.removeEventListener("pointerup", onPointerUp);
      document.body.style.cursor = "";
    };
  }, [onPointerMove, onPointerUp]);

  // ---- Height-style resolution ----
  // Legacy mode (resizable=false): max-height transition — open clamps to `${defaultHeight}px`, closed to 0.
  // Resizable desktop: inline height set to the user's value; closed animates to 0.
  // Resizable mobile: fixed mobileHeight; drag handle hidden.
  const legacyMode = !resizable;
  const effectiveResizableHeight = isMobile ? mobileHeight : height;

  const contentStyle = legacyMode
    ? {
        maxHeight: isOpen ? `${defaultHeight}px` : "0px",
        // Legacy mode keeps overflow visible so descendant popovers (e.g. SwatchPopover) can paint outside the panel.
        overflow: (transitioning || !isOpen ? "hidden" : "visible") as React.CSSProperties["overflow"],
      }
    : {
        height: isOpen ? `${effectiveResizableHeight}px` : "0px",
        // Resizable mode clips Y to `auto` (scrolls internally) and X to `hidden` so grid
        // content doesn't bleed into the panels below or cause horizontal overflow.
        overflowY: (transitioning || !isOpen ? "hidden" : "auto") as React.CSSProperties["overflowY"],
        overflowX: "hidden" as React.CSSProperties["overflowX"],
      };

  const transitionClass = legacyMode
    ? "transition-[max-height] duration-150 ease-in-out"
    : "transition-[height] duration-150 ease-in-out";

  const showDragHandle = resizable && isOpen && !isMobile;

  // Header tint is applied unconditionally, or only while collapsed when
  // `tintOnlyWhenCollapsed` is set — header-color as a "what's inside" hint
  // that doesn't double-up with the body while the panel is open.
  const headerTint = tint && (!tintOnlyWhenCollapsed || !isOpen) ? tint : null;
  // When the collapsed header stands in for a selected item (tintOnlyWhenCollapsed),
  // paint the selected shade and stay flat on hover — tint.hover (22%) is less
  // saturated than tint.selected (32%) and would read as an inverted hover.
  const headerBg = headerTint ? (tintOnlyWhenCollapsed ? headerTint.selected : headerTint.base) : null;
  const headerHoverBg = headerTint && !tintOnlyWhenCollapsed ? headerTint.hover : null;

  return (
    <div className="border-t-[3px] border-border first:border-t-0">
      {/* Header — always visible */}
      <div
        className="flex items-center gap-1.5 w-full px-1.5 sm:px-2 py-1 text-xs text-text-secondary shrink-0 transition-colors"
        style={headerBg ? { backgroundColor: headerBg } : undefined}
        onMouseEnter={headerHoverBg && headerBg ? (e) => { (e.currentTarget as HTMLElement).style.backgroundColor = headerHoverBg; } : undefined}
        onMouseLeave={headerHoverBg && headerBg ? (e) => { (e.currentTarget as HTMLElement).style.backgroundColor = headerBg; } : undefined}
      >
        <button
          type="button"
          className="flex items-center gap-1.5 flex-1 min-w-0 hover:text-text-primary transition-colors"
          onClick={toggle}
          aria-expanded={isOpen}
        >
          {/* Chevron */}
          <span
            className="inline-block transition-transform duration-150"
            style={{ transform: isOpen ? "rotate(0deg)" : "rotate(-90deg)" }}
            aria-hidden="true"
          >
            &#x25BC;
          </span>
          <span className="font-bold uppercase tracking-wide">{title}</span>
          {headerRight && (
            <span className="ml-auto flex items-center gap-1 min-w-0 truncate">
              {headerRight}
            </span>
          )}
        </button>
        {headerAction}
      </div>

      {/* Content area */}
      <div
        ref={contentRef}
        className={transitionClass}
        style={contentStyle}
      >
        <div className={contentClassName ?? "pl-5 pr-1.5 sm:pr-2 pb-1.5"}>
          {children}
        </div>
      </div>

      {/* Drag handle — resizable desktop only. 3px tall, transparent by default
          so the next panel's `border-t-[3px]` is the sole visible inter-panel
          separator. Hovering surfaces the affordance via `bg-text-secondary`.
          When `onCornerPointerDown` is supplied, render as a flex row: the handle
          consumes the remaining width (`flex-1`) and a small fixed-width corner sits
          flush against its right edge. The corner initiates both horizontal and
          vertical drags and overrides the cursor to `nwse-resize`. */}
      {showDragHandle && (
        onCornerPointerDown ? (
          <div className="relative z-10 flex flex-row">
            <div
              role="separator"
              aria-orientation="horizontal"
              aria-label={`Resize ${title} panel`}
              onPointerDown={onHandlePointerDown}
              className="flex-1 h-[3px] bg-transparent hover:bg-text-secondary transition-colors cursor-row-resize select-none"
              style={{ touchAction: "none" }}
            />
            <div
              aria-hidden="true"
              onPointerDown={(e) => {
                // Order matters: the two axis handlers each write their own cursor
                // (`row-resize` then `col-resize`) to `document.body.style.cursor`,
                // then the final write here (`nwse-resize`) wins.
                onHandlePointerDown(e);
                onCornerPointerDown(e);
                document.body.style.cursor = "nwse-resize";
              }}
              className="w-[7px] h-[3px] bg-transparent hover:bg-text-secondary transition-colors select-none cursor-nwse-resize"
              style={{ touchAction: "none" }}
            />
          </div>
        ) : (
          <div
            role="separator"
            aria-orientation="horizontal"
            aria-label={`Resize ${title} panel`}
            onPointerDown={onHandlePointerDown}
            className="relative z-10 h-[3px] bg-transparent hover:bg-text-secondary transition-colors cursor-row-resize select-none"
            style={{ touchAction: "none" }}
          />
        )
      )}
    </div>
  );
}
