import { html } from 'lit'
import { classMap } from 'lit/directives/class-map.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { customElement, state } from 'lit/decorators.js'

import { defaultFileUploadStrings } from 'shared-types'
import { navigateCyclic, trapTabInside, type IFileAndTransfer } from 'shared-utils/fileupload'

import '@/components/alert'
import '@/components/icon'
import '@/components/input-wrapper'
import '@/components/modal'
import '@/components/progressbar'
import { PktFileUploadBase } from './fileupload-base'
import {
  createCommentOperation,
  createRemoveOperation,
  createRenameOperation,
} from './fileupload-operations'
import { renderFilenameQueueItem, renderThumbnailQueueItem } from './fileupload-renderers'
import type {
  FileItem,
  IPktFileUpload,
  TQueueItemOperation,
  TQueueOperationContext,
  TQueueOperationLabel,
  TFileComment,
  TFileTransfer,
  TFileUploadItemRenderer,
  TTransferCancelledDetail,
  TTransferProgress,
  TFileValidateDetail,
  TFileValidator,
  TUploadStrategy,
} from './fileupload-types'
export type {
  FileItem,
  IPktFileUpload,
  TQueueItemOperation,
  TQueueOperationContext,
  TQueueOperationLabel,
  TFileComment,
  TFileTransfer,
  TFileUploadItemRenderer,
  TTransferCancelledDetail,
  TTransferProgress,
  TFileValidateDetail,
  TFileValidator,
  TUploadStrategy,
}

type FileImageFailureFlags = { thumbnail?: boolean; preview?: boolean }

@customElement('pkt-fileupload')
export class PktFileUpload extends PktFileUploadBase implements IPktFileUpload {
  @state() private activeOperationByFileId: Record<string, string | undefined> = {}
  @state() private isPreviewModalOpen = false
  @state() private previewCurrentIndex = 0
  @state() private failedImageFileIds: Record<string, FileImageFailureFlags> = {}

  render() {
    const content = this.renderContent()

    if (!this.label) return content

    return html`
      <pkt-input-wrapper
        .forId=${`${this.id}-input`}
        .label=${this.label}
        .helptext=${this.helptext}
        ?disabled=${this.disabled}
        ?hasError=${this.hasEffectiveError}
        ?optionalTag=${this.optionalTag}
        ?requiredTag=${this.requiredTag}
        class=${classMap({
          'pkt-fileupload-wrapper': true,
          'pkt-fileupload-wrapper--full-width': this.fullwidth,
        })}
      >
        ${content}
      </pkt-input-wrapper>
    `
  }

  private renderContent() {
    const filesAndTransfers = this.getSortedFilesAndTransfers()
    const previewableImages = this.getPreviewableImages(filesAndTransfers)
    const supportedFormatsText = this.getSupportedFormatsText()
    const helptextId = !this.label && this.helptext ? `${this.id}-helptext` : undefined
    const errorId = this.hasEffectiveError ? `${this.id}-error` : undefined
    const describedBy = [helptextId, errorId].filter(Boolean).join(' ') || undefined
    const isThumbnailView = this.itemRenderer === 'thumbnail'
    const dropZoneCopy = isThumbnailView
      ? this.multiple
        ? {
            dragInactive: defaultFileUploadStrings.dropZoneDragMultipleThumbnail,
            dragActive: defaultFileUploadStrings.dropZoneDragActiveMultipleThumbnail,
            openFileDialog: defaultFileUploadStrings.dropZoneOpenFileDialogMultipleThumbnail,
          }
        : {
            dragInactive: defaultFileUploadStrings.dropZoneDragSingleThumbnail,
            dragActive: defaultFileUploadStrings.dropZoneDragActiveSingleThumbnail,
            openFileDialog: defaultFileUploadStrings.dropZoneOpenFileDialogSingleThumbnail,
          }
      : this.multiple
        ? {
            dragInactive: defaultFileUploadStrings.dropZoneDragMultiple,
            dragActive: defaultFileUploadStrings.dropZoneDragActiveMultiple,
            openFileDialog: defaultFileUploadStrings.dropZoneOpenFileDialogMultiple,
          }
        : {
            dragInactive: defaultFileUploadStrings.dropZoneDragSingle,
            dragActive: defaultFileUploadStrings.dropZoneDragActiveSingle,
            openFileDialog: defaultFileUploadStrings.dropZoneOpenFileDialogSingle,
          }

    return html`
      <div
        class=${classMap({
          'pkt-fileupload': true,
          'pkt-fileupload--full-width': this.fullwidth,
          'pkt-fileupload--error': this.hasEffectiveError,
          'pkt-fileupload--disabled': this.disabled,
        })}
        aria-disabled=${String(this.disabled)}
        ?inert=${this.disabled}
      >
        <div class="pkt-sr-only" aria-live="polite" aria-atomic="true">
          ${this.getSrUploadedMessage()}
        </div>
        <div class="pkt-sr-only" aria-live="assertive" aria-atomic="true">
          ${this.getSrErrorsMessage()}
        </div>
        <div
          class=${classMap({
            'pkt-fileupload__drop-zone': true,
            'pkt-fileupload__drop-zone--drag-active': this.isDragActive,
            'pkt-fileupload__drop-zone--disabled': this.disabled,
          })}
          @dragover=${this.onDragOver}
          @dragleave=${this.onDragLeave}
          @drop=${this.onDrop}
          @click=${this.onDropZoneClick}
        >
          <input
            id=${`${this.id}-input`}
            type="file"
            name=${ifDefined(this.uploadStrategy === 'form' ? this.name : undefined)}
            ?multiple=${this.multiple}
            ?disabled=${this.disabled}
            ?required=${this.required && this.uploadStrategy === 'form'}
            accept=${this.getResolvedAcceptValue()}
            aria-label=${ifDefined(
              this.label ? undefined : this.multiple ? 'Velg filer' : 'Velg fil',
            )}
            aria-required=${ifDefined(this.required ? 'true' : undefined)}
            aria-invalid=${ifDefined(this.hasEffectiveError ? 'true' : undefined)}
            aria-describedby=${ifDefined(describedBy)}
            @change=${this.onNativeFileChange}
          />
          ${this.uploadStrategy === 'custom'
            ? filesAndTransfers.map(
                (fileItem) =>
                  html`<input type="hidden" name=${this.name} value=${fileItem.fileId} />`,
              )
            : null}
          <div class="pkt-fileupload__drop-zone__placeholder">
            <pkt-icon
              name="attachment"
              class="pkt-fileupload__drop-zone__placeholder__icon"
              aria-hidden="true"
            ></pkt-icon>
            <p class="pkt-fileupload__drop-zone__placeholder__title">
              ${this.isDragActive
                ? `${dropZoneCopy.dragActive} ...`
                : html`${dropZoneCopy.dragInactive}
                    <button
                      type="button"
                      class="pkt-fileupload__drop-zone__placeholder__title__open-file-dialog"
                      @click=${this.openFileDialog}
                    >
                      ${dropZoneCopy.openFileDialog}
                    </button>`}
            </p>
            ${supportedFormatsText
              ? html`<p class="pkt-fileupload__drop-zone__placeholder__formats">
                  ${defaultFileUploadStrings.supportedFormatsPrefix} ${supportedFormatsText}
                </p>`
              : null}
          </div>
        </div>
        ${this.hasEffectiveError
          ? html`
              <pkt-alert
                skin="error"
                role="alert"
                aria-live="assertive"
                compact
                id=${`${this.id}-error`}
                class="pkt-alert--error pkt-fileupload__error-alert"
              >
                ${this.effectiveErrorMessage}
              </pkt-alert>
            `
          : null}
        ${filesAndTransfers.length > 0
          ? html`<ul class="pkt-fileupload__queue-display">
              ${filesAndTransfers.map((file) => this.renderQueueItem(file, previewableImages))}
            </ul>`
          : null}
        ${this.renderPreviewModal(previewableImages)}
      </div>
    `
  }

  private renderQueueItem(file: IFileAndTransfer, previewableImages: IFileAndTransfer[]) {
    const fileSize = typeof file.file?.size === 'number' ? this.formatFileSize(file.file.size) : ''
    const operations = this.getQueueItemOperations()

    if (this.itemRenderer === 'thumbnail') {
      const canOpenPreview = this.canOpenPreview(file)
      const thumbnailUrl = this.failedImageFileIds[file.fileId]?.thumbnail
        ? undefined
        : this.getThumbnailUrl(file)
      return renderThumbnailQueueItem({
        file,
        inputName: this.name,
        disabled: this.disabled,
        fileSize,
        operations,
        activeOperationId: this.activeOperationByFileId[file.fileId],
        onActivateOperation: (fileId, operationId) => this.activateOperation(fileId, operationId),
        onCloseOperation: (fileId) => this.closeOperation(fileId),
        getFileAttribute: <T>(fileId: string, name: string) =>
          this.getFileAttribute<T>(fileId, name),
        setFileAttribute: (fileId: string, name: string, value: unknown) =>
          this.setFileAttribute(fileId, name, value),
        thumbnailUrl,
        previewEnabled: this.enableImagePreview,
        canOpenPreview,
        transferProgress: file.progress,
        transferErrorMessage: file.errorMessage,
        transferShowProgress: file.showProgress,
        transferLastProgress: file.lastProgress,
        loadingText: 'Laster opp',
        truncateTail: this.truncateTail,
        onCancel: (fileId) => this.cancelTransfer(fileId),
        onOpenPreview: (fileId) => this.openPreview(fileId, previewableImages),
        onThumbnailImageError: (fileId) => this.markImageFailed(fileId, 'thumbnail'),
      })
    }

    return renderFilenameQueueItem({
      file,
      inputName: this.name,
      disabled: this.disabled,
      fileSize,
      operations,
      activeOperationId: this.activeOperationByFileId[file.fileId],
      onActivateOperation: (fileId, operationId) => this.activateOperation(fileId, operationId),
      onCloseOperation: (fileId) => this.closeOperation(fileId),
      getFileAttribute: <T>(fileId: string, name: string) => this.getFileAttribute<T>(fileId, name),
      setFileAttribute: (fileId: string, name: string, value: unknown) =>
        this.setFileAttribute(fileId, name, value),
      transferProgress: file.progress,
      transferErrorMessage: file.errorMessage,
      transferShowProgress: file.showProgress,
      transferLastProgress: file.lastProgress,
      loadingText: 'Laster opp',
      truncateTail: this.truncateTail,
      onCancel: (fileId) => this.cancelTransfer(fileId),
    })
  }

  private markImageFailed(fileId: string, kind: keyof FileImageFailureFlags): void {
    this.failedImageFileIds = {
      ...this.failedImageFileIds,
      [fileId]: { ...this.failedImageFileIds[fileId], [kind]: true },
    }
  }

  private getPreviewableImages(filesAndTransfers: IFileAndTransfer[]): IFileAndTransfer[] {
    if (!this.enableImagePreview) return []
    return filesAndTransfers.filter((item) => item.progress === 'done' && this.isImageFile(item))
  }

  private canOpenPreview(file: IFileAndTransfer): boolean {
    return this.enableImagePreview && file.progress === 'done' && this.isImageFile(file)
  }

  private openPreview(fileId: string, previewableImages: IFileAndTransfer[]): void {
    const index = previewableImages.findIndex((item) => item.fileId === fileId)
    if (index < 0) return
    this.previewCurrentIndex = index
    this.isPreviewModalOpen = true
    this.failedImageFileIds = {
      ...this.failedImageFileIds,
      [fileId]: { ...this.failedImageFileIds[fileId], preview: false },
    }
  }

  private closePreview = (): void => {
    this.isPreviewModalOpen = false
  }

  private navigatePreview(direction: 'prev' | 'next', previewableImages: IFileAndTransfer[]): void {
    if (previewableImages.length === 0) return
    this.previewCurrentIndex = navigateCyclic(
      this.previewCurrentIndex,
      direction,
      previewableImages.length,
    )
  }

  private onPreviewKeyDown = (
    event: KeyboardEvent,
    previewableImages: IFileAndTransfer[],
  ): void => {
    if (!this.isPreviewModalOpen) return
    if (event.key === 'Tab') {
      trapTabInside(event, event.currentTarget as HTMLElement | null)
      return
    }
    switch (event.key) {
      case 'ArrowLeft':
        event.preventDefault()
        this.navigatePreview('prev', previewableImages)
        break
      case 'ArrowRight':
        event.preventDefault()
        this.navigatePreview('next', previewableImages)
        break
      case 'Escape':
        event.preventDefault()
        this.closePreview()
        break
      default:
        break
    }
  }

  private renderPreviewModal(previewableImages: IFileAndTransfer[]) {
    if (!this.enableImagePreview || previewableImages.length === 0) return null
    const currentImage = previewableImages[this.previewCurrentIndex]
    if (!currentImage) return null
    const hasMultipleImages = previewableImages.length > 1
    const imageUrl = this.getThumbnailUrl(currentImage)
    const hasImage = !!imageUrl && !this.failedImageFileIds[currentImage.fileId]?.preview
    const filename =
      currentImage.attributes?.targetFilename || currentImage.file?.name || 'Forhåndsvisning'

    return html`
      <pkt-modal
        .open=${this.isPreviewModalOpen}
        .headingText=${filename}
        closeOnBackdropClick
        class="pkt-fileupload__image-preview-modal"
        @close=${this.closePreview}
        @keydown=${(event: KeyboardEvent) => this.onPreviewKeyDown(event, previewableImages)}
      >
        <div class="pkt-fileupload__image-preview">
          ${hasMultipleImages
            ? html`<button
                type="button"
                class="pkt-fileupload__image-preview__nav pkt-fileupload__image-preview__nav--prev pkt-btn pkt-btn--medium pkt-btn--secondary pkt-btn--icon-only"
                aria-label=${`Forrige bilde ${this.previewCurrentIndex} / ${previewableImages.length}`}
                @click=${() => this.navigatePreview('prev', previewableImages)}
              >
                <pkt-icon
                  class="pkt-btn__icon pkt-icon"
                  name="chevron-thin-left"
                  aria-hidden="true"
                ></pkt-icon>
                <span class="pkt-btn__text"></span>
              </button>`
            : null}

          <div class="pkt-fileupload__image-preview__content">
            ${hasImage
              ? html`<img
                  src=${imageUrl}
                  alt=${`${filename} - ${this.previewCurrentIndex + 1} av ${previewableImages.length}`}
                  class="pkt-fileupload__image-preview__image"
                  @error=${() => this.markImageFailed(currentImage.fileId, 'preview')}
                />`
              : html`<pkt-icon
                  name="picture"
                  class="pkt-fileupload__queue-display__item__icon pkt-icon--medium"
                  aria-hidden="true"
                ></pkt-icon>`}
          </div>

          ${hasMultipleImages
            ? html`<span class="pkt-fileupload__image-preview__counter">
                ${this.previewCurrentIndex + 1} / ${previewableImages.length}
              </span>`
            : null}
          ${hasMultipleImages
            ? html`<button
                type="button"
                class="pkt-fileupload__image-preview__nav pkt-fileupload__image-preview__nav--next pkt-btn pkt-btn--medium pkt-btn--secondary pkt-btn--icon-only"
                aria-label=${`Neste bilde ${this.previewCurrentIndex + 2} / ${previewableImages.length}`}
                @click=${() => this.navigatePreview('next', previewableImages)}
              >
                <pkt-icon
                  class="pkt-btn__icon pkt-icon"
                  name="chevron-thin-right"
                  aria-hidden="true"
                ></pkt-icon>
                <span class="pkt-btn__text"></span>
              </button>`
            : null}
        </div>
      </pkt-modal>
    `
  }

  private getQueueItemOperations(): TQueueItemOperation[] {
    const operations: TQueueItemOperation[] = []

    if (this.renameFilesEnabled && this.itemRenderer !== 'thumbnail') {
      operations.push(createRenameOperation())
    }

    if (this.addCommentsEnabled && this.itemRenderer !== 'thumbnail') {
      operations.push(createCommentOperation())
    }

    operations.push(...this.extraOperations)
    operations.push(createRemoveOperation((fileId) => this.removeFileItem(fileId)))

    return operations
  }

  private activateOperation(fileId: string, operationId: string): void {
    this.activeOperationByFileId = {
      ...this.activeOperationByFileId,
      [fileId]: operationId,
    }
  }

  private closeOperation(fileId: string): void {
    this.activeOperationByFileId = {
      ...this.activeOperationByFileId,
      [fileId]: undefined,
    }
  }

  private getFileAttribute<T>(fileId: string, name: string): T | undefined {
    const currentFile = this.getCurrentFiles().find((item) => item.fileId === fileId)
    if (!currentFile) return undefined
    return (currentFile.attributes?.[name] as T | undefined) ?? undefined
  }

  private setFileAttribute(fileId: string, name: string, value: unknown): void {
    const currentFile = this.getCurrentFiles().find((item) => item.fileId === fileId)
    if (!currentFile) return
    const nextAttributes = {
      ...currentFile.attributes,
      [name]: value,
    }
    if (value === undefined) {
      delete nextAttributes[name]
    }
    this.updateFileItem(fileId, {
      attributes: nextAttributes,
    })
  }
}
