/**
 * UI primitives, in the shadcn idiom — copied-in components you own rather
 * than a dependency you configure. Kept in one file because there are few
 * enough that a directory of six-line modules would be worse.
 *
 * Every interactive element clears a 44px touch target, which is the
 * documented iOS minimum and the practical difference between a control that
 * works on a phone and one that is merely present on it.
 */
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from '@radix-ui/react-slot';
import { ChevronRight } from 'lucide-react';
import { Link } from 'react-router-dom';
import {
  forwardRef,
  useState,
  type ButtonHTMLAttributes,
  type HTMLAttributes,
  type InputHTMLAttributes,
  type LabelHTMLAttributes,
  type ReactNode,
} from 'react';

export function cn(...inputs: ClassValue[]): string {
  return twMerge(clsx(inputs));
}

// ------------------------------------------------------------------- button

/**
 * One button, two vocabularies.
 *
 * There used to be two Button *components* — this one and a verbatim shadcn
 * copy at `@/components/ui/button` with a different variant vocabulary
 * (`default`/`destructive`/`link`) and a 36px size scale that fails PIG's own
 * 44px touch-target rule. Two implementations of the same control drift, and
 * these two already had: one grew a `danger` variant, the other a `link`.
 *
 * They are now a single cva. Both vocabularies are declared here as aliases of
 * the same classes, so `variant="primary"` and `variant="default"` are the
 * same button, and `@/components/ui/button` is a re-export that only supplies
 * shadcn's different *default* variant. The remaining work is to retire the
 * shadcn names at the three call sites that use them and delete the shim.
 */
const buttonVariants = cva(
  'inline-flex items-center justify-center gap-2 rounded-lg text-sm font-medium ' +
    'transition-colors duration-1 ease-enter disabled:pointer-events-none disabled:opacity-50 ' +
    '[&_svg]:shrink-0 ' +
    /*
     * The ring is declared here as well as in the global `:focus-visible` rule.
     * A button that also carries a local `focus-visible:ring-*` class beat the
     * base rule on specificity and painted the near-invisible subtle accent;
     * declaring it in the cva puts it in the same cascade layer as those
     * overrides, so `cn()` merging resolves it rather than the stylesheet.
     */
    'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand ' +
    'focus-visible:ring-offset-2 focus-visible:ring-offset-bg ' +
    // touch-manipulation removes the 300ms tap delay that older mobile Safari
    // applies while waiting to see whether a tap is a double-tap zoom.
    'touch-manipulation select-none whitespace-nowrap',
  {
    variants: {
      variant: {
        primary: 'bg-primary text-primary-foreground shadow-sm hover:opacity-90 active:opacity-80',
        secondary: 'bg-surface-2 text-fg hover:bg-border active:bg-border',
        outline: 'border border-border bg-transparent hover:bg-surface-2',
        ghost: 'bg-transparent hover:bg-surface-2',
        danger: 'bg-danger text-white hover:opacity-90',
        /* shadcn's vocabulary, mapped onto the same three treatments. */
        default: 'bg-primary text-primary-foreground shadow-sm hover:opacity-90 active:opacity-80',
        destructive: 'bg-danger text-white hover:opacity-90',
        link: 'bg-transparent text-accent-fg underline-offset-4 hover:underline',
      },
      size: {
        // min-h keeps the target tappable even when the label is short.
        sm: 'h-11 min-h-[44px] px-3 text-xs',
        md: 'h-11 min-h-[44px] px-4',
        lg: 'h-12 min-h-[48px] px-6 text-base',
        icon: 'h-11 w-11 min-h-[44px] min-w-[44px] p-0',
        /* shadcn's `default` size. Deliberately PIG's height, not 36px. */
        default: 'h-11 min-h-[44px] px-4',
      },
    },
    defaultVariants: { variant: 'secondary', size: 'md' },
  },
);

export { buttonVariants };

export interface ButtonProps
  extends ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  /** Render the child element instead of a `<button>`, keeping the classes. */
  asChild?: boolean;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Component = asChild ? Slot : 'button';
    return (
      <Component
        ref={ref}
        className={cn(buttonVariants({ variant, size }), className)}
        {...props}
      />
    );
  },
);
Button.displayName = 'Button';

// -------------------------------------------------------------------- input

export const Input = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>>(
  ({ className, ...props }, ref) => (
    <input
      ref={ref}
      className={cn(
        'h-11 w-full rounded-lg border border-border bg-surface px-3 text-fg',
        'placeholder:text-muted focus-visible:border-accent',
        // The base stylesheet enforces a 16px minimum so Safari does not zoom
        // the viewport on focus; this must not override it downward.
        className,
      )}
      {...props}
    />
  ),
);
Input.displayName = 'Input';

// --------------------------------------------------------------------- card

/**
 * `min-w-0` is on the base class deliberately, not left to each call site.
 *
 * A grid or flex child defaults to `min-width: auto`, meaning it refuses to
 * shrink below its content — and cards routinely contain something
 * unshrinkable (a `tabular-nums` figure, a `whitespace-nowrap` badge, a long
 * unbroken title). The result is a card wider than its column, which drags the
 * whole page into horizontal scrolling on a phone.
 *
 * This has now been fixed three separate times at individual call sites, which
 * is the signal that it belongs here instead. `min-width: 0` is inert for a
 * block-level card outside a flex or grid container, so applying it always
 * costs nothing and removes the entire class of bug.
 */
export function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
  return <div className={cn('card min-w-0', className)} {...props} />;
}

export function CardHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
  return <div className={cn('flex flex-col gap-1 p-4 sm:p-5', className)} {...props} />;
}

export function CardTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {
  return <h3 className={cn('font-semibold leading-tight', className)} {...props} />;
}

export function CardContent({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
  return <div className={cn('p-4 pt-0 sm:p-5 sm:pt-0', className)} {...props} />;
}

export function CardFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
  return <div className={cn('flex items-center gap-3 p-4 pt-0 sm:p-5 sm:pt-0', className)} {...props} />;
}

// -------------------------------------------------------------------- badge

const badgeVariants = cva(
  'inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium',
  {
    variants: {
      tone: {
        neutral: 'bg-surface-2 text-muted',
        accent: 'bg-accent-subtle text-accent-fg',
        positive: 'bg-positive/10 text-positive',
        warning: 'bg-warning/10 text-warning',
        danger: 'bg-danger/10 text-danger',
        info: 'bg-info/10 text-info',
      },
    },
    defaultVariants: { tone: 'neutral' },
  },
);

export function Badge({
  className,
  tone,
  ...props
}: HTMLAttributes<HTMLSpanElement> & VariantProps<typeof badgeVariants>) {
  return <span className={cn(badgeVariants({ tone }), className)} {...props} />;
}

// -------------------------------------------------------------- micro label

/**
 * The one small-caps label in the product.
 *
 * There were five of these — 10px, 11px and 12px, `tracking-wide`,
 * `tracking-wider` and `tracking-[0.12em]`, `font-medium` and `font-semibold`
 * — across 58 hand-written call sites, which is why no two panels' labels
 * lined up. 11px at 0.06em was the majority reading and the one that survives
 * the sidebar rail's width.
 *
 * `as` exists because the same label is a `dt` in a definition list, a `th` in
 * a table head and a `span` inside a flex row; rendering all three as a `div`
 * is how a table stops being a table for a screen reader.
 */
const MICRO_LABEL =
  'text-[11px] font-medium uppercase leading-tight tracking-[0.06em] text-muted';

export function Label({
  as: Component = 'div',
  className,
  ...props
}: LabelHTMLAttributes<HTMLElement> & {
  as?: 'div' | 'span' | 'p' | 'dt' | 'th' | 'legend' | 'label' | 'h2' | 'h3' | 'h4';
}) {
  return (
    <Component className={cn(MICRO_LABEL, className)} {...props} />
  );
}

// ------------------------------------------------------------------ section

/**
 * A titled group of content: the panel heading, its count, its action, and
 * optionally a chevron that folds it away.
 *
 * Six of these existed at 11px-uppercase through 16px-sentence-case, so a page
 * built from three of them read as three products. `tone` is the only choice
 * left: `panel` owns a card, `micro` names a group inside one.
 */
export function Section({
  title,
  description,
  count,
  action,
  level = 3,
  tone = 'panel',
  collapsible = false,
  defaultOpen = true,
  className,
  children,
}: {
  title: ReactNode;
  description?: ReactNode;
  /** Rendered beside the title as a tabular figure. `0` still renders. */
  count?: number;
  action?: ReactNode;
  level?: 2 | 3 | 4;
  tone?: 'panel' | 'micro';
  collapsible?: boolean;
  defaultOpen?: boolean;
  className?: string;
  children?: ReactNode;
}) {
  const [open, setOpen] = useState(defaultOpen);
  const Heading = ({ 2: 'h2', 3: 'h3', 4: 'h4' } as const)[level];

  const titleClass =
    tone === 'panel' ? 'text-base font-semibold leading-tight text-fg' : MICRO_LABEL;

  const heading = (
    <>
      <span className={cn('min-w-0 break-words', titleClass)}>{title}</span>
      {count == null ? null : <span className="nums shrink-0 text-sm text-muted">{count}</span>}
    </>
  );

  return (
    <section className={cn('min-w-0', className)}>
      <div className={cn('flex min-w-0 items-start gap-2', collapsible ? '' : 'py-0.5')}>
        <Heading className="min-w-0 flex-1">
          {collapsible ? (
            <button
              type="button"
              onClick={() => setOpen((was) => !was)}
              aria-expanded={open}
              className="flex min-h-11 w-full items-center gap-2 rounded-lg text-left transition-colors duration-1 ease-enter hover:text-fg"
            >
              <ChevronRight
                className={cn(
                  'size-4 shrink-0 text-muted transition-transform duration-1 ease-enter',
                  open && 'rotate-90',
                )}
                aria-hidden
              />
              {heading}
            </button>
          ) : (
            <span className="flex min-w-0 items-baseline gap-2">{heading}</span>
          )}
        </Heading>
        {action ? <div className="shrink-0">{action}</div> : null}
      </div>
      {description ? (
        <p className={cn('mt-1 min-w-0 text-sm text-muted', collapsible && 'pl-6')}>{description}</p>
      ) : null}
      {children != null && (!collapsible || open) ? (
        <div className={cn('mt-2 min-w-0', tone === 'panel' && !collapsible && 'mt-3')}>
          {children}
        </div>
      ) : null}
    </section>
  );
}

// --------------------------------------------------------------------- stat

const statValueSizes = {
  /** Inline tile inside a panel. */
  sm: 'text-sm',
  /** A figure a panel is about. */
  md: 'text-lg',
  /** The page's headline number. */
  lg: 'text-2xl sm:text-3xl',
} as const;

const statSurfaces = {
  /** The default, and the only one that existed: a card of its own. */
  card: 'card p-4',
  /** A tile on `bg-surface-2` inside a card. No border, no shadow. */
  inset: 'rounded-md bg-surface-2 p-2.5',
  /** No surface at all — the caller owns the container. */
  bare: '',
} as const;

/**
 * A single labelled number.
 *
 * `nums` applies tabular figures so a value does not jitter horizontally as it
 * updates — which it does, on a dashboard that refreshes while someone is
 * looking at it.
 *
 * `min-w-0 break-words` is on the value, not left to the caller: this tile is
 * always a grid child, a grid child refuses to shrink below its content, and
 * the content is an unbreakable currency string. The measured failure was
 * `GROSS MARGIN $658,194.3` — a digit short — on any phone under 400px, and
 * the same figure colliding with the next card at 1440 with the Piggy dock
 * open. A truncated financial figure is worse than no figure.
 *
 * That stopped the clipping and traded it for a second failure nobody measured,
 * because `scrollWidth === clientWidth` is true of a number that has WRAPPED:
 * `$658,194.` on one line and `37` on the next, at every width under about
 * 220px — a phone, and 1440 with the dock open. So the tile is now a container
 * query context (`stat-tile`) and the `lg` figure carries `stat-figure-lg`,
 * which steps 30 → 24 → 22 → 20 → 18 → 16px as the tile narrows. See the
 * measured ladder in `index.css`. Every tile in a grid row is the same width,
 * so a row steps together; `break-words` stays as the last-resort net.
 *
 * An absent value renders a muted em dash rather than the tone colour: "—" in
 * danger red reads as a number that went wrong rather than one nobody has.
 */
export function Stat({
  label,
  value,
  hint,
  tone,
  size = 'lg',
  surface = 'card',
  href,
  className,
}: {
  label: string;
  value: ReactNode;
  hint?: ReactNode;
  tone?: 'positive' | 'warning' | 'danger' | 'default';
  size?: 'sm' | 'md' | 'lg';
  surface?: 'card' | 'inset' | 'bare';
  /** Makes the whole tile a router link to the page that explains the figure. */
  href?: string;
  className?: string;
}) {
  const absent = value == null || value === '' || value === '—' || value === '-';
  const toneClass = absent
    ? 'text-muted'
    : tone === 'positive'
      ? 'text-positive'
      : tone === 'warning'
        ? 'text-warning'
        : tone === 'danger'
          ? 'text-danger'
          : 'text-fg';

  const body = (
    <>
      <Label>{label}</Label>
      <div
        className={cn(
          'nums min-w-0 break-words font-semibold leading-tight',
          size === 'sm' ? 'mt-0.5' : 'mt-1',
          statValueSizes[size],
          // Only the page KPI step is container-scaled; `sm` and `md` are
          // already small enough that no figure in the product wraps them.
          size === 'lg' && 'stat-figure-lg',
          toneClass,
        )}
      >
        {absent ? '—' : value}
      </div>
      {hint ? <div className="mt-1 min-w-0 text-xs text-muted">{hint}</div> : null}
    </>
  );

  const shell = cn(
    'block min-w-0',
    /*
     * The container context is declared ONLY on the page-KPI step, and that is
     * a measured constraint rather than tidiness. `container-type: inline-size`
     * carries `contain: layout style inline-size`, which suppresses a box's
     * content-based intrinsic contribution — so putting it on every `Stat`
     * changed how the `sm` and `md` tiles inside Growth's account cards
     * negotiated width with the flex rows around them, and /growth began
     * overflowing its viewport by 32px at 393 and 12px at 1440. Zero horizontal
     * overflow is the product's oldest measured guarantee; a nicer number is
     * not worth spending it. The `lg` tiles are grid children with declared
     * tracks, where the contribution is not what decides the width.
     */
    size === 'lg' && 'stat-tile',
    statSurfaces[surface],
    href && 'transition-colors duration-1 ease-enter hover:bg-surface-2',
    className,
  );

  return href ? (
    <Link to={href} className={shell}>
      {body}
    </Link>
  ) : (
    <div className={shell}>{body}</div>
  );
}

// ------------------------------------------------------------------ skeleton

export function Skeleton({ className }: { className?: string }) {
  return <div className={cn('animate-pulse rounded-md bg-surface-2', className)} />;
}

// --------------------------------------------------------------- empty state

/**
 * Nothing here, said once.
 *
 * Seven bespoke empty states stood beside this one, differing only in how much
 * vertical room they took: a panel's worth of padding inside a 120px list row
 * pushes the thing below it off the screen. `size` is that decision and the
 * only one — `panel` is the historic rendering and stays the default.
 */
export function EmptyState({
  icon,
  title,
  description,
  action,
  size = 'panel',
  className,
}: {
  icon?: ReactNode;
  title: string;
  description?: string;
  action?: ReactNode;
  /** `inline` inside a list or a card body, `panel` for a card, `page` for a route. */
  size?: 'inline' | 'panel' | 'page';
  className?: string;
}) {
  const box =
    size === 'inline' ? 'gap-2 px-4 py-6' : size === 'page' ? 'gap-4 px-6 py-20' : 'gap-3 px-6 py-12';
  const titleClass =
    size === 'inline'
      ? 'text-sm font-medium'
      : size === 'page'
        ? 'text-base font-semibold'
        : 'font-medium';

  return (
    <div
      className={cn(
        'flex min-w-0 flex-col items-center justify-center text-center',
        box,
        className,
      )}
    >
      {icon ? <div className="text-muted">{icon}</div> : null}
      <div className="min-w-0">
        <p className={cn('break-words', titleClass)}>{title}</p>
        {description ? (
          <p
            className={cn(
              'mx-auto mt-1 max-w-sm break-words text-muted',
              size === 'inline' ? 'text-xs' : 'text-sm',
            )}
          >
            {description}
          </p>
        ) : null}
      </div>
      {action}
    </div>
  );
}

// -------------------------------------------------------- confidence marker

/**
 * Provenance, shown rather than hidden.
 *
 * PIG holds records about real people assembled from public sources, and some
 * rest on a single weak citation. Presenting those with the same visual weight
 * as a corroborated record is how a CRM quietly becomes misinformation — so
 * anything short of `confirmed` is labelled wherever it appears.
 */
export function ConfidenceBadge({ confidence }: { confidence: string }) {
  if (confidence === 'confirmed') return null;
  const tone =
    confidence === 'probable' ? 'info' : confidence === 'disputed' ? 'danger' : 'warning';
  const label =
    confidence === 'probable'
      ? 'Probable'
      : confidence === 'disputed'
        ? 'Disputed'
        : 'Unverified';
  return (
    <Badge tone={tone} title="How well-sourced this record is">
      {label}
    </Badge>
  );
}
