import React from 'react'
import { View, Platform } from 'react-native'
import PropTypes from 'prop-types'

import { useThemeTokens } from '../ThemeProvider/useThemeTokens'
import { a11yProps } from '../utils/props/a11yProps'
import { copyPropTypes } from '../utils/props/copyPropTypes'
import { getTokensPropType } from '../utils/props/tokens'
import { selectSystemProps } from '../utils/props/selectSystemProps'
import { useCopy } from '../utils/useCopy'
import { variantProp } from '../utils/props/variantProp'
import { viewProps } from '../utils/props/viewProps'
import { convertFromMegaByteToByte } from '../utils/convertFromMegaByteToByte'

import { InputLabel } from '../InputLabel/InputLabel'
import { Button } from '../Button/Button'
import { Notification } from '../Notification/Notification'
import { Spacer } from '../Spacer/Spacer'
import { StackView } from '../StackView/StackView'

import { NotificationContent } from './NotificationContent'
import { dictionary as defaultDictionary } from './dictionary'

const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])

const getHintFromFileTypes = (fileTypes) => {
  const acceptedFileTypes = fileTypes.map((type) => type.toUpperCase())

  return acceptedFileTypes.reduce((acc, curr, index) => {
    if (index === 0) {
      return `${curr}`
    }
    return `${acc}, ${curr}`
  }, '')
}

const stringifyFileTypesList = (fileTypes, getCopy) => {
  const acceptedFileTypes = fileTypes.map((type) => `.${type.toUpperCase()}`)

  return acceptedFileTypes.reduce((acc, curr, index) => {
    if (index === 0) {
      return curr
    }
    if (index === acceptedFileTypes.length - 1) {
      return `${acc} ${getCopy('or')} ${curr}`
    }
    return `${acc}, ${curr}`
  }, '')
}

const selectComponentTokens = (tokens, componentIdentifier) => {
  return Object.entries(tokens).reduce((acc, [key, value]) => {
    if (key.startsWith(componentIdentifier) && value !== null) {
      const newKey =
        key.replace(componentIdentifier, '').replace('Text', '').charAt(0).toLowerCase() +
        key.replace(componentIdentifier, '').replace('Text', '').slice(1)
      acc[newKey] = value
    }
    return acc
  }, {})
}

/**
 * FileUpload component for uploading files.
 *
 * @component
 * @example
 * // Usage:
 * <FileUpload
 *   tokens={tokens}
 *   variant={variant}
 *   copy={copy}
 *   fileTypes={fileTypes}
 *   allowMultipleFiles={allowMultipleFiles}
 *   maxFileSize={maxFileSize}
 *   maxFilesCount={maxFilesCount}
 *   onUpload={onUpload}
 *   onDelete={onDelete}
 *   documentPicker={documentPicker}
 *   {...rest}
 * />
 *
 * @param {Object} props - The component props.
 * @param {Object} props.tokens - The tokens for styling the component.
 * @param {string} props.variant - The variant of the component.
 * @param {string} props.copy - The copy for the component.
 * @param {string[]} props.fileTypes - The allowed file types for upload.
 * @param {boolean} props.allowMultipleFiles - Whether multiple files can be uploaded.
 * @param {number} props.maxFileSize - The maximum file size in megabytes.
 * @param {number} props.maxFilesCount - The maximum number of files that can be uploaded.
 * @param {number} props.minFilesCount - The minimum number of files that can be uploaded.
 * @param {number} props.minFileSize - The minimum file size in megabytes.
 * @param {Function} props.onUpload - The callback function for file upload.
 * @param {Function} props.onDelete - The callback function for file deletion.
 * @param {Object} props.documentPicker - The document picker object.
 * @param {any} props.rest - Additional props to be spread to the component.
 * @returns {JSX.Element} The rendered FileUpload component.
 */
export const FileUpload = React.forwardRef(
  (
    {
      tokens,
      variant,
      copy = 'en',
      fileTypes,
      allowMultipleFiles = false,
      maxFileSize,
      maxFilesCount = 1,
      minFilesCount = 1,
      minFileSize = 0,
      onUpload,
      onDelete,
      documentPicker,
      dictionary = defaultDictionary,
      ...rest
    },
    ref
  ) => {
    if (minFilesCount <= 0) {
      throw new Error('minFilesCount should be greater than 0')
    }

    const themeTokens = useThemeTokens('FileUpload', tokens, variant)
    const getCopy = useCopy({ dictionary, copy })

    const inputFileRef = React.useRef(null)

    const [filesStatus, setFilesStatus] = React.useState([])
    const [renderTrigger, setRenderTrigger] = React.useState(0)

    React.useEffect(() => {
      setRenderTrigger((prev) => prev + 1)
    }, [filesStatus])

    const areFileTypesExtensionsValid = (files) => {
      const invalidFiles = files.filter((file) => {
        const fileExtension = file.name.split('.').pop().toLowerCase()
        return !fileTypes.includes(fileExtension)
      })
      if (invalidFiles.length > 0) {
        setFilesStatus([
          {
            description: getCopy('wrongFileType').replace(
              '%{fileType}',
              stringifyFileTypesList(fileTypes, getCopy)
            ),
            style: 'error'
          }
        ])
        return false
      }
      return true
    }

    const areFileSizesWithinLimit = (files) => {
      const invalidFiles = files.filter((file) => {
        const fileSize = file.size
        return (
          fileSize < convertFromMegaByteToByte(minFileSize) ||
          fileSize > convertFromMegaByteToByte(maxFileSize)
        )
      })

      if (invalidFiles.length > 0) {
        const invalidFile = invalidFiles[0]
        const fileSize = invalidFile.size
        if (fileSize < convertFromMegaByteToByte(minFileSize)) {
          setFilesStatus([
            {
              description: getCopy('fileTooSmall').replace('%{size}', minFileSize),
              style: 'error'
            }
          ])
        } else {
          setFilesStatus([
            {
              description: getCopy('fileTooBig').replace('%{size}', maxFileSize),
              style: 'error'
            }
          ])
        }
        return false
      }
      return true
    }

    const areFilesCountWithinLimit = (files) => {
      if (maxFilesCount && files.length > maxFilesCount) {
        setFilesStatus([
          {
            description: getCopy('tooManyFiles').replace('%{maxFilesCount}', maxFilesCount),
            style: 'error'
          }
        ])
        return false
      }
      if (minFilesCount && files.length < minFilesCount) {
        setFilesStatus([
          {
            description: getCopy('fewFiles').replace('%{minFilesCount}', minFilesCount),
            style: 'error'
          }
        ])
        return false
      }
      return true
    }

    const handleFilesUpload = async (files) => {
      try {
        const results = await onUpload(files)
        const currentFilesStatus = files.map((file, index) => {
          if (results[index].error) {
            return {
              description: (
                <NotificationContent
                  file={file}
                  color={selectComponentTokens(themeTokens, 'notification').color}
                >{` ${getCopy('uploadError')} ${results[index].error}.`}</NotificationContent>
              ),
              style: 'error'
            }
          }
          return {
            description: (
              <NotificationContent
                file={file}
                color={selectComponentTokens(themeTokens, 'notification').color}
              >{` ${getCopy('uploadSuccess')}`}</NotificationContent>
            ),
            style: 'success',
            file,
            onDismiss: () => handleFileDeletion(file)
          }
        })
        setFilesStatus(currentFilesStatus)
      } catch (error) {
        setFilesStatus([
          {
            description: allowMultipleFiles
              ? getCopy('problemUploadingMultipleFiles')
              : getCopy('problemUploading'),
            style: 'error'
          }
        ])
      }
    }

    const handleFileDeletion = async (file) => {
      try {
        const result = await onDelete(file)
        setFilesStatus((prevStatus) => {
          const filteredFilesStatus = prevStatus.filter((f) => f.file && f.file.name !== file.name)
          return result.error
            ? [
                ...filteredFilesStatus,
                {
                  description: (
                    <NotificationContent
                      file={file}
                      color={selectComponentTokens(themeTokens, 'notification').color}
                    >{` ${getCopy('uploadSuccess')}`}</NotificationContent>
                  ),
                  style: 'success',
                  file,
                  onDismiss: () => handleFileDeletion(file)
                },
                {
                  description: (
                    <NotificationContent
                      file={file}
                      color={selectComponentTokens(themeTokens, 'notification').color}
                    >{` ${getCopy('deleteProblem')} ${result.error}.`}</NotificationContent>
                  ),
                  style: 'error',
                  file
                }
              ]
            : filteredFilesStatus
        })
      } catch (error) {
        setFilesStatus([
          {
            description: (
              <NotificationContent
                file={file}
                color={selectComponentTokens(themeTokens, 'notification').color}
              >{` ${getCopy('problemDeleting')}`}</NotificationContent>
            ),
            style: 'error'
          }
        ])
      }
    }

    const handleOnChange = async (event) => {
      const targetFiles = event.target.files
      const files = Array.from(targetFiles)

      if (files.length === 0) return

      if (fileTypes && !areFileTypesExtensionsValid(files)) return

      if (maxFileSize && !areFileSizesWithinLimit(files)) return

      if ((maxFilesCount || minFilesCount) && !areFilesCountWithinLimit(files)) return

      await handleFilesUpload(files)

      if (inputFileRef.current) inputFileRef.current.value = ''
    }

    const onFileUploadButtonClick = async () => {
      if (Platform.OS === 'web') {
        inputFileRef.current.click()
      } else {
        try {
          const result = await documentPicker.getDocumentAsync({
            multiple: allowMultipleFiles
          })
          if (result) {
            if (result.canceled) return
            const files = Array.isArray(result.assets) ? result.assets : [result.assets]
            const event = { target: { files } }
            await handleOnChange(event)
          }
        } catch (_) {
          setFilesStatus([
            {
              description: allowMultipleFiles
                ? getCopy('problemUploadingFiles')
                : getCopy('problemUploading'),
              style: 'error'
            }
          ])
        }
      }
    }

    return (
      <View ref={ref} {...selectProps(rest)}>
        <InputLabel
          label={getCopy('label')}
          hint={
            fileTypes &&
            getCopy('allowedFileTypes').replace('%{fileTypes}', getHintFromFileTypes(fileTypes))
          }
          hintPosition="below"
        />
        <Spacer space={4} />
        {Platform.OS === 'web' && (
          <input
            type="file"
            multiple={allowMultipleFiles}
            ref={inputFileRef}
            hidden={true}
            accept={fileTypes && fileTypes.map((type) => `.${type}`).join(', ')}
            onChange={handleOnChange}
          />
        )}
        <Button
          onClick={onFileUploadButtonClick}
          tokens={selectComponentTokens(themeTokens, 'button')}
        >
          {getCopy('buttonLabel')}
        </Button>
        {filesStatus.length > 0 && (
          <View>
            <Spacer space={4} />
            <StackView space={4}>
              {filesStatus.map(({ description, style, onDismiss }, index) => (
                <Notification
                  key={`${index.toString()}-${renderTrigger}`}
                  variant={{ style }}
                  dismissible={!!onDismiss}
                  onDismiss={onDismiss}
                  tokens={selectComponentTokens(themeTokens, 'notification')}
                >
                  {description}
                </Notification>
              ))}
            </StackView>
          </View>
        )}
      </View>
    )
  }
)

FileUpload.displayName = 'FileUpload'

const dictionaryEntryShape = PropTypes.shape({
  label: PropTypes.string.isRequired,
  buttonLabel: PropTypes.string.isRequired,
  dismissButtonLabel: PropTypes.string.isRequired,
  wrongFileType: PropTypes.string.isRequired,
  allowedFileTypes: PropTypes.string.isRequired,
  fileTooBig: PropTypes.string.isRequired,
  fileIsEmpty: PropTypes.string.isRequired,
  problemUploading: PropTypes.string.isRequired,
  problemDeleting: PropTypes.string.isRequired,
  problemUploadingMultipleFiles: PropTypes.string.isRequired,
  only: PropTypes.string.isRequired,
  and: PropTypes.string.isRequired,
  or: PropTypes.string.isRequired,
  uploadSuccess: PropTypes.string.isRequired,
  uploadError: PropTypes.string.isRequired,
  deleteProblem: PropTypes.string.isRequired,
  tooManyFiles: PropTypes.string.isRequired,
  fileTooSmall: PropTypes.string.isRequired,
  fewFiles: PropTypes.string.isRequired
})

FileUpload.propTypes = {
  ...selectedSystemPropTypes,
  tokens: getTokensPropType('FileUpload'),
  variant: variantProp.propType,
  /**
   * To define the locale of the copy
   */
  copy: copyPropTypes,
  /**
   * The file types allowed for upload. Example: fileTypes={['pdf', 'docx']}.
   */
  fileTypes: PropTypes.arrayOf(PropTypes.string),
  /**
   * Whether to allow multiple files to be uploaded.
   */
  allowMultipleFiles: PropTypes.bool,
  /**
   * The maximum file size allowed for upload in MB.
   */
  maxFileSize: PropTypes.number,
  /**
   * The maximum number of files allowed for upload.
   */
  maxFilesCount: PropTypes.number,
  /**
   * Callback function called when a file is uploaded.
   */
  onUpload: PropTypes.func,
  /**
   * Callback function called when a file is deleted
   */
  onDelete: PropTypes.func,
  /**
   * The document picker to use for mobile
   */
  documentPicker: PropTypes.object,
  /**
   * The minimum number of files allowed for upload.
   */
  minFilesCount: PropTypes.number,
  /**
   * The minimum file size allowed for upload in MB.
   */
  minFileSize: PropTypes.number,
  /**
   * Custom dictionary containing the labels to use for the steps
   */
  dictionary: PropTypes.shape({
    en: dictionaryEntryShape,
    fr: dictionaryEntryShape
  })
}
