'use client';

import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';

import { cn } from '../lib/cn';
import { Button } from './button';

/**
 * Modal dialog, built on Radix so focus trapping, escape handling, scroll
 * locking and `aria-modal` semantics are correct without hand-rolling them.
 */

export const Dialog = DialogPrimitive.Root;
export const DialogTrigger = DialogPrimitive.Trigger;
export const DialogClose = DialogPrimitive.Close;

export const DialogContent = React.forwardRef<
  React.ComponentRef<typeof DialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { hideClose?: boolean }
>(function DialogContent({ className, children, hideClose, ...props }, ref) {
  return (
    <DialogPrimitive.Portal>
      <DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-ink/30 backdrop-blur-[3px] data-[state=open]:animate-in data-[state=open]:fade-in" />

      {/*
        A full-screen layer that centres the panel with flexbox.
       
        The panel used to centre itself with `top-1/2` and `-translate-y-1/2`,
        which is the usual recipe and is quietly broken for a panel whose height
        changes while it is open. The translation is resolved against the
        panel's own height, so when a re-render made the content taller or
        shorter the browser kept the children where the old height had put them:
        the box sat at one offset and its contents at another, clipped at the
        top by `overflow-hidden` and leaving several hundred pixels of empty
        panel below the footer. Ticking a college in Add User is exactly such a
        re-render (ASSUMPTIONS.md G23).
       
        Centring in a flex container removes the transform, so there is no
        stale offset to go wrong, and `max-h-full` inside the padded layer
        replaces the viewport arithmetic.
       
        The layer ignores pointer events so that clicking beside the panel still
        reaches the overlay and dismisses the dialog; the panel takes them back.
      */}
      <div className="pointer-events-none fixed inset-0 z-50 flex items-center justify-center p-4">
        <DialogPrimitive.Content
          ref={ref}
          className={cn(
            'pointer-events-auto w-full max-w-lg',
            'glass-strong rounded-[var(--radius-card)] border',
            /*
             * A column whose height follows its content — deliberately with no
             * cap of its own.
             *
             * Capping the panel is the obvious way to keep a tall dialog on
             * screen and it is what produced the bug this replaced: the browser
             * laid the children out against the panel's unclamped height, then
             * clamped the box, and never recomputed the children. The result
             * was a panel whose contents sat ~376px above their own container,
             * clipped at the top and leaving several hundred pixels of empty
             * panel below the footer. It appeared whenever a re-render changed
             * the content height while the dialog was open — ticking a college
             * in Add User did it every time (ASSUMPTIONS.md G23).
             *
             * The cap now lives on the body instead. The panel is therefore
             * always exactly as tall as it needs to be, nothing clamps it, and
             * there is no second height for the layout to disagree about.
             *
             * `overflow-clip`, not `overflow-hidden`. A `hidden` box is still a
             * scroll container — it just has no visible scrollbar — and the
             * browser will happily scroll one to bring a focused element into
             * view. Clicking a college in Add User moves focus to the
             * visually-hidden checkbox inside the label, and while the panel was
             * momentarily taller than its cap the browser scrolled it by ~490px
             * to reveal that checkbox. The panel then stayed scrolled: header
             * gone above the fold, and 490px of empty panel below the footer.
             * `clip` produces the same clipping with no scroll container at all,
             * so there is nothing left to scroll (ASSUMPTIONS.md G23).
             */
            'flex flex-col overflow-clip',
            className,
          )}
          {...props}
        >
          {children}
          {!hideClose ? (
            <DialogPrimitive.Close
              className="absolute right-3 top-3 rounded p-1 text-ink-subtle transition-colors hover:bg-surface-sunken hover:text-ink"
              aria-label="Close dialog"
            >
              <X className="size-4" aria-hidden="true" />
            </DialogPrimitive.Close>
          ) : null}
        </DialogPrimitive.Content>
      </div>
    </DialogPrimitive.Portal>
  );
});

export function DialogHeader({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element {
  return (
    <div
      className={cn('shrink-0 space-y-1 border-b border-glass-edge px-5 py-4 pr-12', className)}
      {...props}
    />
  );
}

export const DialogTitle = React.forwardRef<
  React.ComponentRef<typeof DialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(function DialogTitle({ className, ...props }, ref) {
  return (
    <DialogPrimitive.Title
      ref={ref}
      className={cn('text-base font-semibold text-ink', className)}
      {...props}
    />
  );
});

export const DialogDescription = React.forwardRef<
  React.ComponentRef<typeof DialogPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(function DialogDescription({ className, ...props }, ref) {
  return (
    <DialogPrimitive.Description
      ref={ref}
      className={cn('text-sm text-ink-muted', className)}
      {...props}
    />
  );
});

export function DialogBody({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element {
  /*
   * The only part that scrolls, and the only part with a height limit.
   *
   * `16rem` is the room reserved for the header, the footer and the margin
   * around the panel, so the whole dialog fits the viewport at any height
   * without the panel itself ever being clamped — see the note on
   * `DialogContent`.
   */
  return (
    <div
      className={cn('max-h-[calc(100vh-16rem)] overflow-y-auto px-5 py-4', className)}
      {...props}
    />
  );
}

export function DialogFooter({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element {
  return (
    <div
      className={cn(
        'shrink-0 flex flex-col-reverse gap-2 border-t border-glass-edge bg-glass-sunken px-5 py-3 sm:flex-row sm:justify-end',
        className,
      )}
      {...props}
    />
  );
}

/* -------------------------------------------------------------------------- */
/* Confirmation dialog                                                        */
/* -------------------------------------------------------------------------- */

export interface ConfirmDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title: string;
  description: string;
  confirmLabel?: string;
  cancelLabel?: string;
  tone?: 'default' | 'danger';
  loading?: boolean;
  /**
   * When set, the action requires a typed reason before it can proceed. Used
   * for every transition the workflow marks `requiresReason` — rejections,
   * voids, overrides (SPEC.md §33.13).
   */
  reason?: {
    label: string;
    value: string;
    onChange: (value: string) => void;
    placeholder?: string;
  };
  /**
   * A refusal from the server, shown in place rather than closing the dialog.
   *
   * Some of these actions are refused for reasons the caller could not have
   * known in advance — the last active superadmin, an account with history that
   * cannot be deleted. Closing the dialog and losing the explanation would
   * leave the user pressing the same button again.
   */
  error?: string | null;
  onConfirm: () => void;
}

/**
 * Confirmation for irreversible or high-risk operations (SPEC.md §6).
 *
 * When a reason is required, the confirm button stays disabled until one is
 * typed — the same rule the server enforces, surfaced early so the user is not
 * told "no" only after submitting.
 */
export function ConfirmDialog({
  open,
  onOpenChange,
  title,
  description,
  confirmLabel = 'Confirm',
  cancelLabel = 'Cancel',
  tone = 'default',
  loading = false,
  reason,
  error = null,
  onConfirm,
}: ConfirmDialogProps): React.JSX.Element {
  const reasonMissing = Boolean(reason) && reason!.value.trim().length === 0;

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-md">
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
          <DialogDescription>{description}</DialogDescription>
        </DialogHeader>

        {error ? (
          <DialogBody className={reason ? 'pb-0' : undefined}>
            <p role="alert" className="rounded-[var(--radius-control)] bg-danger-bg px-3 py-2 text-sm text-danger">
              {error}
            </p>
          </DialogBody>
        ) : null}

        {reason ? (
          <DialogBody>
            <label htmlFor="confirm-reason" className="text-sm font-medium text-ink">
              {reason.label}
              <span aria-hidden="true" className="ml-0.5 text-danger">
                *
              </span>
              <span className="sr-only"> (required)</span>
            </label>
            <textarea
              id="confirm-reason"
              value={reason.value}
              onChange={(event) => reason.onChange(event.target.value)}
              placeholder={reason.placeholder}
              rows={3}
              required
              className={cn(
                'mt-1.5 w-full rounded-[var(--radius-control)] border border-border-strong bg-white',
                'px-3 py-2 text-sm text-ink placeholder:text-ink-subtle',
              )}
            />
            <p className="mt-1 text-xs text-ink-subtle">
              This reason is recorded permanently in the audit trail.
            </p>
          </DialogBody>
        ) : null}

        <DialogFooter>
          <Button variant="secondary" onClick={() => onOpenChange(false)} disabled={loading}>
            {cancelLabel}
          </Button>
          <Button
            variant={tone === 'danger' ? 'danger' : 'primary'}
            onClick={onConfirm}
            loading={loading}
            disabled={reasonMissing}
          >
            {confirmLabel}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}
