"use client";

import type {
  ButtonHTMLAttributes,
  ChangeEvent,
  CSSProperties,
  InputHTMLAttributes,
  KeyboardEvent as ReactKeyboardEvent,
  ReactNode,
  SelectHTMLAttributes,
  TextareaHTMLAttributes,
} from "react";
import { Children, isValidElement, useEffect, useMemo, useRef, useState, type ReactElement } from "react";
import { createPortal } from "react-dom";
import { IconCalendar, IconCheck, IconChevronDown, IconClock, IconX } from "@/components/icons";
import { formatDate, formatTime } from "@/lib/utils";
import { useLang } from "@/context/LanguageContext";

/* ---------------- Button (Bootstrap/Viho) ---------------- */

type ButtonVariant =
  | "primary"
  | "light"
  | "danger"
  | "success"
  | "warning"
  | "info"
  | "secondary"
  | "outline";

/* Solid `primary` / `light` / `outline` remain for main CTAs;
   every other variant uses the badge-style soft pills. */
const buttonVariants: Record<ButtonVariant, string> = {
  primary: "btn-primary",
  light: "btn-primary-light",
  secondary: "btn-soft-secondary",
  danger: "btn-soft-danger",
  success: "btn-soft-success",
  warning: "btn-soft-warning",
  info: "btn-soft-info",
  outline: "btn-outline-primary",
};

export function Button({
  variant = "primary",
  size = "md",
  className,
  loading,
  children,
  ...props
}: ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: ButtonVariant;
  size?: "sm" | "md" | "lg";
  loading?: boolean;
}) {
  return (
    <button
      className={`btn ${buttonVariants[variant]} ${size === "sm" ? "btn-sm" : ""} ${
        size === "lg" ? "btn-lg" : ""
      } ${loading ? "disabled" : ""} ${className ?? ""}`}
      disabled={loading || props.disabled}
      {...props}
    >
      {loading && (
        <span
          className="spinner-border spinner-border-sm me-2"
          style={{ width: "0.9rem", height: "0.9rem" }}
          role="status"
        />
      )}
      {children}
    </button>
  );
}

/* ---------------- Form controls (theme-form) ---------------- */

export function Input({
  label,
  className,
  ...props
}: InputHTMLAttributes<HTMLInputElement> & { label?: string }) {
  return (
    <div className="form-group mb-3">
      {label && <label className="col-form-label pt-0 f-w-600">{label}</label>}
      <input className={`form-control ${className ?? ""}`} {...props} />
    </div>
  );
}

/* ---------------- Select (custom themed dropdown) ----------------
   The native <select> popup is painted by the OS/browser (system blue on
   Windows Chromium) and cannot be themed reliably, so this component keeps
   the .form-control look but renders its own list: white rows, soft teal
   hover, solid teal selected — matching the app UI variant everywhere.
   API-compatible with the previous native version: accepts <option> and
   <optgroup> children and fires onChange with a synthetic { target: { value } }. */

interface SelectOption {
  value: string;
  label: string;
  disabled: boolean;
}
/** an option row, or an <optgroup> header row */
type SelectEntry = SelectOption | { group: string };
const isOption = (e: SelectEntry): e is SelectOption => "value" in e;

/** Flatten an <option>'s children ("{roll} — {name}" etc.) to plain text. */
function optionText(node: ReactNode): string {
  if (node == null || typeof node === "boolean") return "";
  if (typeof node === "string" || typeof node === "number") return String(node);
  if (Array.isArray(node)) return node.map(optionText).join("");
  if (isValidElement(node)) return optionText((node.props as { children?: ReactNode }).children);
  return "";
}

export function Select({
  label,
  className,
  children,
  value,
  onChange,
  disabled,
  wrapperClassName = "form-group mb-3",
  ...rest
}: SelectHTMLAttributes<HTMLSelectElement> & {
  label?: string;
  /** Classes for the wrapping group — pass e.g. "form-group mb-0" inside card headers. */
  wrapperClassName?: string;
}) {
  void rest; // name/required/form semantics are not used by this app's selects
  const { t } = useLang();
  const options = useMemo<SelectEntry[]>(
    () => {
      const out: SelectEntry[] = [];
      const pushOption = (child: ReactElement) => {
        const p = child.props as { value?: string | number; disabled?: boolean; children?: ReactNode };
        out.push({
          value: String(p.value ?? ""),
          label: optionText(p.children).trim(),
          disabled: !!p.disabled,
        });
      };
      for (const child of Children.toArray(children)) {
        if (!isValidElement(child)) continue;
        if (child.type === "optgroup") {
          const p = child.props as { label?: string | number; children?: ReactNode };
          out.push({ group: String(p.label ?? "") });
          for (const opt of Children.toArray(p.children)) {
            if (isValidElement(opt)) pushOption(opt as ReactElement);
          }
        } else {
          pushOption(child as ReactElement);
        }
      }
      return out;
    },
    [children]
  );

  const [open, setOpen] = useState(false);
  const [active, setActive] = useState(-1);
  const [query, setQuery] = useState("");
  const [alignEnd, setAlignEnd] = useState(false);
  const [dropUp, setDropUp] = useState(false);
  const rootRef = useRef<HTMLDivElement>(null);
  const btnRef = useRef<HTMLButtonElement>(null);
  const searchRef = useRef<HTMLInputElement>(null);
  const listRef = useRef<HTMLDivElement>(null);
  const menuRef = useRef<HTMLDivElement>(null);

  const searchable = options.filter(isOption).length >= 8;
  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) return options;
    // searching keeps a group header only while it still has matching options
    const res: SelectEntry[] = [];
    let pendingGroup: { group: string } | null = null;
    for (const e of options) {
      if (!isOption(e)) {
        pendingGroup = e;
        continue;
      }
      if (e.label.toLowerCase().includes(q)) {
        if (pendingGroup) {
          res.push(pendingGroup);
          pendingGroup = null;
        }
        res.push(e);
      }
    }
    return res;
  }, [options, query]);

  const selectedValue = String(value ?? "");
  const selected = (options.find((o) => isOption(o) && o.value === selectedValue) as SelectOption | undefined) ?? null;

  /** next index from `from` moving `dir`, skipping group header rows */
  function stepActive(from: number, dir: 1 | -1): number {
    let i = from;
    do {
      i += dir;
    } while (i >= 0 && i < filtered.length && !isOption(filtered[i]));
    return i >= 0 && i < filtered.length ? i : from;
  }

  // Close on outside press
  useEffect(() => {
    if (!open) return;
    const onDoc = (e: MouseEvent) => {
      if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
    };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, [open]);

  // Focus the search box when the menu opens with many options
  useEffect(() => {
    if (open && searchable) searchRef.current?.focus();
  }, [open, searchable]);

  // Keep the keyboard-highlighted row in view
  useEffect(() => {
    if (!open) return;
    const el = listRef.current?.children[active] as HTMLElement | undefined;
    el?.scrollIntoView({ block: "nearest" });
  }, [active, open]);

  // The menu widens to fit the longest option (width: max-content); when the
  // button sits near a boundary the menu anchors to the right instead so it
  // never spills off-screen. The boundary is the nearest scrollable ancestor
  // (e.g. a wizard step body) — otherwise a menu crossing the card's right
  // edge would open a horizontal scrollbar inside it — with the viewport as
  // the fallback.
  useEffect(() => {
    if (!open) {
      setAlignEnd(false);
      setDropUp(false);
      return;
    }
    const btnRect = btnRef.current?.getBoundingClientRect();
    const menuW = menuRef.current?.offsetWidth ?? 0;
    const menuH = menuRef.current?.offsetHeight ?? 300;
    if (btnRect) {
      // open upward when the viewport has no room below the button
      const spaceBelow = window.innerHeight - btnRect.bottom;
      setDropUp(spaceBelow < menuH + 12 && btnRect.top > spaceBelow);
      let edge = window.innerWidth - 8;
      let parent = btnRef.current?.parentElement as HTMLElement | null;
      while (parent) {
        const oy = getComputedStyle(parent).overflowY;
        if (oy === "auto" || oy === "scroll") {
          edge = Math.min(edge, parent.getBoundingClientRect().right - 4);
          break;
        }
        parent = parent.parentElement;
      }
      setAlignEnd(btnRect.left + Math.max(menuW, btnRect.width) > edge);
    }
  }, [open, options]);

  function closeMenu() {
    setOpen(false);
    setQuery("");
    setActive(-1);
    btnRef.current?.focus();
  }

  function choose(opt: SelectOption) {
    if (opt.disabled) return;
    const wasOpen = open;
    setOpen(false);
    setQuery("");
    setActive(-1);
    if (wasOpen) btnRef.current?.focus();
    if (opt.value !== selectedValue) {
      onChange?.({ target: { value: opt.value } } as ChangeEvent<HTMLSelectElement>);
    }
  }

  function onKeyDown(e: ReactKeyboardEvent<HTMLButtonElement>) {
    if (disabled) return;
    switch (e.key) {
      case "ArrowDown":
      case "ArrowUp":
        e.preventDefault();
        if (!open) {
          setOpen(true);
          setQuery("");
          const idx = filtered.findIndex((o) => isOption(o) && o.value === selectedValue);
          setActive(e.key === "ArrowDown" ? stepActive(idx, 1) : stepActive(idx, -1));
        } else {
          setActive((a) => stepActive(a, e.key === "ArrowDown" ? 1 : -1));
        }
        break;
      case "Home":
        if (open) {
          e.preventDefault();
          setActive(stepActive(-1, 1));
        }
        break;
      case "End":
        if (open) {
          e.preventDefault();
          setActive(stepActive(filtered.length, -1));
        }
        break;
      case "Enter":
      case " ":
        e.preventDefault();
        if (open) {
          const opt = filtered[active];
          if (opt && isOption(opt)) choose(opt);
        } else {
          setOpen(true);
          setQuery("");
          setActive(Math.max(0, filtered.findIndex((o) => isOption(o) && o.value === selectedValue)));
        }
        break;
      case "Escape":
        if (open) {
          e.preventDefault();
          closeMenu();
        }
        break;
      case "Tab":
        setOpen(false);
        setQuery("");
        break;
    }
  }

  // Keyboard handling inside the search input: arrows browse results, Enter picks, Escape closes
  function onSearchKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
    if (e.key === "ArrowDown" || e.key === "ArrowUp") {
      e.preventDefault();
      setActive((a) => stepActive(a, e.key === "ArrowDown" ? 1 : -1));
    } else if (e.key === "Enter") {
      e.preventDefault();
      const opt = filtered[active >= 0 ? active : 0];
      if (opt && isOption(opt)) choose(opt);
    } else if (e.key === "Escape") {
      e.preventDefault();
      closeMenu();
    }
  }

  return (
    <div className={wrapperClassName}>
      {label && <label className="col-form-label pt-0 f-w-600">{label}</label>}
      <div className={`ems-select ${open ? "open" : ""}`} ref={rootRef}>
        <button
          type="button"
          ref={btnRef}
          className={`form-control ems-select-btn ${className ?? ""}`}
          disabled={disabled}
          aria-haspopup="listbox"
          aria-expanded={open}
          onClick={() => {
            if (disabled) return;
            if (!open) {
              setQuery("");
              setActive(Math.max(0, filtered.findIndex((o) => isOption(o) && o.value === selectedValue)));
            }
            setOpen((o) => !o);
          }}
          onKeyDown={onKeyDown}
        >
          <span className="ems-select-label" title={selected?.label}>{selected ? selected.label : "\u00A0"}</span>
          <IconChevronDown className="svg-16 ems-select-caret" />
        </button>
        {open && (
          <div
            className={`ems-select-menu ems-select-list ${alignEnd ? "ems-menu-end" : ""} ${dropUp ? "ems-menu-up" : ""}`}
            role="listbox"
            ref={menuRef}
          >
            {searchable && (
              <div className="ems-select-search">
                <input
                  ref={searchRef}
                  type="text"
                  className="form-control form-control-sm"
                  placeholder={t.common.search}
                  value={query}
                  onChange={(e) => {
                    setQuery(e.target.value);
                    setActive(
                      e.target.value ? stepActive(-1, 1) : Math.max(0, filtered.findIndex((o) => isOption(o) && o.value === selectedValue))
                    );
                  }}
                  onKeyDown={onSearchKeyDown}
                  onMouseDown={(e) => e.stopPropagation()}
                />
              </div>
            )}
            <div ref={listRef}>
              {filtered.map((entry, i) =>
                isOption(entry) ? (
                  <div
                    key={entry.value + "-" + i}
                    role="option"
                    aria-selected={entry.value === selectedValue}
                    aria-disabled={entry.disabled || undefined}
                    className={`ems-select-item ${entry.value === selectedValue ? "selected" : ""} ${
                      i === active ? "active" : ""
                    } ${entry.disabled ? "disabled" : ""}`}
                    onMouseEnter={() => !entry.disabled && setActive(i)}
                    onMouseDown={(e) => e.preventDefault()} // keep button focus
                    onClick={() => choose(entry)}
                  >
                    <span className="ems-select-label">{entry.label}</span>
                    {entry.value === selectedValue && <IconCheck className="svg-14 ems-select-check" />}
                  </div>
                ) : (
                  <div key={"grp" + i} className="ems-select-group" role="presentation">
                    {entry.group}
                  </div>
                )
              )}
              {filtered.length === 0 && (
                <div className="ems-select-empty">{t.common.noResults}</div>
              )}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

/* ---------------- Pagination (client-side) ----------------
   usePagination slices any array (apply it AFTER client-side filters);
   the page clamps to the page count, so changing filters never leaves
   an empty page. <Pagination> renders the Viho/Bootstrap bar with a
   rows-per-page selector (reuses the searchable Select). */

export function usePagination<T>(items: T[], initialSize = 10) {
  const [page, setPage] = useState(1);
  const [size, setSizeState] = useState(initialSize);
  const total = items.length;
  const pageCount = Math.max(1, Math.ceil(total / size));
  const safePage = Math.min(page, pageCount);
  const paged = useMemo(
    () => items.slice((safePage - 1) * size, safePage * size),
    [items, safePage, size]
  );
  function setSize(n: number) {
    setSizeState(n);
    setPage(1);
  }
  return {
    page: safePage,
    setPage,
    size,
    setSize,
    paged,
    total,
    pageCount,
    from: total === 0 ? 0 : (safePage - 1) * size + 1,
    to: Math.min(safePage * size, total),
  };
}

export function Pagination({
  page,
  pageCount,
  total,
  from,
  to,
  size,
  onPageChange,
  onSizeChange,
}: {
  page: number;
  pageCount: number;
  total: number;
  from: number;
  to: number;
  size: number;
  onPageChange: (p: number) => void;
  onSizeChange?: (n: number) => void;
}) {
  const { t } = useLang();
  if (total === 0) return null;

  // Numbered window: current ± 2 with ellipsis
  const pages: Array<number | "…"> = [];
  const start = Math.max(1, Math.min(page - 2, pageCount - 4));
  const end = Math.min(pageCount, start + 4);
  if (start > 1) {
    pages.push(1);
    if (start > 2) pages.push("…");
  }
  for (let p = start; p <= end; p++) pages.push(p);
  if (end < pageCount) {
    if (end < pageCount - 1) pages.push("…");
    pages.push(pageCount);
  }

  return (
    <div className="d-flex flex-wrap align-items-center justify-content-between gap-2 px-3 py-2 border-top">
      <p className="mb-0 ems-fs-12 text-muted">
        {t.common.showing} {from}–{to} {t.common.of} <b>{total}</b>
      </p>
      <div className="d-flex align-items-center gap-2">
        {onSizeChange && (
          <div style={{ width: 84 }} title={t.common.rowsPerPage}>
            <Select
              wrapperClassName="form-group mb-0"
              value={String(size)}
              onChange={(e) => onSizeChange(Number(e.target.value))}
            >
              <option value="10">10</option>
              <option value="25">25</option>
              <option value="50">50</option>
            </Select>
          </div>
        )}
        <ul className="pagination pagination-sm mb-0">
          <li className={`page-item ${page <= 1 ? "disabled" : ""}`}>
            <button className="page-link" type="button" aria-label="Previous" onClick={() => onPageChange(page - 1)}>
              ‹
            </button>
          </li>
          {pages.map((p, i) =>
            p === "…" ? (
              <li className="page-item disabled" key={`e${i}`}>
                <span className="page-link">…</span>
              </li>
            ) : (
              <li className={`page-item ${p === page ? "active" : ""}`} key={p}>
                <button className="page-link" type="button" onClick={() => onPageChange(p)}>
                  {p}
                </button>
              </li>
            )
          )}
          <li className={`page-item ${page >= pageCount ? "disabled" : ""}`}>
            <button className="page-link" type="button" aria-label="Next" onClick={() => onPageChange(page + 1)}>
              ›
            </button>
          </li>
        </ul>
      </div>
    </div>
  );
}

export function Textarea({
  label,
  className,
  ...props
}: TextareaHTMLAttributes<HTMLTextAreaElement> & { label?: string }) {
  return (
    <div className="form-group mb-3">
      {label && <label className="col-form-label pt-0 f-w-600">{label}</label>}
      <textarea className={`form-control ${className ?? ""}`} rows={3} {...props} />
    </div>
  );
}

/* ---------------- Badges ---------------- */

const STATUS_BADGE: Record<string, string> = {
  PENDING: "badge-warning",
  ACTIVE: "badge-success",
  APPROVED: "badge-success",
  REJECTED: "badge-danger",
  SUSPENDED: "badge-danger",
  EXPIRED: "badge-secondary",
  CANCELLED: "badge-secondary",
  PAID: "badge-success",
  PARTIAL: "badge-warning",
  DUE: "badge-danger",
  SUPER_ADMIN: "badge-primary",
  INSTITUTION_ADMIN: "badge-info",
  UPCOMING: "badge-info",
  RUNNING: "badge-success",
  FINISHED: "badge-secondary",
  PRESENT: "badge-success",
  ABSENT: "badge-danger",
  LEAVE: "badge-warning",
  ISSUED: "badge-info",
  RETURNED: "badge-success",
  OVERDUE: "badge-danger",
  SENT: "badge-success",
  FAILED: "badge-danger",
  IN: "badge-success",
  OUT: "badge-warning",
  LOW: "badge-danger",
  IN_REVIEW: "badge-info",
  RESOLVED: "badge-success",
  PLANNED: "badge-secondary",
  IN_PROGRESS: "badge-info",
  COMPLETED: "badge-success",
  RAPID: "badge-warning",
  DETAILS: "badge-primary",
  CONTACTED: "badge-info",
  CONVERTED: "badge-success",
};

export function StatusBadge({ status, label }: { status: string; label: string }) {
  return (
    <span className={`badge ${STATUS_BADGE[status] ?? "badge-secondary"}`}>{label}</span>
  );
}

export function Pill({ children, className }: { children: ReactNode; className?: string }) {
  return <span className={`ems-type-pill ${className ?? ""}`}>{children}</span>;
}

/* ---------------- Modal (Bootstrap markup, React-controlled) ---------------- */

export function Modal({
  open,
  onClose,
  title,
  children,
  wide,
}: {
  open: boolean;
  onClose: () => void;
  title: string;
  children: ReactNode;
  wide?: boolean;
}) {
  // Stays mounted briefly after `open` flips false so the close animation
  // (backdrop fade + panel shrink) can play before unmounting.
  const [render, setRender] = useState(open);
  const [closing, setClosing] = useState(false);
  const renderRef = useRef(open);

  useEffect(() => {
    if (!open) return;
    const handler = (e: KeyboardEvent) => e.key === "Escape" && onClose();
    window.addEventListener("keydown", handler);
    return () => window.removeEventListener("keydown", handler);
  }, [open, onClose]);

  useEffect(() => {
    if (open) {
      renderRef.current = true;
      setRender(true);
      setClosing(false);
      // lock page scroll while the modal is open
      const prev = document.body.style.overflow;
      document.body.style.overflow = "hidden";
      return () => {
        document.body.style.overflow = prev;
      };
    }
    if (!renderRef.current) return;
    setClosing(true);
    const id = window.setTimeout(() => {
      renderRef.current = false;
      setRender(false);
      setClosing(false);
    }, 170);
    return () => window.clearTimeout(id);
  }, [open]);

  if (!render) return null;
  return (
    <div className={`ems-modal ${closing ? "ems-modal-closing" : "ems-modal-opening"}`}>
      <div className="ems-modal-backdrop" onClick={onClose} />
      <div className={`modal-dialog modal-dialog-centered ${wide ? "modal-lg" : ""}`} style={{ margin: 0 }}>
        <div className="modal-content">
          <div className="modal-header">
            <h4 className="modal-title f-w-600">{title}</h4>
            <button type="button" className="btn-close cursor-pointer" onClick={onClose} style={{ boxShadow: "none" }}>
              <IconX className="svg-20" />
            </button>
          </div>
          <div className="modal-body">{children}</div>
        </div>
      </div>
    </div>
  );
}

/* ---------------- Loader / Empty / Alert ---------------- */

export function Spinner({ className }: { className?: string }) {
  return (
    <span
      className={`spinner-border text-primary ${className ?? ""}`}
      style={{ width: "1.6rem", height: "1.6rem" }}
      role="status"
    />
  );
}

/* Quiet, height-stable loading state for content areas. Member pages keep
   their PageHeader mounted and render this under it, so the
   loader → content swap never collapses the layout or flashes the whole
   screen — content fades/slides in over the reserved space when it lands. */
export function LoadingArea({ label }: { label?: string }) {
  return (
    <div className="ems-load-area">
      <Spinner />
      {label ? <p className="mb-0 ems-fs-12 text-muted mt-3">{label}</p> : null}
    </div>
  );
}

/** Shimmering placeholder block — the building block for skeleton screens. */
export function Skeleton({
  width,
  height = 12,
  radius = 8,
  className,
}: {
  width?: number | string;
  height?: number | string;
  radius?: number | string;
  className?: string;
}) {
  return (
    <span
      className={`ems-skeleton ${className ?? ""}`}
      style={{ width, height, borderRadius: radius }}
      aria-hidden="true"
    />
  );
}

/** Placeholder rows for table-style content. */
export function TableSkeleton({ rows = 5, cols = 4 }: { rows?: number; cols?: number }) {
  return (
    <div>
      {Array.from({ length: rows }).map((_, r) => (
        <div
          className="d-flex align-items-center gap-3 py-2"
          key={r}
          style={{ borderBottom: "1px solid #eef2f0" }}
        >
          {Array.from({ length: cols }).map((_, c) => (
            <Skeleton key={c} height={12} width={c === 0 ? "24%" : `${Math.floor(70 / Math.max(1, cols - 1))}%`} />
          ))}
        </div>
      ))}
    </div>
  );
}

/** Full-page skeleton shown while a page's data loads — mirrors the usual
    page anatomy (title, stat tiles, table card) instead of a spinner. */
export function PageLoader({ text }: { text?: string }) {
  // The skeleton fades in only after a short delay — fast page loads settle
  // before anything flashes on screen.
  const [show, setShow] = useState(false);
  useEffect(() => {
    const id = window.setTimeout(() => setShow(true), 150);
    return () => window.clearTimeout(id);
  }, []);
  return (
    <div
      role="status"
      aria-busy="true"
      aria-label={text}
      style={{ opacity: show ? 1 : 0, transition: "opacity 0.2s ease" }}
    >
      {/* page header */}
      <div className="mb-4">
        <Skeleton width={230} height={24} radius={10} />
        <div className="mt-2">
          <Skeleton width={340} height={12} />
        </div>
      </div>
      {/* stat tiles */}
      <div className="row g-3 mb-4">
        {[0, 1, 2, 3].map((i) => (
          <div className="col-6 col-md-3" key={i}>
            <div className="card">
              <div className="card-body py-3">
                <Skeleton width={92} height={11} />
                <div className="mt-2">
                  <Skeleton width={64} height={20} />
                </div>
              </div>
            </div>
          </div>
        ))}
      </div>
      {/* table card */}
      <div className="card">
        <div className="card-header py-2">
          <Skeleton width={170} height={12} />
        </div>
        <div className="card-body">
          <TableSkeleton rows={6} cols={5} />
        </div>
      </div>
    </div>
  );
}

export function EmptyState({
  icon,
  title,
  subtitle,
  action,
}: {
  icon?: ReactNode;
  title: string;
  subtitle?: string;
  action?: ReactNode;
}) {
  return (
    <div className="d-flex flex-column align-items-center justify-content-center gap-2 py-5 text-center">
      {icon && <div className="ems-empty-icon">{icon}</div>}
      <p className="f-w-600 mb-0 text-dark">{title}</p>
      {subtitle && <p className="text-muted ems-fs-13 mb-0" style={{ maxWidth: 380 }}>{subtitle}</p>}
      {action && <div className="mt-2">{action}</div>}
    </div>
  );
}

export function Alert({
  type,
  children,
  onClose,
}: {
  type: "success" | "error" | "info" | "warning";
  children: ReactNode;
  onClose?: () => void;
}) {
  const styles = {
    success: "alert-success",
    error: "alert-danger",
    info: "alert-primary",
    warning: "alert-warning",
  };
  return (
    <div className={`alert ${styles[type]} d-flex align-items-center justify-content-between fade show`} role="alert">
      <span>{children}</span>
      {onClose && (
        <button type="button" className="btn-close cursor-pointer p-0 bg-transparent border-0" onClick={onClose}>
          <IconX className="svg-16" />
        </button>
      )}
    </div>
  );
}

/* ---------------- Cards / Stats / Table ---------------- */

export function Card({
  children,
  className,
  title,
  icon,
  action,
}: {
  children: ReactNode;
  className?: string;
  title?: string;
  icon?: ReactNode;
  action?: ReactNode;
}) {
  return (
    <div className={`card ${className ?? ""}`}>
      {(title || action) && (
        <div className="card-header d-flex align-items-center justify-content-between">
          <h5 className="f-w-600 mb-0 d-flex align-items-center gap-2">
            {icon}
            {title}
          </h5>
          {action}
        </div>
      )}
      <div className="card-body">{children}</div>
    </div>
  );
}

export function StatCard({
  label,
  value,
  icon,
  accent = "teal",
  hint,
}: {
  label: string;
  value: ReactNode;
  icon: ReactNode;
  accent?: "teal" | "blue" | "purple" | "orange" | "red" | "sky" | "pink";
  hint?: string;
}) {
  return (
    <div className="card">
      <div className="card-body">
        <div className="media d-flex align-items-center gap-3">
          <div className={`ems-stat-icon ${accent}`}>{icon}</div>
          <div className="media-body min-w-0">
            <p className="mb-0 ems-fs-13 text-muted text-truncate">{label}</p>
            <h4 className="mb-0 f-w-700">{value}</h4>
            {hint && <p className="mb-0 ems-fs-11 text-muted text-truncate">{hint}</p>}
          </div>
        </div>
      </div>
    </div>
  );
}

export function PageHeader({
  title,
  subtitle,
  action,
}: {
  title: string;
  subtitle?: string;
  action?: ReactNode;
}) {
  return (
    <div className="row mb-4 align-items-end page-header-title">
      <div className="col-md-7">
        <h4 className="f-w-700 mb-1">{title}</h4>
        {subtitle && <p className="text-muted mb-0 ems-fs-13">{subtitle}</p>}
      </div>
      {action && <div className="col-md-5 text-md-end mt-3 mt-md-0">{action}</div>}
    </div>
  );
}

export function Th({ children, className }: { children: ReactNode; className?: string }) {
  return (
    <th scope="col" className={`f-w-700 ${className ?? ""}`}>
      {children}
    </th>
  );
}

export function Td({ children, className }: { children: ReactNode; className?: string }) {
  return <td className={className}>{children}</td>;
}

export function TableWrap({ children }: { children: ReactNode }) {
  return (
    <div className="card">
      <div className="table-responsive">
        <table className="table table-hover align-middle mb-0">{children}</table>
      </div>
    </div>
  );
}

/* ---------------- TimePicker (custom themed) ----------------
   Same reasoning as <Select>: the native <input type="time"> popup is
   OS-drawn (system blue on Windows) and cannot be themed. This picker
   keeps the .form-control look and opens a themed popover with
   hour / minute / AM-PM columns in the app's teal variant.
   Value contract matches the native input: "HH:MM" (24h) in,
   "HH:MM" out via a synthetic { target: { value } } event. */

const TIME_HOURS = Array.from({ length: 12 }, (_, i) => i + 1); // 1..12
const TIME_MINUTES = Array.from({ length: 12 }, (_, i) => i * 5); // 00..55 step 5

export function TimePicker({
  label,
  value,
  onChange,
  disabled,
  className,
  wrapperClassName = "form-group mb-3",
}: {
  label?: string;
  value: string;
  onChange: (e: { target: { value: string } }) => void;
  disabled?: boolean;
  className?: string;
  wrapperClassName?: string;
}) {
  const [open, setOpen] = useState(false);
  const rootRef = useRef<HTMLDivElement>(null);
  const btnRef = useRef<HTMLButtonElement>(null);
  const menuRef = useRef<HTMLDivElement>(null);

  const match = /^(\d{1,2}):(\d{2})$/.exec(value ?? "");
  const h24 = match ? Math.min(23, Math.max(0, Number(match[1]))) : 0;
  const minute = match ? Math.min(59, Math.max(0, Number(match[2]))) : 0;
  const meridiem: "AM" | "PM" = h24 < 12 ? "AM" : "PM";
  const hour12 = h24 % 12 === 0 ? 12 : h24 % 12;

  // Close on outside press
  useEffect(() => {
    if (!open) return;
    const onDoc = (e: MouseEvent) => {
      if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
    };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, [open]);

  // Scroll the selected hour/minute rows into view on open
  useEffect(() => {
    if (!open) return;
    menuRef.current
      ?.querySelectorAll("[data-selected='true']")
      .forEach((el) => el.scrollIntoView({ block: "center" }));
  }, [open]);

  function emit(next: { hour12?: number; minute?: number; meridiem?: "AM" | "PM" }) {
    const h = next.hour12 ?? hour12;
    const m = next.minute ?? minute;
    const mer = next.meridiem ?? meridiem;
    const h24Next = (h % 12) + (mer === "PM" ? 12 : 0);
    const nextValue = `${String(h24Next).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
    if (nextValue !== value) onChange({ target: { value: nextValue } });
  }

  return (
    <div className={wrapperClassName}>
      {label && <label className="col-form-label pt-0 f-w-600">{label}</label>}
      <div className={`ems-select ${open ? "open" : ""}`} ref={rootRef}>
        <button
          type="button"
          ref={btnRef}
          className={`form-control ems-select-btn ems-time-btn ${className ?? ""}`}
          disabled={disabled}
          aria-haspopup="dialog"
          aria-expanded={open}
          onClick={() => !disabled && setOpen((o) => !o)}
          onKeyDown={(e) => {
            if (e.key === "Escape" && open) {
              e.preventDefault();
              setOpen(false);
            }
          }}
        >
          <span className="ems-select-label">
            <IconClock className="svg-16 ems-time-clock" />
            {formatTime(value, "en")}
          </span>
          <IconChevronDown className="svg-16 ems-select-caret" />
        </button>
        {open && (
          <div className="ems-select-menu ems-time-menu" ref={menuRef} role="dialog">
            <div className="ems-time-col">
              {TIME_HOURS.map((h) => (
                <button
                  type="button"
                  key={h}
                  className={`ems-time-opt ${h === hour12 ? "selected" : ""}`}
                  data-selected={h === hour12}
                  onClick={() => emit({ hour12: h })}
                >
                  {h}
                </button>
              ))}
            </div>
            <div className="ems-time-col">
              {TIME_MINUTES.map((m) => (
                <button
                  type="button"
                  key={m}
                  className={`ems-time-opt ${m === minute ? "selected" : ""}`}
                  data-selected={m === minute}
                  onClick={() => emit({ minute: m })}
                >
                  {String(m).padStart(2, "0")}
                </button>
              ))}
            </div>
            <div className="ems-time-col ems-time-mer">
              {(["AM", "PM"] as const).map((mer) => (
                <button
                  type="button"
                  key={mer}
                  className={`ems-time-opt ${mer === meridiem ? "selected" : ""}`}
                  data-selected={mer === meridiem}
                  onClick={() => emit({ meridiem: mer })}
                >
                  {mer}
                </button>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

/* ---------------- DatePicker / MonthPicker (custom themed) ----------------
   Native <input type="date"/"month"> popups are OS-drawn (system blue on
   Windows) and cannot be themed — same fix as <Select>/<TimePicker>: a
   .form-control-looking button opening a themed calendar popover.
   Value contracts match the native inputs: "YYYY-MM-DD" and "YYYY-MM";
   changes fire a synthetic { target: { value } } event. The week starts on
   Saturday, matching the Bangladeshi academic week (WEEK_DAYS). */

/** "Sa Su Mo Tu We Th Fr" in the active locale. */
function weekdayLabels(locale: string): string[] {
  const fmt = new Intl.DateTimeFormat(locale === "bn" ? "bn-BD" : "en-GB", { weekday: "short" });
  // 2024-01-06 is a Saturday
  return Array.from({ length: 7 }, (_, i) => fmt.format(new Date(2024, 0, 6 + i)));
}

const pad2 = (n: number) => String(n).padStart(2, "0");
const toISODate = (y: number, m: number, d: number) => `${y}-${pad2(m + 1)}-${pad2(d)}`;
const todayISODate = () => {
  const d = new Date();
  return toISODate(d.getFullYear(), d.getMonth(), d.getDate());
};

function PickerShell({
  open,
  onToggle,
  rootRef,
  btnRef,
  className,
  disabled,
  display,
  children,
  label,
  wrapperClassName,
  style,
}: {
  open: boolean;
  onToggle: () => void;
  rootRef: React.RefObject<HTMLDivElement | null>;
  btnRef: React.RefObject<HTMLButtonElement | null>;
  className?: string;
  disabled?: boolean;
  display: ReactNode;
  children: ReactNode;
  label?: string;
  wrapperClassName?: string;
  style?: CSSProperties;
}) {
  const menuRef = useRef<HTMLDivElement>(null);
  // Fixed coordinates for the portalled menu (computed from the button rect)
  const [pos, setPos] = useState<{ top: number; left: number } | null>(null);

  // Close on outside press — the menu lives in a portal at <body>, so both
  // the control root and the menu itself count as "inside"
  useEffect(() => {
    if (!open) return;
    const onDoc = (e: MouseEvent) => {
      const target = e.target as Node;
      if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
      rootRef.current?.dispatchEvent(new CustomEvent("emsclose"));
    };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, [open, rootRef]);

  // Position when opening; flip above the button when the viewport has no
  // room below (tall calendars near the bottom of the screen / modals)
  useEffect(() => {
    if (!open) return;
    const place = () => {
      const rect = btnRef.current?.getBoundingClientRect();
      if (!rect) return;
      const menuH = menuRef.current?.offsetHeight ?? 330;
      const menuW = menuRef.current?.offsetWidth ?? 272;
      const spaceBelow = window.innerHeight - rect.bottom;
      const up = spaceBelow < menuH + 12 && rect.top > spaceBelow;
      // phones: the (nearly full-width) menu centers in the viewport —
      // clientWidth, not innerWidth, matches the fixed-position layout box
      const vw = document.documentElement.clientWidth;
      const left = vw < 768 ? Math.max(8, (vw - menuW) / 2) : rect.left;
      setPos({ top: up ? rect.top - 6 : rect.bottom + 4, left });
    };
    place();
    // Re-measure once the menu has rendered its real height/width (the
    // portalled menu mounts only after the first pass sets `pos`)
    const raf = requestAnimationFrame(place);
    const settle = window.setTimeout(place, 60);
    // Any scroll or resize would leave the menu mis-anchored — just close
    const close = () => rootRef.current?.dispatchEvent(new CustomEvent("emsclose"));
    window.addEventListener("scroll", close, true);
    window.addEventListener("resize", close);
    return () => {
      cancelAnimationFrame(raf);
      window.clearTimeout(settle);
      window.removeEventListener("scroll", close, true);
      window.removeEventListener("resize", close);
    };
  }, [open, rootRef, btnRef]);

  return (
    <div className={wrapperClassName ?? "form-group mb-3"} style={style}>
      {label && <label className="col-form-label pt-0 f-w-600">{label}</label>}
      <div className={`ems-select ${open ? "open" : ""}`} ref={rootRef}>
        <button
          type="button"
          ref={btnRef}
          className={`form-control ems-select-btn ${className ?? ""}`}
          disabled={disabled}
          aria-haspopup="dialog"
          aria-expanded={open}
          onClick={onToggle}
          onKeyDown={(e) => {
            if (e.key === "Escape" && open) {
              e.preventDefault();
              rootRef.current?.dispatchEvent(new CustomEvent("emsclose"));
            }
          }}
        >
          <span className="ems-select-label">
            <IconCalendar className="svg-16 ems-time-clock" />
            {display}
          </span>
          <IconChevronDown className="svg-16 ems-select-caret" />
        </button>
        {open && pos &&
          createPortal(
            <div
              className="ems-select-menu ems-select-menu-fixed"
              role="dialog"
              ref={menuRef}
              style={{ top: pos.top, left: pos.left }}
            >
              {children}
            </div>,
            document.body
          )}
      </div>
    </div>
  );
}

export function DatePicker({
  label,
  value,
  onChange,
  min,
  max,
  disabled,
  className,
  wrapperClassName,
  style,
  placeholder = "—",
}: {
  label?: string;
  value: string; // "YYYY-MM-DD" or ""
  onChange: (e: { target: { value: string } }) => void;
  min?: string;
  max?: string;
  disabled?: boolean;
  className?: string;
  wrapperClassName?: string;
  style?: CSSProperties;
  placeholder?: string;
}) {
  const { t, locale } = useLang();
  const [open, setOpen] = useState(false);
  const [yearMode, setYearMode] = useState(false);
  const rootRef = useRef<HTMLDivElement>(null);
  const btnRef = useRef<HTMLButtonElement>(null);

  const base = value || todayISODate();
  const [view, setView] = useState(() => ({ y: Number(base.slice(0, 4)), m: Number(base.slice(5, 7)) - 1 }));
  useEffect(() => {
    if (open) {
      const b = value || todayISODate();
      setView({ y: Number(b.slice(0, 4)), m: Number(b.slice(5, 7)) - 1 });
    } else {
      setYearMode(false);
    }
  }, [open, value]);

  // close requests from the shared shell
  useEffect(() => {
    const el = rootRef.current;
    if (!el) return;
    const close = () => setOpen(false);
    el.addEventListener("emsclose", close);
    return () => el.removeEventListener("emsclose", close);
  });

  const { y, m } = view;
  const firstOffset = (new Date(y, m, 1).getDay() + 1) % 7; // Sat-first grid
  const daysInMonth = new Date(y, m + 1, 0).getDate();
  const today = todayISODate();
  const monthTitle = new Intl.DateTimeFormat(locale === "bn" ? "bn-BD" : "en-GB", {
    month: "long",
    year: "numeric",
  }).format(new Date(y, m, 1));

  const monthFloor = min ? `${min.slice(0, 8)}01` : null; // for prev-arrow disable
  const prevDisabled = monthFloor ? toISODate(y, m, 1) <= monthFloor : false;
  const nextDisabled = max ? toISODate(y, m + 1, 1) > max : false;

  /* year quick-jump (e.g. birth dates): a 12-year page grid */
  const yearPageStart = y - (((y % 12) - 9 + 24) % 12); // pages run 2013–2024, 2025–2036…
  const minYear = min ? Number(min.slice(0, 4)) : null;
  const maxYear = max ? Number(max.slice(0, 4)) : null;
  const yearPrevDisabled = minYear !== null && yearPageStart - 1 < minYear;
  const yearNextDisabled = maxYear !== null && yearPageStart + 12 > maxYear;
  const yearOut = (yy: number) =>
    (minYear !== null && `${yy}-12-31` < min!) || (maxYear !== null && `${yy}-01-01` > max!);

  function pick(d: number) {
    const iso = toISODate(y, m, d);
    if ((min && iso < min) || (max && iso > max)) return;
    setOpen(false);
    btnRef.current?.focus();
    if (iso !== value) onChange({ target: { value: iso } });
  }

  return (
    <PickerShell
      open={open}
      onToggle={() => !disabled && setOpen((o) => !o)}
      rootRef={rootRef}
      btnRef={btnRef}
      className={className}
      disabled={disabled}
      label={label}
      wrapperClassName={wrapperClassName}
      style={style}
      display={value ? formatDate(value, locale) : <span className="text-muted">{placeholder}</span>}
    >
      <div className="ems-date-menu" role="presentation">
        <div className="ems-date-head">
          <button
            type="button"
            className="ems-date-nav"
            disabled={yearMode ? yearPrevDisabled : prevDisabled}
            onClick={() =>
              setView(yearMode ? { y: y - 12, m } : { y: m === 0 ? y - 1 : y, m: m === 0 ? 11 : m - 1 })
            }
            aria-label={yearMode ? "Previous 12 years" : "Previous month"}
          >
            ‹
          </button>
          {/* clicking the title toggles the 12-year quick-jump grid — picking
              a birth year no longer means paging through months */}
          <button
            type="button"
            className="ems-date-title-btn"
            onClick={() => setYearMode((v) => !v)}
            aria-expanded={yearMode}
            title={yearMode ? monthTitle : ""}
          >
            {yearMode
              ? `${yearPageStart} – ${yearPageStart + 11}`
              : monthTitle}
            <span className="ems-date-title-caret">▾</span>
          </button>
          <button
            type="button"
            className="ems-date-nav"
            disabled={yearMode ? yearNextDisabled : nextDisabled}
            onClick={() =>
              setView(yearMode ? { y: y + 12, m } : { y: m === 11 ? y + 1 : y, m: m === 11 ? 0 : m + 1 })
            }
            aria-label={yearMode ? "Next 12 years" : "Next month"}
          >
            ›
          </button>
        </div>
        {yearMode ? (
          <div className="ems-date-grid ems-year-grid">
            {Array.from({ length: 12 }).map((_, i) => {
              const yy = yearPageStart + i;
              return (
                <button
                  type="button"
                  key={yy}
                  className={`ems-month-opt ${yy === y ? "selected" : ""}`}
                  disabled={yearOut(yy)}
                  onClick={() => {
                    setView({ y: yy, m });
                    setYearMode(false);
                  }}
                >
                  {new Intl.DateTimeFormat(locale === "bn" ? "bn-BD" : "en-GB", {
                    year: "numeric",
                  }).format(new Date(yy, m, 1))}
                </button>
              );
            })}
          </div>
        ) : (
          <>
            <div className="ems-date-grid">
              {weekdayLabels(locale).map((wd, i) => (
                <span key={i} className="ems-date-wd">
                  {wd}
                </span>
              ))}
              {Array.from({ length: firstOffset }).map((_, i) => (
                <span key={"b" + i} />
              ))}
              {Array.from({ length: daysInMonth }).map((_, i) => {
                const d = i + 1;
                const iso = toISODate(y, m, d);
                const out = !!((min && iso < min) || (max && iso > max));
                return (
                  <button
                    type="button"
                    key={d}
                    className={`ems-date-day ${iso === value ? "selected" : ""} ${iso === today ? "today" : ""}`}
                    disabled={out}
                    onClick={() => pick(d)}
                  >
                    {new Intl.DateTimeFormat(locale === "bn" ? "bn-BD" : "en-GB", { day: "numeric" }).format(
                      new Date(y, m, d)
                    )}
                  </button>
                );
              })}
            </div>
            <div className="ems-date-foot">
              <button
                type="button"
                className="ems-date-today-btn"
                onClick={() => {
                  const iso = todayISODate();
                  if (!((min && iso < min) || (max && iso > max))) {
                    setOpen(false);
                    if (iso !== value) onChange({ target: { value: iso } });
                  }
                }}
              >
                {t.common.today}
              </button>
            </div>
          </>
        )}
      </div>
    </PickerShell>
  );
}

export function MonthPicker({
  label,
  value,
  onChange,
  min,
  max,
  disabled,
  className,
  wrapperClassName,
  style,
  placeholder = "—",
}: {
  label?: string;
  value: string; // "YYYY-MM" or ""
  onChange: (e: { target: { value: string } }) => void;
  min?: string;
  max?: string;
  disabled?: boolean;
  className?: string;
  wrapperClassName?: string;
  style?: CSSProperties;
  placeholder?: string;
}) {
  const { locale } = useLang();
  const [open, setOpen] = useState(false);
  const rootRef = useRef<HTMLDivElement>(null);
  const btnRef = useRef<HTMLButtonElement>(null);

  const base = value || `${new Date().getFullYear()}-${pad2(new Date().getMonth() + 1)}`;
  const [viewYear, setViewYear] = useState(() => Number(base.slice(0, 4)));
  useEffect(() => {
    if (open) setViewYear(Number((value || `${new Date().getFullYear()}-${pad2(new Date().getMonth() + 1)}`).slice(0, 4)));
  }, [open, value]);

  useEffect(() => {
    const el = rootRef.current;
    if (!el) return;
    const close = () => setOpen(false);
    el.addEventListener("emsclose", close);
    return () => el.removeEventListener("emsclose", close);
  });

  const monthName = new Intl.DateTimeFormat(locale === "bn" ? "bn-BD" : "en-GB", {
    month: "short",
    year: "numeric",
  });

  const prevDisabled = min ? viewYear <= Number(min.slice(0, 4)) : false;
  const nextDisabled = max ? viewYear >= Number(max.slice(0, 4)) : false;

  return (
    <PickerShell
      open={open}
      onToggle={() => !disabled && setOpen((o) => !o)}
      rootRef={rootRef}
      btnRef={btnRef}
      className={className}
      disabled={disabled}
      label={label}
      wrapperClassName={wrapperClassName}
      style={style}
      display={
        value ? (
          monthName.format(new Date(Number(value.slice(0, 4)), Number(value.slice(5, 7)) - 1, 1))
        ) : (
          <span className="text-muted">{placeholder}</span>
        )
      }
    >
      <div className="ems-date-menu ems-month-menu" role="presentation">
        <div className="ems-date-head">
          <button
            type="button"
            className="ems-date-nav"
            disabled={prevDisabled}
            onClick={() => setViewYear(viewYear - 1)}
            aria-label="Previous year"
          >
            ‹
          </button>
          <span className="ems-date-title">
            {new Intl.DateTimeFormat(locale === "bn" ? "bn-BD" : "en-GB", {
              year: "numeric",
            }).format(new Date(viewYear, 0, 1))}
          </span>
          <button
            type="button"
            className="ems-date-nav"
            disabled={nextDisabled}
            onClick={() => setViewYear(viewYear + 1)}
            aria-label="Next year"
          >
            ›
          </button>
        </div>
        <div className="ems-date-grid ems-month-grid">
          {Array.from({ length: 12 }).map((_, m) => {
            const iso = `${viewYear}-${pad2(m + 1)}`;
            const out = !!((min && iso < min) || (max && iso > max));
            return (
              <button
                type="button"
                key={m}
                className={`ems-month-opt ${iso === value ? "selected" : ""}`}
                disabled={out}
                onClick={() => {
                  setOpen(false);
                  btnRef.current?.focus();
                  if (iso !== value) onChange({ target: { value: iso } });
                }}
              >
                {new Intl.DateTimeFormat(locale === "bn" ? "bn-BD" : "en-GB", {
                  month: "short",
                }).format(new Date(viewYear, m, 1))}
              </button>
            );
          })}
        </div>
      </div>
    </PickerShell>
  );
}
