import { useCallback, useImperativeHandle, useRef } from 'react';
import Debug from 'debug';
import mitt from 'mitt';

import type { WithSpacingProps } from '~/src/lib/hocs/withSpacing';
import type { FormEvent, FormEventHandler, ReactNode, RefObject } from 'react';

import useDebounce from '~/src/hooks/useDebounce';
import withSpacing from '~/src/lib/hocs/withSpacing';
import { stripNonDomProps } from '~/src/lib/utils/stripNonDomProps';
import { useTracker } from '../../lib/tracker/useTracker';

const debug = Debug('songwhip/Form');

export const FORM_SECTION_SPACING = '2.2rem';

export interface FormApi {
  submit(): Promise<void>;
  isValid(): boolean;
  clear(): void;
  scrollTo(params: { name: string; delay?: number }): void;
}

export interface FormValues {
  [name: string]: string | boolean;
}

export type FormOnSubmit<TValues extends FormValues> = (params: {
  event: FormEvent<HTMLFormElement>;
  values: TValues;
  form: HTMLFormElement;
}) => Promise<void> | void;

export type FormOnChange = (params: { isValid: boolean }) => void;

export interface FormProps<TValues extends FormValues>
  extends WithSpacingProps {
  children: ReactNode;
  apiRef?: RefObject<FormApi | null>;
  onSubmit?: FormOnSubmit<TValues>;
  onChange?: FormOnChange;
  withTracking?: boolean;
  trackingId?: string;
  testId?: string;
  isDisabled?: boolean;
}

const Form = <TValues extends FormValues>({
  children,
  apiRef,
  onSubmit,
  onChange,
  withTracking = true,
  trackingId,
  testId,
  style = {},
  isDisabled,
  ...withSpacingProps
}: FormProps<TValues>) => {
  const formRef = useRef<HTMLFormElement>(null);
  const { trackEvent } = useTracker();
  const emitter = useRef(mitt());

  // expose submit method via apiRef prop
  useImperativeHandle(
    apiRef,
    () => ({
      submit() {
        return new Promise<void>((resolve, reject) => {
          if (!formRef.current) {
            return reject(
              new Error('Failed to submit form. Missing form ref.')
            );
          }

          emitter.current.on('done', () => {
            resolve();
          });

          formRef.current.requestSubmit();
        });
      },

      isValid() {
        return !!formRef.current?.checkValidity();
      },

      clear() {
        formRef.current?.reset();
      },

      scrollTo({ name, delay = 0 }: { name: string; delay?: number }) {
        const el = formRef.current?.elements.namedItem(name);

        if (el instanceof HTMLElement) {
          setTimeout(() => {
            el.scrollIntoView({ behavior: 'smooth', block: 'center' });
          }, delay);
        }
      },
    }),
    []
  );

  return (
    <form
      {...stripNonDomProps(withSpacingProps)}
      style={{
        ...style,
        ...(isDisabled ? { pointerEvents: 'none', opacity: 0.6 } : {}),
      }}
      data-testid={testId}
      ref={formRef}
      onInvalid={() => {
        emitter.current.emit('done');
      }}
      onInput={useDebounce(
        (event) => {
          debug('on input', event);

          if (onChange) {
            onChange({
              isValid: !!formRef.current?.checkValidity(),
            });
          }
        },
        600,
        []
      )}
      onSubmit={useCallback<FormEventHandler<HTMLFormElement>>(
        async (event) => {
          try {
            event.preventDefault();

            const form = formRef.current;

            if (!onSubmit || !form) return;

            // Don't continue if any form values are invalid. This
            // also shows any native validation prompts.
            if (!form.reportValidity()) {
              // stop this bubbling to any other listeners
              event.stopPropagation();
              return;
            }

            const values = getValues<TValues>(form);

            if (withTracking) {
              trackEvent({
                type: 'form-submit',
                id: trackingId || undefined,
                data: JSON.stringify(values),
              });
            }

            await onSubmit({
              event,
              values,
              form,
            });
          } finally {
            emitter.current.emit('done');
          }
        },
        [onSubmit]
      )}
    >
      {/* you can't add `disabled` attr to a <form> */}
      <fieldset
        disabled={isDisabled}
        style={{
          // make this component effectively disappear from layout,
          // we only need it for it's `disabled` attribute
          display: 'contents',
        }}
      >
        {children}
        {/* nested <button> means Enter key on focused input triggers 'submit' */}
        <button type="submit" hidden />
      </fieldset>
    </form>
  );
};

const getValues = <TValues extends FormValues>(form: HTMLFormElement) => {
  const values = {} as TValues;

  const inputs = form.elements;
  let i = inputs.length;

  while (i--) {
    const el = inputs[i];

    if (
      el instanceof HTMLInputElement ||
      el instanceof HTMLTextAreaElement ||
      el instanceof HTMLSelectElement
    ) {
      if (!el.name) continue;

      if (el.type === 'checkbox') {
        (values as FormValues)[el.name] = (el as HTMLInputElement).checked;
      } else if (el.type === 'radio') {
        if ((el as HTMLInputElement).checked) {
          (values as FormValues)[el.name] = el.value;
        }
      } else {
        (values as FormValues)[el.name] = cleanValue(el.value);
      }
    }
  }

  return values;
};

const cleanValue = (value: string) =>
  value
    .trim()
    // excess space
    .replace(/ +/g, ' ');

const FormWithSpacing = withSpacing(Form);

export default FormWithSpacing as typeof Form;
