import React, { useRef, useState } from 'react'
import { CSVLink } from 'react-csv'
import type Link from 'react-csv/components/Link'

import Button from '../Button/Button'
import { hasValue } from '../../services/HelperServiceTyped'
import Icon from '../Icons/Icon'
import { type IconStringList } from '../Icons/Icon.models'
import isClient from '../../services/isClient'
import Spinner from '../Spinner/Spinner'
import styles from './_single-csv.module.scss'

type CSVLinkRef = {
  link: HTMLAnchorElement
}

type CsvFormat<CsvParams> = {
  /** Api to get the CSV data */
  api: (
    apiArg?: string | number | undefined | unknown,
    csvParams?: unknown,
  ) => Promise<Blob>
  /** Optional api arguments */
  apiArg?: string | number | unknown
  /** Api parameters to be passed in for response filtering */
  params: CsvParams
  /** Optional async downloading. This is useful if the CSV download takes a long time to generate. */
  asyncDownload?: boolean
  /** Optional callout after clicking on the csv link */
  callout?: () => void
}

type CSV<CsvItem extends Record<string, unknown>, CsvParams, HeaderItem> =
  | CsvFormatType<CsvParams>
  | CsvDataType<CsvItem, HeaderItem>

type CsvBase = {
  /** CSV name to be displayed on the downloaded spreadsheet */
  csvName: string
  /** Link name to be displayed in the UI */
  linkName: string
  /** Optional file type for CSV */
  ['xls_file']?: 'xls' | 'csv'
  /** Optional boolean to let us know if there was an error in the consuming component */
  error?: boolean
  /** If there was an error, then we would want to alert the consuming component with this callback */
  errorCallback?: () => void
  /** Optional className */
  customClass?: string
  /** Optional button className */
  btnCustomClass?: string
  /** @deprecated - Optional prop to show a button instead of a link - this should not be used. */
  variant?: 'button'
  /** Optional icon to use instead of the default `download` icon. */
  icon?: IconStringList
}

type CsvDataType<
  CsvItem extends Record<string, unknown>,
  HeaderItem,
> = CsvBase & {
  /** Formatted CSV data. This data is generated on the consuming component and passed in. */
  csvData: CsvItem[]
  csvFormat?: never
  /** Optional callout after clicking the csv link */
  callout?: (link?: HTMLDivElement | string) => void
  /** Optional headers to be passed into the CSV */
  headers?: HeaderItem[]
  /** Optional className for the csv link */
  hiddenClass?: string
}

type CsvFormatType<CsvParams> = CsvBase & {
  csvData?: never
  /** This will generate a CSV by utilizing an api. Not generated on the front end. */
  csvFormat: CsvFormat<CsvParams>
}

export type SingleCsvProps<
  CsvItem extends Record<string, unknown>,
  CsvParams,
  HeaderItem,
> = {
  /** Determines how the CSV will be generated. */
  csv: CSV<CsvItem, CsvParams, HeaderItem>
  /** Optional close function */
  close?: () => void
  /** Optional prop to show just the download icon when a single option exists. */
  onlyOption?: boolean
  /** Optionally hide this component until some condition is met. */
  hidden?: boolean
  /** Optional identifier */
  csvId?: string
  /** Optionally hide the download icon */
  hideIcon?: boolean
  /** Callback to update the state indicating change in download. */
  setLoading?: (value: boolean) => void
  /** State indicating download status */
  loading?: boolean
  /** Callback function triggered when a CSV action is performed. */
  handleCsvAction?: (value: boolean) => void
  /** Optional prop to add a test id to the SingleCsv for QA testing */
  qaTestId?: string
}

const SingleCsv = <
  CsvItem extends Record<string, unknown>,
  CsvParams,
  HeaderItem,
>({
  csv,
  close,
  onlyOption,
  hidden,
  csvId,
  hideIcon,
  setLoading,
  loading,
  handleCsvAction,
  qaTestId = 'single-csv',
}: SingleCsvProps<CsvItem, CsvParams, HeaderItem>): React.JSX.Element => {
  const [clicked, setClicked] = useState(false)
  // Type casting needed because CSVLink's ref type is complex and includes both Link component and HTMLAnchorElement properties
  const linkElement = useRef<CSVLinkRef & HTMLAnchorElement>(null)

  if ((csv.csvData && csv.csvData.length > 0 && clicked) || csv.error) {
    if (clicked) {
      setClicked(false)
      if (csv.error && csv.errorCallback) {
        setTimeout(() => {
          csv.errorCallback?.()
        }, 500)
      }
    }
  }

  if (!csv.csvFormat && csv.csvData?.length > 0) {
    const splitCSV = (sep: string | unknown) =>
      typeof sep === 'string' ? sep.replace(/"/g, '""') : sep

    csv.csvData = csv.csvData.map((d) => {
      Object.keys(d).forEach((v: keyof CsvItem) => {
        // @ts-expect-error TypeScript has issues with iterating over an Object
        d[v] = splitCSV(d[v])
      })
      return d
    })
  }

  const getCsvData = () => {
    if (!loading && csv.csvFormat) {
      setLoading?.(true)
      handleCsvAction?.(true)

      const csvParams = {
        ...csv.csvFormat?.params,
        format: 'csv',
      }
      let apiCall
      if (hasValue(csv.csvFormat?.apiArg)) {
        apiCall = csv.csvFormat.api(csv.csvFormat?.apiArg, csvParams)
      } else {
        apiCall = csv.csvFormat.api(csvParams)
      }
      apiCall
        .then((response) => {
          if (response && !csv.csvFormat?.asyncDownload && isClient) {
            const BOMChar = '\uFEFF'
            const blob = new Blob([BOMChar + response], {
              type: 'text/csv;charset=utf-8;',
            })
            const link = document.createElement('a')
            if (link.download !== undefined) {
              link.setAttribute('href', URL.createObjectURL(blob))
              link.setAttribute(
                'download',
                `${csv.csvName}.${csv['xls_file'] ? 'xls' : 'csv'}`,
              )
              link.style.visibility = 'hidden'
              document.body.appendChild(link)
              link.click()
              document.body.removeChild(link)
            }
          }
          csv.csvFormat?.callout?.()
          close?.()
        })
        .finally(() => {
          setLoading?.(false)
          handleCsvAction?.(false)
        })
    }
  }

  return (
    <React.Fragment>
      <div
        className={`${styles.csvLink} ${
          clicked || hidden ? styles.hidden : ''
        } ${onlyOption ? styles.onlyOption : ''}`}
        qa-test-id={qaTestId}
      >
        <div
          className={`${styles.linkName} ${styles.customDisplay} ${
            csv.customClass || ''
          }`}
          id={csvId || 'link-name'}
          onClick={() => {
            if (!csv.csvFormat) {
              /** Type casting needed because the callout function expects HTMLDivElement | string,
               but we're passing HTMLAnchorElement. This is safe because we only use it for
               the callout callback and don't rely on div-specific properties */
              csv.callout?.(
                linkElement.current?.link as unknown as HTMLDivElement,
              )
              setClicked(true)
            }
          }}
        >
          {csv?.variant === 'button' ? (
            <Button
              as='unstyled'
              onClick={csv.csvFormat && getCsvData}
              className={csv.btnCustomClass ? csv.btnCustomClass : ''}
              qaTestId={`${qaTestId}-button`}
            >
              {csv.linkName}
            </Button>
          ) : (
            !hideIcon && (
              <>
                {csv.csvFormat ? (
                  <Button
                    as='unstyled'
                    onClick={() => {
                      if (!loading) {
                        getCsvData()
                      }
                    }}
                    qaTestId={`${qaTestId}-button`}
                  >
                    {loading ? (
                      <Spinner />
                    ) : (
                      <Icon icon='download' color='dark-blue' />
                    )}
                  </Button>
                ) : (
                  <Icon
                    icon={csv.icon ? csv.icon : 'download'}
                    color='dark-blue'
                    qaTestId={`${qaTestId}-icon`}
                  />
                )}
              </>
            )
          )}
          {!onlyOption ? (
            <span onClick={csv.csvFormat && getCsvData} data-test='link-name'>
              {csv.linkName}
            </span>
          ) : null}
        </div>
        {clicked && (
          <div className={styles.loaderContainer}>
            <Spinner />
            <span>{csv.linkName}</span>
          </div>
        )}
        {!csv.csvFormat && (
          <CSVLink
            data={csv.csvData}
            headers={csv?.headers as string[]}
            filename={`${csv.csvName}.csv`}
            asyncOnClick={true}
            target='_self'
            onClick={(_, done) => {
              if (csv.csvData.length > 0) {
                if (close) {
                  close()
                }
                done()
              }
            }}
            // The ref type needs to be compatible with both Link component and HTMLAnchorElement
            ref={
              linkElement as unknown as React.RefObject<
                Link & HTMLAnchorElement
              >
            }
          />
        )}
      </div>
    </React.Fragment>
  )
}

export default SingleCsv
