/*
Prinsipper:
- Fileupload-komponenten er stateless. All state må håndteres utenfor.
- Dette er en controlled component, så når filer legges til, fjernes eller oppdateres, blir den nye fil-lista sendt ut via onFilesChanged. Komponenten over må dermed oppdatere value-prop'en.
- Når brukeren velger en fil for opplasting, gir komponenten fila en unik ID.
- `value`-typen til denne komponenten er en FileItemList
- Støtte for to opplastingsstrategier:
  - 'form': Filene legges til i et skjema og lastes opp når skjemaet sendes inn.
  - 'custom': Når en fil legges til, kalles en callback-funksjon for å håndtere opplastingen manuelt. Når skjemaet sendes inn, postes bare fil-IDene. Server-applikasjonen kan dermed knytte de allerede opplastede filene til fil-IDene.
- Støtte for flere filvalg (multiple) og enkel filvalg.
- Støtte for tilleggsfunksjoner som å legge til kommentarer og endre filnavn via utvidelser.
- Støtte for tilpassede renderer-komponenter for visning av filer i køen.
 */
import classNames from 'classnames'
import {
  FC,
  forwardRef,
  HTMLAttributes,
  InputHTMLAttributes,
  ReactNode,
  useCallback,
  useEffect,
  useId,
  useImperativeHandle,
  useMemo,
  useRef,
  useState,
} from 'react'

import { PktAlert } from '../alert/Alert'
import { PktInputWrapper } from '../inputwrapper/InputWrapper'
import { DropZone } from './DropZone'
import { addCommentOperation } from './extensions/Comments'
import { removeFileOperation } from './extensions/Remove'
import { renameFileOperation } from './extensions/Rename'
import { useFileAttributes } from './hooks'
import { ItemRenderers, QueueDisplay } from './QueueDisplay'
import {
  TFileId,
  FileItem,
  TFileItemList,
  TFileTransfer,
  TItemRenderer,
  PktFileUploadContext,
  TQueueItemExtension,
  TUploadStrategy,
} from './types'
import { formatFileSize, parseFileSize } from './utils'

// Re-export for external use
export { formatFileSize, parseFileSize } from './utils'

/**
 * Shared props for `PktFileUpload` (both `form` and `custom` strategies).
 *
 * The component can be used as:
 * - **controlled**: pass `value` + `onFilesChanged`
 * - **uncontrolled**: pass `defaultValue` (internal state is not stored; you still receive new lists via callbacks)
 */
interface IBaseFileUploadProps
  extends Omit<
    InputHTMLAttributes<HTMLInputElement>,
    'checked' | 'size' | 'type' | 'value' | 'onChange' | 'width' | 'defaultValue'
  > {
  /** Called with the next file list when files are added/removed/updated. */
  onFilesChanged?: (files: TFileItemList) => void
  /** Allow selecting/dropping multiple files. */
  multiple?: boolean
  /** Controlled value (recommended). */
  value?: TFileItemList
  /** Upload mode. Defaults to `"form"`. */
  uploadStrategy?: TUploadStrategy
  /** Field name. Used for native input (`form`) or hidden ID inputs (`custom`). */
  name: string
  /** Enable comment operation (disabled automatically in thumbnail view). */
  addCommentsEnabled?: boolean
  /** Enable rename operation (disabled automatically in thumbnail view). */
  renameFilesEnabled?: boolean
  /** Renderer for queue items (`filename`, `thumbnail`, or custom function). */
  itemRenderer?: keyof typeof ItemRenderers | TItemRenderer
  /** Number of trailing characters to keep when truncating long filenames. */
  truncateTail?: number
  /**
   * Called immediately when a file is added in `uploadStrategy="custom"`.
   * Use it to start uploading the file and update `transfers`.
   */
  onFileUploadRequested?: (fileItem: FileItem) => void
  /** Uncontrolled initial value (use either `value` or `defaultValue`, not both). */
  defaultValue?: TFileItemList
  /** Extra operations appended after built-ins (rename/comment). */
  extraOperations?: Array<TQueueItemExtension>
  /** Stretch to full container width (drop zone + queue). */
  fullWidth?: boolean
  /** Allowed formats for built-in validation (extensions like `pdf`, or MIME patterns like `image/*`). */
  allowedFormats?: string[]
  /** Custom message for invalid format. Use `{formats}` placeholder. */
  formatErrorMessage?: string
  /** Max file size (e.g. `"5MB"` or bytes). */
  maxFileSize?: string | number
  /** Custom message for size validation. Use `{maxSize}` placeholder. */
  sizeErrorMessage?: string
  /** Optional additional validation hook (runs after built-in format/size checks). Return string to block. */
  onFileValidation?: (file: File) => string | null
  /** External/programmatic error message shown under the component. */
  errorMessage?: string
  /** External error flag (combined with internal validation errors). */
  hasError?: boolean
  /** Disable the whole component (no interaction). */
  disabled?: boolean
  /** Optional label/title (wraps the component in `PktInputWrapper`). */
  label?: string
  /** Help text under the label. */
  helptext?: string | ReactNode
  /** Show "Valgfritt" tag in wrapper. */
  optionalTag?: boolean
  /** Show "Må fylles ut" tag in wrapper. */
  requiredTag?: boolean
  /** Marks the upload as required. Native validation is used in `form`; submit validation in `custom`. */
  required?: boolean
  /** Enable image preview modal (only applies to thumbnail renderer). */
  enableImagePreview?: boolean
}

/** Props for `uploadStrategy="form"` (native file input submission). */
interface IFileInputFileUploadProps
  extends IBaseFileUploadProps,
  Omit<HTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'defaultValue'> {
  uploadStrategy?: 'form'
  onFileUploadRequested?: never
}

/**
 * Props for `uploadStrategy="custom"` (upload handled externally).
 *
 * Requirements:
 * - `transfers` must be provided (to render status/progress)
 * - `onFileUploadRequested` is called for each added file
 */
interface ImmediateFileUploadProps extends IBaseFileUploadProps {
  id: string
  uploadStrategy: 'custom'
  transfers: Array<TFileTransfer>
  onFileUploadRequested: (fileItem: FileItem) => void
  onTransferCancelled?: (fileItemId: string) => void
}

export type IPktFileUpload = IFileInputFileUploadProps | ImmediateFileUploadProps

export const PktFileUpload: FC<IPktFileUpload> = forwardRef<HTMLInputElement, IPktFileUpload>(
  (
    {
      value: valueProp,
      defaultValue,
      id: idProp,
      multiple = false,
      uploadStrategy = 'form',
      addCommentsEnabled = false,
      renameFilesEnabled = false,
      truncateTail,
      onFilesChanged,
      onFileUploadRequested,
      extraOperations = [],
      itemRenderer: itemRendererProp = ItemRenderers.filename,
      fullWidth = false,
      allowedFormats,
      formatErrorMessage,
      maxFileSize, // default to 5 MB
      sizeErrorMessage,
      onFileValidation,
      errorMessage: externalErrorMessage,
      hasError: externalHasError = false,
      disabled = false,
      label,
      helptext,
      optionalTag,
      requiredTag,
      required = false,
      enableImagePreview = false,
      ['aria-describedby']: ariaDescribedByProp,
      ['aria-invalid']: ariaInvalidProp,
      ...props
    }: IPktFileUpload,
    forwardedRef,
  ) => {
    const generatedId = useId()
    const id = idProp ?? `pkt-fileupload-${generatedId}`
    const fileInputRef = useRef<HTMLInputElement>(null)
    useImperativeHandle(forwardedRef, () => fileInputRef.current as HTMLInputElement, [])

    const transfers = 'transfers' in props ? props.transfers : undefined
    const [validationError, setValidationError] = useState<string | null>(null)

    // Combine external error with internal validation error
    const hasError = externalHasError || !!validationError
    const errorMessage = externalErrorMessage || validationError
    const errorMessageId = errorMessage ? `${id}-error` : undefined
    const helptextId = helptext ? `${id}-helptext` : undefined
    const describedBy = [ariaDescribedByProp, helptextId, errorMessageId].filter(Boolean).join(' ') || undefined
    const isAriaInvalid = hasError || ariaInvalidProp === true || ariaInvalidProp === 'true'

    // Parse maxFileSize once (supports "5MB" strings or raw bytes)
    const maxFileSizeBytes = maxFileSize ? parseFileSize(maxFileSize) : undefined

    // Built-in validation function
    const validateFile = useCallback(
      (file: File): string | null => {
        // 1. Check format if allowedFormats is specified
        if (allowedFormats && allowedFormats.length > 0) {
          const fileExtension = file.name.split('.').pop()?.toLowerCase() || ''
          const fileMimeType = file.type.toLowerCase()

          const isAllowed = allowedFormats.some((format) => {
            const normalizedFormat = format.toLowerCase().replace(/^\./, '') // Remove leading dot if present

            // Check MIME type patterns (e.g. 'image/*', 'application/pdf')
            if (normalizedFormat.includes('/')) {
              if (normalizedFormat.endsWith('/*')) {
                const category = normalizedFormat.replace('/*', '')
                return fileMimeType.startsWith(category + '/')
              }
              return fileMimeType === normalizedFormat
            }

            // Check file extension (e.g. 'pdf', 'jpg', '.png')
            return fileExtension === normalizedFormat
          })

          if (!isAllowed) {
            const formatsDisplay = allowedFormats.join(', ')
            const defaultMessage = `Ugyldig filtype. Tillatte formater: ${formatsDisplay}`
            return formatErrorMessage?.replace('{formats}', formatsDisplay) || defaultMessage
          }
        }

        // 2. Check file size if maxFileSize is specified
        if (maxFileSizeBytes && file.size > maxFileSizeBytes) {
          const maxSizeDisplay = formatFileSize(maxFileSizeBytes)
          const defaultMessage = `Filen er for stor. Maks størrelse er ${maxSizeDisplay}.`
          return sizeErrorMessage?.replace('{maxSize}', maxSizeDisplay) || defaultMessage
        }

        // 3. Run custom validation if provided
        if (onFileValidation) {
          return onFileValidation(file)
        }

        return null
      },
      [allowedFormats, formatErrorMessage, maxFileSizeBytes, sizeErrorMessage, onFileValidation],
    )

    if (valueProp !== undefined && defaultValue !== undefined) {
      // eslint-disable-next-line no-console
      console.warn(
        "PktFileupload: Både value og defaultValue er angitt. Komponenten kan være enten 'controlled' eller 'uncontrolled', ikke begge deler. Bruk kun én av dem. Se https://react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable",
      )
    }
    if (valueProp !== undefined && onFilesChanged === undefined) {
      // eslint-disable-next-line no-console
      console.warn(
        "PktFileupload: value-prop er angitt uten onFilesChanged-callback. Når en komponent er 'controlled', må endringer håndteres via en callback. Vennligst legg til onFilesChanged-callback for å håndtere endringer i fil-listen. Se https://react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable",
      )
    }

    const isControlled = valueProp !== undefined
    const value = isControlled ? valueProp : defaultValue || []

    useEffect(() => {
      if (uploadStrategy !== 'custom' || !required) return
      const input = fileInputRef.current
      const form = input?.form
      if (!form) return

      const handleFormSubmit = (event: SubmitEvent) => {
        if (value.length > 0) return
        event.preventDefault()
        setValidationError('Velg minst én fil før du sender inn skjemaet.')
      }

      form.addEventListener('submit', handleFormSubmit)
      return () => {
        form.removeEventListener('submit', handleFormSubmit)
      }
    }, [required, uploadStrategy, value.length])

    const itemRenderer = typeof itemRendererProp === 'string' ? ItemRenderers[itemRendererProp] : itemRendererProp
    const isThumbnailView = itemRendererProp === 'thumbnail'
    const explicitAccept = typeof props.accept === 'string' ? props.accept.trim() : undefined
    const acceptFromAllowedFormats = useMemo(() => {
      if (!allowedFormats || allowedFormats.length === 0) return undefined
      return allowedFormats
        .map((format) => format.trim())
        .filter(Boolean)
        .map((format) => {
          const normalized = format.toLowerCase()
          if (normalized.includes('/')) return normalized
          if (normalized.startsWith('.')) return normalized
          return `.${normalized}`
        })
        .join(', ')
    }, [allowedFormats])
    const resolvedAcceptForDropZone = explicitAccept || acceptFromAllowedFormats

    const effectiveRenameEnabled = renameFilesEnabled && !isThumbnailView
    const effectiveCommentsEnabled = addCommentsEnabled && !isThumbnailView

    const onFilesAdded = useCallback(
      (addedFileItems: TFileItemList) => {
        // Run validation on each file
        for (const fileItem of addedFileItems) {
          const error = validateFile(fileItem.file)
          if (error) {
            setValidationError(error)
            return // Don't add any files if validation fails
          }
        }

        // Clear any previous validation error
        setValidationError(null)

        const newValue: TFileItemList = multiple ? [...value, ...addedFileItems] : [addedFileItems[0]]

        if (onFilesChanged) {
          onFilesChanged(newValue)
        }
        if (uploadStrategy === 'custom' && onFileUploadRequested) {
          addedFileItems.forEach((fileItem) => {
            onFileUploadRequested?.(fileItem)
          })
        }
      },
      [onFilesChanged, value, multiple, uploadStrategy, onFileUploadRequested, validateFile],
    )

    const onFileUpdated = useCallback(
      (fileId: TFileId, updates: Partial<FileItem>) => {
        const newValue = value.map((fileItem) => (fileItem.fileId === fileId ? { ...fileItem, ...updates } : fileItem))
        if (onFilesChanged) {
          onFilesChanged(newValue)
        }
      },
      [onFilesChanged, value],
    )

    const onFileRemoved = useCallback(
      (fileId: TFileId) => {
        const newValue = value.filter((fileItem) => fileItem.fileId !== fileId)
        if (onFilesChanged) {
          onFilesChanged(newValue)
        }
        if ('onTransferCancelled' in props && props.onTransferCancelled) {
          props.onTransferCancelled?.(fileId)
        }
      },
      [onFilesChanged, value, props],
    )

    const { fileAttributes } = useFileAttributes(value, onFileUpdated)

    const queueItemExtensions = useMemo(
      () =>
        [
          {
            op: renameFileOperation,
            enabled: effectiveRenameEnabled,
          },
          {
            op: (attributes: Parameters<typeof addCommentOperation>[0]) => addCommentOperation(attributes),
            enabled: effectiveCommentsEnabled,
          },
        ]
          .filter(({ enabled }) => enabled)
          .map(({ op }) => op)
          .concat(...extraOperations),
      [effectiveRenameEnabled, effectiveCommentsEnabled],
    )

    // Screen reader announcement IDs
    const srAnnouncementIds = useMemo(
      () => ({
        uploaded: `${id}-uploaded`,
        errors: `${id}-errors`,
      }),
      [id],
    )

    // Compute counts for screen reader announcements.
    // F.eks. "1 av 3 filer lastet opp" eller "2 av 3 filer feilet"
    const { totalCount, uploadedCount, failedCount } = useMemo(() => {
      const totalCount = value.length
      if (!transfers) {
        return { totalCount, uploadedCount: 0, failedCount: 0 }
      }

      let uploadedCount = 0
      let failedCount = 0

      for (const fileItem of value) {
        const transfer = transfers.find((t) => t.fileId === fileItem.fileId)
        if (transfer?.progress === 'done') uploadedCount++
        if (transfer?.progress === 'error') failedCount++
      }

      return { totalCount, uploadedCount, failedCount }
    }, [value, transfers])

    const totalLabel = totalCount === 1 ? 'fil' : 'filer'

    const fileUploadContent = (
      <div
        className={classNames('pkt-fileupload', {
          'pkt-fileupload--full-width': fullWidth,
          'pkt-fileupload--error': hasError,
          'pkt-fileupload--disabled': disabled,
        })}
        aria-disabled={disabled}
        {...(disabled ? { inert: '' as any } : {})}
      >
        <PktFileUploadContext.Provider value={{ name: props.name || '', multiple: !!multiple, id }}>
          {/* Screen reader announcements - visually hidden */}
          <div id={srAnnouncementIds.uploaded} className="pkt-sr-only" aria-live="polite" aria-atomic="true">
            {totalCount > 0 && uploadedCount > 0 && `${uploadedCount} av ${totalCount} ${totalLabel} lastet opp`}
          </div>
          <div id={srAnnouncementIds.errors} className="pkt-sr-only" aria-live="assertive" aria-atomic="true">
            {totalCount > 0 &&
              failedCount > 0 &&
              `Feil ved opplasting: ${failedCount} av ${totalCount} ${totalLabel} feilet`}
          </div>

          <DropZone
            id={id}
            name={props.name || ''}
            value={value}
            onFilesAdded={onFilesAdded}
            multiple={multiple}
            uploadStrategy={uploadStrategy}
            ref={fileInputRef}
            accept={isThumbnailView ? '.jpeg, .jpg, .png, .gif, .webp, .heic' : resolvedAcceptForDropZone}
            isThumbnailView={isThumbnailView}
            disabled={disabled}
            srAnnouncementIds={srAnnouncementIds}
            required={required && uploadStrategy === 'form'}
            aria-required={required ? 'true' : undefined}
            aria-invalid={isAriaInvalid ? 'true' : undefined}
            aria-describedby={describedBy}
            hasVisibleLabel={!!label}
          />
          {hasError && errorMessage && (
            <PktAlert
              skin="error"
              aria-live="assertive"
              role="alert"
              compact
              id={errorMessageId}
              className="pkt-fileupload__error-alert"
            >
              {errorMessage}
            </PktAlert>
          )}
          <QueueDisplay
            files={value}
            cancelTransfer={onFileRemoved}
            truncateTail={truncateTail}
            transfers={transfers}
            uploadStrategy={uploadStrategy}
            ItemRenderer={itemRenderer}
            enableImagePreview={isThumbnailView && enableImagePreview}
            queueItemOperations={queueItemExtensions
              .map((ext) => ext(fileAttributes))
              .concat(removeFileOperation(onFileRemoved))}
          />
        </PktFileUploadContext.Provider>
      </div>
    )

    // Wrap with InputWrapper if label is provided
    if (label) {
      return (
        <PktInputWrapper
          forId={id}
          label={label}
          helptext={helptext}
          disabled={disabled}
          hasError={hasError}
          optionalTag={optionalTag}
          requiredTag={requiredTag}
          className={classNames('pkt-fileupload-wrapper', { 'pkt-fileupload-wrapper--full-width': fullWidth })}
        >
          {fileUploadContent}
        </PktInputWrapper>
      )
    }

    return fileUploadContent
  },
)

PktFileUpload.displayName = 'PktFileUpload'
