import { clsx } from 'clsx';
import { useId, useRef } from 'react';

import { Sentiment } from '../common';
import { InlinePrompt, type InlinePromptProps } from '../prompt';
import {
  FieldLabelContextProvider,
  InputDescribedByProvider,
  InputIdContextProvider,
  InputInvalidProvider,
} from '../inputs/contexts';
import { Label } from '../label';

export type FieldProps = {
  /** `null` disables auto-generating the `id` attribute, falling back to nesting-based label association over setting `htmlFor` explicitly. */
  id?: string | null;
  /** Should be specified unless the wrapped control has its own labeling mechanism, e.g. `Checkbox`. */
  label?: React.ReactNode;
  /** @default true */
  required?: boolean;
  /** @deprecated Use `description` prop instead. */
  hint?: React.ReactNode;
  message?: React.ReactNode;
  /**
   * Override for the [InlinePrompt icon's default, accessible name](/?path=/docs/other-statusicon-accessibility--docs)
   * announced by the screen readers
   * */
  messageIconLabel?: string;
  /**
   * If true, shows a loading spinner in place of the message icon of the InlinePrompt
   * @default false
   */
  messageLoading?: boolean;
  description?: React.ReactNode;
  /** @deprecated Use `message` and `type={Sentiment.NEGATIVE}` prop instead. */
  error?: React.ReactNode;
  /** @default Sentiment.NEUTRAL */
  sentiment?: InlinePromptProps['sentiment'];
  className?: string;
  children?: React.ReactNode;
};

export const Field = ({
  id,
  label,
  required = true,
  message: propMessage,
  messageIconLabel,
  messageLoading,
  hint,
  description = hint,
  sentiment: propType = Sentiment.NEUTRAL,
  className,
  children,
  ...props
}: FieldProps) => {
  const labelRef = useRef<HTMLLabelElement>(null);
  const sentiment = props.error ? Sentiment.NEGATIVE : propType;
  const message = propMessage || props.error;
  const hasError = sentiment === Sentiment.NEGATIVE;

  const labelId = useId();

  const fallbackInputId = useId();
  const inputId = id !== null ? (id ?? fallbackInputId) : undefined;

  const messageId = useId();
  const descriptionId = useId();

  /**
   * form control can have multiple messages to describe it,
   * e.g the description underneath the label and inline alert
   */
  function ariaDescribedbyByIds() {
    const messageIds = [];
    if (description) {
      messageIds.push(descriptionId);
    }
    if (message) {
      messageIds.push(messageId);
    }
    return messageIds.length > 0 ? messageIds.join(' ') : undefined;
  }

  return (
    <FieldLabelContextProvider value={{ id: labelId, ref: labelRef }}>
      <InputIdContextProvider value={inputId}>
        <InputDescribedByProvider value={ariaDescribedbyByIds()}>
          <InputInvalidProvider value={hasError}>
            <div
              className={clsx(
                'np-field form-group d-block',
                {
                  'has-success': sentiment === Sentiment.POSITIVE,
                  'has-warning': sentiment === Sentiment.WARNING,
                  'has-error': hasError,
                  'has-info': sentiment === Sentiment.NEUTRAL,
                },
                className,
              )}
            >
              {label != null ? (
                <>
                  <Label ref={labelRef} id={labelId} htmlFor={inputId}>
                    {required ? label : <Label.Optional>{label}</Label.Optional>}
                  </Label>
                  <Label.Description id={descriptionId}>{description}</Label.Description>
                  <div className="np-field-control">{children}</div>
                </>
              ) : (
                children
              )}

              {message && (
                <InlinePrompt
                  sentiment={sentiment}
                  id={messageId}
                  mediaLabel={messageIconLabel}
                  className="np-field__prompt"
                  loading={messageLoading}
                  width="full"
                >
                  {message}
                </InlinePrompt>
              )}
            </div>
          </InputInvalidProvider>
        </InputDescribedByProvider>
      </InputIdContextProvider>
    </FieldLabelContextProvider>
  );
};
