import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Debug from 'debug';

import type { I18nFormatter } from '~/src/lib/i18n';
import type { FC, RefObject } from 'react';
import type {
  CropImageDialogOnDone,
  CropImageResult,
} from '../CropImageDialog';
import type { FilePickerApi, FilePickerProps } from '../FilePicker';
import type { FormApi, FormOnSubmit } from '../Form';
import type { Icon } from '../Icon/toIcon';

import locales from '~/locales/fragments/customBrandDialog';
import ServiceButton from '~/src/components/ItemPage/sections/lib/ServiceButton';
import useFetchSessionUser from '~/src/hooks/useFetchSessionUser';
import useIsLargeScreen from '~/src/hooks/useIsLargeScreen';
import { resolveServiceDataFromUrl } from '~/src/lib/getServiceDisplayData';
import { useI18nStatic } from '~/src/lib/i18n';
import deleteImage from '~/src/lib/image/deleteImage';
import useUploadImage from '~/src/lib/image/useUploadImage';
import darkTheme from '~/src/lib/theme/dark';
import { createImage, testImageHasTransparency } from '~/src/lib/utils/image';
import Box from '../Box';
import Card from '../Card';
import CardAction from '../Card/CardAction';
import Clickable from '../Clickable';
import CropImageDialog from '../CropImageDialog';
import DashedBox from '../DashedBox';
import ErrorText from '../ErrorText';
import FadeOnMount from '../FadeOnMount';
import FilePicker from '../FilePicker';
import Form, { FORM_SECTION_SPACING } from '../Form';
import { BitmapIconHoverTarget, toBitmapIcon } from '../Icon/BitmapIcon';
import ImageIcon from '../Icon/ImageIcon';
import InputLabel from '../InputLabel';
import Loading from '../Loading';
import { useAppAlert, useAppConfirm } from '../NextApp/lib/CoreUi';
import SwitchSetting from '../Switch/SwitchSetting';
import Text from '../Text';
import TextInput from '../TextInput';
import TextInputInnerBox from '../TextInput/TextInputInnerBox';

const debug = Debug('songwhip/CustomBrandForm');
const SCALED_ICON_WIDTH = 100;

export interface CustomBrandFormProps {
  apiRef?: RefObject<FormApi | null>;
  initialDomain?: string;
  initialBrandName?: string;
  initialIconImage?: string;
  initialDynamicColoring?: boolean;

  /**
   * A list of domains already allocated used to validate the domain
   * input is not already in use.
   */
  existingDomains?: string[];

  onChange: (params: { isLoading: boolean }) => void;
  onDelete: (params: { domain: string }) => void;

  onSubmit: (params: {
    iconImage: string;
    domain: string;
    brandName: string;
    dynamicColoring: boolean;
  }) => void;
}

const CustomBrandForm = ({
  initialBrandName,
  initialDomain,
  initialIconImage,
  initialDynamicColoring,
  existingDomains = [],
  onSubmit,
  onDelete,
  onChange,
  apiRef,
}: CustomBrandFormProps) => {
  const [pickedImage, setPickedImage] = useState<CropImageResult>();
  const { uploadImage } = useUploadImage({ trackingId: 'customBrandIcon' });
  const currentImageSrc = pickedImage?.src || initialIconImage;
  const [brandName, setBrandName] = useState(initialBrandName);
  const { t, tx } = useI18nStatic<'customBrandDialog'>(locales);
  const [isLoading, setIsLoading] = useState(false);
  const appConfirm = useAppConfirm();
  const appAlert = useAppAlert();
  const isEditMode = !!initialDomain;
  const { userAccountId } = useFetchSessionUser();

  const [dynamicColoring, setDynamicColoring] = useState(
    initialDynamicColoring ?? true
  );

  const PreviewIcon = useMemo<Icon | undefined>(
    () =>
      currentImageSrc
        ? toBitmapIcon({ src: currentImageSrc, dynamicColoring })
        : undefined,
    [currentImageSrc, dynamicColoring]
  );

  useEffect(() => {
    onChange({
      isLoading,
    });
  }, [isLoading]);

  const onSubmitInternal = useCallback<
    FormOnSubmit<{ domain: string; brandName: string }>
  >(
    async ({ values: { brandName, domain } }) => {
      let uploadedImageSrc: string | undefined;

      const changes: {
        brandName?: string;
        domain?: string;
        iconImage?: CropImageResult;
        dynamicColoring?: boolean;
      } = {};

      if (brandName !== initialBrandName) changes.brandName = brandName;
      if (domain !== initialDomain) changes.domain = domain;
      if (pickedImage) changes.iconImage = pickedImage;

      if (dynamicColoring !== initialDynamicColoring) {
        changes.dynamicColoring = dynamicColoring;
      }

      const didChange = !!Object.keys(changes).length;

      if (!didChange) {
        debug('abort submit: nothing changed', {
          domain,
          brandName,
          pickedImage,
        });

        return;
      }

      try {
        setIsLoading(true);

        // if a new image was picked, uploaded it
        if (pickedImage) {
          uploadedImageSrc = (
            await uploadImage({
              file: pickedImage.file,
              scaleToWidth: SCALED_ICON_WIDTH,
              asType: 'png',
            })
          ).url;
        }

        await onSubmit({
          brandName: brandName!,
          domain: domain!,
          // if a new image was uploaded use that, else use the original image
          iconImage: uploadedImageSrc ?? initialIconImage!,
          dynamicColoring,
        });

        const iconChanged = !!(initialIconImage && uploadedImageSrc);

        // if submit succeeds and there's an `initialIconImage`
        // then delete it from songwhip-images to cleanup unused
        if (iconChanged) {
          debug('icon replaced: deleting prev icon image');
          deleteImage(initialIconImage, userAccountId);
        }
      } catch (error) {
        appAlert({
          title: t('imageErrorAlertTitle'),
          content: <ErrorText error={error} />,
        });

        // cleanup uploaded image after throw
        if (uploadedImageSrc) {
          deleteImage(uploadedImageSrc, userAccountId);
        }
      } finally {
        setIsLoading(false);
      }
    },
    [pickedImage, currentImageSrc, initialIconImage, dynamicColoring]
  );

  // If an initial domain was passed in then it's means user is editing
  // an existing item so disable domain field as it's immutable.
  const domainDisabled = isEditMode;

  const onDeleteInternal = useCallback(async () => {
    debug('on delete');

    const confirmed = await appConfirm({
      titleText: t('deleteConfirmTitle'),
      actionText: t('deleteConfirmAction'),

      content: tx('deleteConfirmText', {
        domain: initialDomain!,
      }),
    });

    if (!confirmed) {
      debug('delete aborted');
      return;
    }

    try {
      debug('delete confirmed');
      await onDelete({ domain: initialDomain! });

      if (initialIconImage) {
        debug('deleting image', initialIconImage);
        deleteImage(initialIconImage, userAccountId);
      }
    } catch (error) {
      appAlert({
        content: <ErrorText error={error} />,
      });
    }
  }, [initialDomain, initialIconImage]);

  if (isLoading) {
    return (
      <FadeOnMount>
        <Loading coverParent />
      </FadeOnMount>
    );
  }

  return (
    <div>
      <Form
        apiRef={apiRef}
        trackingId="customBrands"
        onSubmit={onSubmitInternal}
        onChange={(params) => {
          debug('on change', params);
        }}
      >
        <button type="submit" hidden />
        <InputLabel
          value={t('domainInputLabel')}
          description={
            !domainDisabled
              ? t('domainInputDescription')
              : t('domainInputDescriptionDisabled')
          }
        >
          <TextInput
            required
            name="domain"
            placeholder="example.com"
            testId="domainInput"
            // If an initial domain was passed in then it's means user is editing
            // an existing item so disable domain field as it's immutable.
            isDisabled={domainDisabled}
            autoFocus
            defaultValue={initialDomain}
            renderBefore={() => {
              return (
                <TextInputInnerBox>
                  <Text size="0.9em">https://</Text>
                </TextInputInnerBox>
              );
            }}
            onChange={({ value, setValue }) => {
              const cleaned = value
                // remove schema
                .replace(/^https?:\/\//, '')
                // remove spaces
                .replace(/\s/g, '')
                .toLowerCase();

              setValue(cleaned);
            }}
            toValidationMessage={({ value }) => {
              const isDomain = /\w+\.\w+/.test(value);

              if (value && !isDomain) {
                return 'Not a valid domain';
              }

              for (const domain of existingDomains) {
                if (value === domain) {
                  return 'Domain in use';
                }
              }

              const service = resolveServiceDataFromUrl('https://' + value);

              if (service.match) {
                return `Domain reserved by ${service.name}`;
              }
            }}
          />
        </InputLabel>
        <InputLabel
          margin={`${FORM_SECTION_SPACING} 0 0`}
          value={t('brandNameInput.label')}
          description={t('brandNameInput.description')}
        >
          <TextInput
            name="brandName"
            placeholder={t('brandNameInput.placeholder')}
            required
            maxLength={35}
            testId="brandNameInput"
            defaultValue={initialBrandName}
            // update state to populate button preview
            onInputEnd={({ value }) => {
              setBrandName(value.trim());
            }}
          />
        </InputLabel>
        <IconPickerSection
          dynamicColoring={dynamicColoring}
          PreviewIcon={PreviewIcon}
          onChange={(croppedImage) => {
            debug('image change', croppedImage);
            setPickedImage(croppedImage);
            setDynamicColoring(dynamicColoring);
          }}
        />
        {PreviewIcon && (
          <SwitchSetting
            margin={`${FORM_SECTION_SPACING} 0 0`}
            title={t('dynamicColoring.title')}
            description={t('dynamicColoring.description')}
            value={dynamicColoring}
            testId="dynamicColoringSwitch"
            onChange={(value) => {
              setDynamicColoring(value);
            }}
          />
        )}
        <InputLabel
          margin={`${FORM_SECTION_SPACING} 0 0`}
          value={t('buttonPreview.title')}
          description={t('buttonPreview.description')}
          // FUN-FACT: When a <button> is inside a <label>, hovering the <label> triggers
          // :hover styles on the <button>. We don't want this so using <div> instead.
          tag="div"
        >
          <DashedBox padding="1.8rem 1.3rem">
            <ServiceButton
              testId="serviceButtonPreview"
              text={brandName || 'Your brand'}
              height="4.8rem"
              Icon={PreviewIcon}
            />
          </DashedBox>
        </InputLabel>
      </Form>
      {isEditMode && (
        <Clickable onClick={onDeleteInternal} testId="deleteBrand">
          <Text color={darkTheme.colorDanger} size="1.6rem" margin="2.3rem 0 0">
            {t('deleteAction')}
          </Text>
        </Clickable>
      )}
    </div>
  );
};

const IconPickerSection: FC<{
  dynamicColoring: boolean;
  onChange: (params: CropImageResult) => void;
  PreviewIcon?: Icon;
}> = ({ onChange: onChange, PreviewIcon, dynamicColoring }) => {
  const appAlert = useAppAlert();
  const [srcImage, setSrcImage] = useState<File>();
  const filePickerApiRef = useRef<FilePickerApi>(null);
  const { t, tx } = useI18nStatic<'customBrandDialog'>(locales);
  const isLargeScreen = useIsLargeScreen();

  const onDialogClose = useCallback(() => {
    setSrcImage(undefined);
  }, []);

  const onImageCrop = useCallback<CropImageDialogOnDone>(
    async ({ result, close }) => {
      debug('on image crop', result);

      await close();
      setSrcImage(undefined);

      onChange(result);
    },
    [onChange]
  );

  const onFileChange = useCallback<FilePickerProps['onChange']>(({ file }) => {
    debug('file changed', file);

    if (!file) return;

    checkImageIsValid(file, t)
      .then(() => {
        setSrcImage(file);
      })
      .catch((error: Error) => {
        debug('invalid image', error);

        // clear the value after
        filePickerApiRef.current?.clear();

        appAlert({
          title: 'Image unsuitable',
          content: error.message,
        });
      });
  }, []);

  return (
    <section>
      <InputLabel
        tag="div"
        margin={`${FORM_SECTION_SPACING} 0 0`}
        value=""
        description={tx('iconPickerDescription')}
      >
        {(() => {
          if (PreviewIcon) {
            return (
              <Card>
                <Box flexRow height="8rem" data-testid="iconPreview">
                  <BitmapIconHoverTarget
                    flexGrow
                    style={{ background: '#111' }}
                    centerContent
                    className="darkSection"
                  >
                    <PreviewIcon
                      size="4rem"
                      testId="iconPreviewDark"
                      coloring={dynamicColoring && 'light'}
                    />
                  </BitmapIconHoverTarget>
                  <BitmapIconHoverTarget
                    flexGrow
                    style={{ background: '#eee' }}
                    centerContent
                    className="lightSection"
                  >
                    <PreviewIcon
                      size="4rem"
                      testId="iconPreviewLight"
                      coloring={dynamicColoring && 'dark'}
                    />
                  </BitmapIconHoverTarget>
                </Box>
                <FilePicker
                  accept={['image/png']}
                  onChange={onFileChange}
                  apiRef={filePickerApiRef}
                  testId="filePicker"
                  tabIndex={-1}
                >
                  <CardAction text={t('iconPickerActionChange')} tag="div" />
                </FilePicker>
              </Card>
            );
          }

          return (
            <FilePicker
              accept={['image/png']}
              onChange={onFileChange}
              apiRef={filePickerApiRef}
              validationMessage={t('iconPickerRequired')}
              testId="filePicker"
            >
              <Card centerContent height="12rem" flexColumn>
                <ImageIcon size="4rem" color="#555" />
                <Text size="1.4rem" isBold margin="0.5em 0 0">
                  {t('iconPickerAction')}
                </Text>
              </Card>
            </FilePicker>
          );
        })()}
      </InputLabel>
      {srcImage && (
        <CropImageDialog
          title={t('cropImage.dialogTitle')}
          originalFile={srcImage}
          description={
            <>
              {isLargeScreen
                ? tx('cropImage.instructionDesktop')
                : tx('cropImage.instructionMobile')}
              {tx('cropImage.imageRequirements')}
            </>
          }
          onDone={onImageCrop}
          onClose={onDialogClose}
        />
      )}
    </section>
  );
};

const checkImageIsValid = async (
  file: File | Blob,
  t: I18nFormatter<'customBrandDialog'>
) => {
  const MAX_BYTES = 1024 * 1024 * 10; // 10mb
  const imageSrc = URL.createObjectURL(file);
  const image = await createImage(imageSrc);
  const imageBytes = file.size;
  const maxBytesExceeded = imageBytes > MAX_BYTES;

  debug('bytes: %s', imageBytes);

  if (maxBytesExceeded) {
    throw new Error(t('imageInvalid.maxBytesExceeded'));
  }

  const resolutionTooLow =
    image.width < SCALED_ICON_WIDTH || image.height < SCALED_ICON_WIDTH;

  debug(
    'resolution %sx%s valid: ',
    image.width,
    image.height,
    !resolutionTooLow
  );

  if (resolutionTooLow) {
    throw new Error(t('imageInvalid.resolutionTooLow'));
  }

  const notTransparent = !testImageHasTransparency(image);
  debug('has transparency: %s', !notTransparent);

  if (notTransparent) {
    throw new Error(t('imageInvalid.notTransparent'));
  }
};

export default CustomBrandForm;
