'use client';

import { Description } from '@tale/ui/description';
import { SkeletonBox } from '@tale/ui/skeleton';
import { useSkeleton } from '@tale/ui/skeleton-context';
import { cva, type VariantProps } from 'class-variance-authority';
import { Eye, EyeOff, Info, XCircle } from 'lucide-react';
import {
  InputHTMLAttributes,
  ReactNode,
  forwardRef,
  useState,
  useId,
  useEffect,
} from 'react';

import { Tooltip } from '@/app/components/ui/overlays/tooltip';
import { useT } from '@/lib/i18n/client';
import { cn } from '@/lib/utils/cn';

import { Label } from './label';

const inputVariants = cva(
  'flex w-full text-base file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-[border-color,box-shadow] duration-150',
  {
    variants: {
      variant: {
        default:
          'rounded-lg border border-transparent bg-input px-3 py-2 ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-1 ring-[color:var(--color-border-input)] focus-visible:ring-primary',
        unstyled: 'bg-transparent border-0 ring-0 ring-offset-0',
      },
      size: {
        default: 'h-10',
        sm: 'h-8',
        lg: 'h-12',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  },
);

type BaseProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> &
  VariantProps<typeof inputVariants> & {
    passwordToggle?: boolean;
    errorMessage?: string;
    isInvalid?: boolean;
    label?: string;
    description?: ReactNode;
    required?: boolean;
    wrapperClassName?: string;
  };

// Plain control — the real input field (+ optional label/description/toggle).
// No skeleton logic of its own.
const InputBase = forwardRef<HTMLInputElement, BaseProps>(
  (
    {
      className,
      type,
      passwordToggle = true,
      autoComplete,
      variant,
      size,
      errorMessage,
      isInvalid,
      label,
      description,
      required,
      wrapperClassName,
      id: providedId,
      style,
      'aria-describedby': callerDescribedBy,
      ...props
    },
    ref,
  ) => {
    const { t } = useT('common');
    const generatedId = useId();
    const id = providedId ?? generatedId;
    const errorId = `${id}-error`;
    const descriptionId = `${id}-description`;
    const isPassword = type === 'password';
    const [show, setShow] = useState(false);
    const [showShake, setShowShake] = useState(false);
    const inputType = isPassword ? (show ? 'text' : 'password') : type;
    const resolvedAutoComplete =
      autoComplete ?? (isPassword ? 'off' : undefined);
    const hasError = !!errorMessage;
    const showInvalid = hasError || !!isInvalid;
    // Merge caller-supplied `aria-describedby` with the internal ids so a
    // parent passing e.g. a counter id doesn't clobber the internal error /
    // description associations the Input owns.
    const describedBy =
      [description && descriptionId, hasError && errorId, callerDescribedBy]
        .filter(Boolean)
        .join(' ') || undefined;

    // Trigger shake animation when error appears
    useEffect(() => {
      if (showInvalid) {
        setShowShake(true);
        const timer = setTimeout(() => setShowShake(false), 400);
        return () => clearTimeout(timer);
      }
      return undefined;
    }, [showInvalid, errorMessage]);

    if (isPassword && passwordToggle) {
      return (
        <div className={cn('flex flex-col gap-1.5', wrapperClassName)}>
          {label && (
            <Label htmlFor={id} required={required} error={hasError}>
              {label}
            </Label>
          )}
          <div className="relative">
            <input
              id={id}
              type={inputType}
              autoComplete={resolvedAutoComplete}
              className={cn(
                inputVariants({ variant, size }),
                showInvalid &&
                  'border-destructive focus-visible:ring-destructive',
                showShake && 'animate-shake',
                'pr-10',
                className,
              )}
              ref={ref}
              required={required}
              aria-invalid={showInvalid || undefined}
              aria-describedby={describedBy}
              aria-errormessage={hasError ? errorId : undefined}
              {...props}
              // Force-clear browser autofill masking when password is revealed.
              // Chrome/Safari apply `-webkit-text-security: disc` via their autofill
              // overlay, which persists even after `type` changes to "text".
              style={{
                ...style,
                ...(show && { WebkitTextSecurity: 'none' }),
              }}
            />
            <Tooltip
              content={show ? t('aria.hidePassword') : t('aria.showPassword')}
            >
              <button
                type="button"
                aria-label={
                  show ? t('aria.hidePassword') : t('aria.showPassword')
                }
                aria-pressed={show}
                className="text-muted-foreground hover:text-foreground absolute inset-y-0 right-2 my-auto inline-flex size-6 items-center justify-center rounded-md transition-colors duration-150"
                onClick={() => setShow((v) => !v)}
              >
                {show ? (
                  <EyeOff className="size-4" aria-hidden="true" />
                ) : (
                  <Eye className="size-4" aria-hidden="true" />
                )}
              </button>
            </Tooltip>
          </div>
          {errorMessage && (
            <p
              id={errorId}
              role="alert"
              aria-live="polite"
              className="text-destructive flex items-center gap-1.5 text-sm"
            >
              <XCircle className="size-4" aria-hidden="true" />
              {errorMessage}
            </p>
          )}
          {description && (
            <Description id={descriptionId}>{description}</Description>
          )}
        </div>
      );
    }

    return (
      <div className={cn('flex flex-col gap-1.5', wrapperClassName)}>
        {label && (
          <Label htmlFor={id} required={required} error={hasError}>
            {label}
          </Label>
        )}
        <input
          id={id}
          type={type}
          autoComplete={resolvedAutoComplete}
          className={cn(
            inputVariants({ variant, size }),
            showInvalid && 'border-destructive focus-visible:ring-destructive',
            showShake && 'animate-shake',
            className,
          )}
          ref={ref}
          required={required}
          aria-invalid={showInvalid || undefined}
          aria-describedby={describedBy}
          aria-errormessage={hasError ? errorId : undefined}
          {...props}
          style={style}
        />
        {errorMessage && (
          <p
            id={errorId}
            role="alert"
            aria-live="polite"
            className="text-destructive flex items-center gap-1.5 text-sm"
          >
            <Info className="size-4" aria-hidden="true" />
            {errorMessage}
          </p>
        )}
        {description && (
          <Description id={descriptionId}>{description}</Description>
        )}
      </div>
    );
  },
);
InputBase.displayName = 'InputBase';

/**
 * Skeleton-aware Input. Inside a `<Skeletonize loading>` it masks the plain
 * control by rendering it inside a `<SkeletonBox>` (laid out invisibly to set
 * the exact size, pulse overlay on top) — no sizing math, no drift.
 */
export const Input = forwardRef<HTMLInputElement, BaseProps>((props, ref) => {
  const loading = useSkeleton();
  if (loading) {
    // `fullWidth` so the mask is block-level: a form field is a block that
    // stacks under its siblings. A bare (inline-block) `SkeletonBox` let
    // stacked fields flow side-by-side while loading, then snap to a column
    // once the real controls mounted — a visible reflow.
    return (
      <SkeletonBox fullWidth>
        <InputBase {...props} ref={ref} />
      </SkeletonBox>
    );
  }
  return <InputBase {...props} ref={ref} />;
});
Input.displayName = 'Input';
