import { PropertyValues } from 'lit'
import { property, state } from 'lit/decorators.js'

import { defaultFileUploadStrings } from 'shared-types'
import {
  countFileTransferStates,
  formatFileSize,
  getSupportedFormatsText as sharedGetSupportedFormatsText,
  isImageFileLike,
  mergeFilesAndTransfers,
  resolveAcceptAttribute,
  validateFile as sharedValidateFile,
} from 'shared-utils/fileupload'

import { PktElement } from '@/base-elements/element'
import converters from '../../helpers/converters'

import type {
  FileItem,
  IPktFileUpload,
  TQueueItemOperation,
  TFileTransfer,
  TFileUploadItemRenderer,
  TFileValidateDetail,
  TFilesChangedReason,
  TTransferCancelledDetail,
  TTransferProgress,
  TUploadStrategy,
} from './fileupload-types'
import { uuidish } from 'shared-utils/utils'

export abstract class PktFileUploadBase
  extends PktElement<IPktFileUpload>
  implements IPktFileUpload
{
  /** Unique id used for internal input + wrapper wiring. Defaults to a per-instance uuid. */
  @property({ type: String }) id: string = `pkt-fileupload-${uuidish()}`
  /** Field name used by native input (`form`) or hidden fileId inputs (`custom`). */
  @property({ type: String }) name: string = 'files'
  /** Optional label shown by `pkt-input-wrapper`. */
  @property({ type: String }) label: string = ''
  /** Optional help text shown under label. */
  @property({ type: String }) helptext: string = ''
  /** Mark input as required in form strategy. */
  @property({ type: Boolean, reflect: true }) required = false
  /** Allow selecting/dropping multiple files. */
  @property({ type: Boolean, reflect: true }) multiple = false
  /** Disable all interaction in drop zone + queue actions. */
  @property({ type: Boolean, reflect: true }) disabled = false
  /** Stretch component width to container. */
  @property({ type: Boolean, reflect: true, attribute: 'fullwidth' }) fullwidth = false
  /** Upload mode: `form` uses native file submit, `custom` emits upload request events per file. */
  @property({ type: String, reflect: true, attribute: 'upload-strategy' })
  uploadStrategy: TUploadStrategy = 'form'
  /** Queue visual mode (`filename` or `thumbnail`). */
  @property({ type: String, reflect: true, attribute: 'item-renderer' })
  itemRenderer: TFileUploadItemRenderer = 'filename'
  /** Native file input accept hint (browser picker filtering). */
  @property({ type: String }) accept: string = ''
  /** Built-in format validation source (csv attribute or array property). */
  @property({ converter: converters.csvToArray, attribute: 'allowed-formats' })
  allowedFormats: string[] = []
  /** Optional custom format validation message. Supports `{formats}` placeholder. */
  @property({ type: String, attribute: 'format-error-message' }) formatErrorMessage = ''
  /** Max allowed file size (bytes or string like `500KB`, `5MB`). */
  @property({ attribute: 'max-file-size' }) maxFileSize?: string | number
  /** Optional custom size validation message. Supports `{maxSize}` placeholder. */
  @property({ type: String, attribute: 'size-error-message' }) sizeErrorMessage = ''
  /** Optional JS callback for custom validation. Property-only on purpose (not HTML-attribute friendly). */
  @property({ attribute: false }) onFileValidation?: (file: File) => string | null
  /** Transfer state list keyed by `fileId` (used for custom upload progress/error/cancel UI). */
  @property({ attribute: false }) transfers: TFileTransfer[] = []
  /** Enables built-in comment operation in queue items (disabled in thumbnail view for parity with React). */
  @property({ type: Boolean, attribute: 'add-comments-enabled' }) addCommentsEnabled = false
  /** Enables built-in rename operation in queue items (disabled in thumbnail view for parity with React). */
  @property({ type: Boolean, attribute: 'rename-files-enabled' }) renameFilesEnabled = false
  /** Custom queue operations (JS property only). Supports inline + expanded operation UIs. */
  @property({ attribute: false }) extraOperations: TQueueItemOperation[] = []
  /** Toggle image thumbnail behavior in queue when renderer supports it. */
  @property({ type: Boolean, reflect: true, attribute: 'enable-image-preview' })
  enableImagePreview = false
  /** External error flag — combines with internal validation errors. */
  @property({ type: Boolean, attribute: 'has-error' }) hasError = false
  /** External error message shown in the alert under the drop zone. */
  @property({ type: String, attribute: 'error-message' }) errorMessage = ''
  /** Show "Valgfritt" tag in the input wrapper. */
  @property({ type: Boolean, attribute: 'optional-tag' }) optionalTag = false
  /** Show "Må fylles ut" tag in the input wrapper. */
  @property({ type: Boolean, attribute: 'required-tag' }) requiredTag = false
  /** Trailing characters to keep when middle-truncating long filenames. Set to `0` to disable. */
  @property({ type: Number, attribute: 'truncate-tail' }) truncateTail: number = 4
  /** Controlled mode source of truth. Parent owns file list and updates this prop from events. */
  @property({ attribute: false }) value?: FileItem[]
  /** Uncontrolled initial file list. Used once during first initialization. */
  @property({ attribute: false }) defaultValue?: FileItem[]

  @state() protected files: FileItem[] = []
  @state() protected isDragActive = false
  @state() protected validationErrorMessage: string | null = null
  @state() protected addedAnnouncement = ''
  @state() private thumbnailUrls: Record<string, string> = {}

  private hasInitializedValue = false
  private hasWarnedInvalidValueCombo = false
  private thumbnailFileById = new Map<string, File>()
  private connectedForm: HTMLFormElement | null = null
  private addedAnnouncementTimer: ReturnType<typeof setTimeout> | null = null
  private readonly requiredSelectionMessage = defaultFileUploadStrings.requiredMissing

  protected get isControlled() {
    return this.value !== undefined
  }

  /** True if the component itself wants to show an error (internal validation or external prop). */
  protected get hasEffectiveError() {
    return !!this.validationErrorMessage || this.hasError || !!this.errorMessage.trim()
  }

  /** Message to show in the alert under the drop zone. Internal validation takes priority. */
  protected get effectiveErrorMessage(): string {
    return this.validationErrorMessage ?? this.errorMessage ?? ''
  }

  protected get hasValidationError() {
    return !!this.validationErrorMessage
  }

  protected firstUpdated(changedProperties: PropertyValues): void {
    super.firstUpdated?.(changedProperties)
    if (!this.hasInitializedValue) {
      this.files = this.value ?? this.defaultValue ?? []
      this.syncThumbnailUrls(this.files)
      this.hasInitializedValue = true
    }
    if (this.value !== undefined && this.defaultValue !== undefined) {
      this.warnInvalidValueCombo()
    }
  }

  protected willUpdate(changedProperties: PropertyValues): void {
    super.willUpdate(changedProperties)
    if (!this.hasInitializedValue) return

    if (
      this.isControlled &&
      (changedProperties.has('value') ||
        changedProperties.has('itemRenderer') ||
        changedProperties.has('enableImagePreview'))
    ) {
      this.files = this.value ?? []
      this.syncThumbnailUrls(this.files)
    }
  }

  protected updated(changedProperties: PropertyValues): void {
    super.updated(changedProperties)
    if (
      changedProperties.has('uploadStrategy') ||
      changedProperties.has('required') ||
      changedProperties.has('value')
    ) {
      this.syncFormSubmitListener()
    }
    if (
      this.uploadStrategy === 'form' &&
      (changedProperties.has('value') ||
        changedProperties.has('files') ||
        changedProperties.has('uploadStrategy'))
    ) {
      this.syncNativeInputFiles()
    }
  }

  private syncNativeInputFiles() {
    const input = this.querySelector<HTMLInputElement>(`#${this.id}-input`)
    if (!input || typeof DataTransfer === 'undefined') return
    try {
      const dataTransfer = new DataTransfer()
      for (const item of this.getCurrentFiles()) {
        if (item.file) dataTransfer.items.add(item.file)
      }
      input.files = dataTransfer.files
    } catch {
      // jsdom and some test environments do not implement DataTransfer; not critical.
    }
  }

  private warnInvalidValueCombo() {
    if (this.hasWarnedInvalidValueCombo) return

    console.warn(
      'PktFileUpload: Både value og defaultValue er angitt. Komponenten kan være enten kontrollert eller ukontrollert, ikke begge. value vil bli prioritert.',
    )
    this.hasWarnedInvalidValueCombo = true
  }

  disconnectedCallback(): void {
    this.teardownFormSubmitListener()
    if (this.addedAnnouncementTimer) {
      clearTimeout(this.addedAnnouncementTimer)
      this.addedAnnouncementTimer = null
    }
    super.disconnectedCallback()
    this.revokeAllThumbnailUrls()
  }

  private setAddedAnnouncement(message: string) {
    this.addedAnnouncement = message
    if (this.addedAnnouncementTimer) clearTimeout(this.addedAnnouncementTimer)
    this.addedAnnouncementTimer = setTimeout(() => {
      this.addedAnnouncement = ''
      this.addedAnnouncementTimer = null
    }, 1500)
  }

  connectedCallback(): void {
    super.connectedCallback()
    this.syncFormSubmitListener()
  }

  protected onNativeFileChange = (event: Event) => {
    const target = event.target as HTMLInputElement
    this.addFiles(target.files ? Array.from(target.files) : [])
    // In custom mode we do not submit native file input values, so we can clear the input
    // to allow selecting the same file again. In form mode we keep native files for form submit.
    if (this.uploadStrategy === 'custom') {
      target.value = ''
    }
  }

  protected openFileDialog = (event: Event) => {
    event.preventDefault()
    event.stopPropagation()
    if (this.disabled) return
    const input = this.querySelector<HTMLInputElement>(`#${this.id}-input`)
    input?.click()
  }

  protected onDropZoneClick = (event: MouseEvent) => {
    if (this.disabled) return
    const target = event.target as HTMLElement
    if (target.closest('.pkt-fileupload__drop-zone__placeholder__title__open-file-dialog')) return
    const input = this.querySelector<HTMLInputElement>(`#${this.id}-input`)
    input?.click()
  }

  protected onDragOver = (event: DragEvent) => {
    event.preventDefault()
    if (this.disabled) return
    this.isDragActive = true
  }

  protected onDragLeave = () => {
    this.isDragActive = false
  }

  protected onDrop = (event: DragEvent) => {
    event.preventDefault()
    this.isDragActive = false
    if (this.disabled) return
    const droppedFiles = event.dataTransfer?.files ? Array.from(event.dataTransfer.files) : []
    this.addFiles(droppedFiles)
  }

  protected addFiles(selectedFiles: File[]) {
    if (selectedFiles.length === 0) return
    const normalizedFiles = this.multiple ? selectedFiles : [selectedFiles[0]]

    for (const file of normalizedFiles) {
      const validationError = this.validateFile(file)
      if (validationError) {
        this.validationErrorMessage = validationError
        return
      }
    }

    this.validationErrorMessage = null
    const newItems: FileItem[] = normalizedFiles.map((file) => ({
      fileId: uuidish(),
      file,
      attributes: {
        targetFilename: file.name,
      },
    }))

    this.setAddedAnnouncement(
      newItems.length === 1
        ? defaultFileUploadStrings.srFileAdded(newItems[0].attributes.targetFilename)
        : defaultFileUploadStrings.srFilesAdded(newItems.length),
    )

    const currentFiles = this.getCurrentFiles()
    const nextFiles = this.multiple ? [...currentFiles, ...newItems] : [newItems[0]]
    this.commitFiles(
      nextFiles,
      'add',
      newItems.map((item) => item.fileId),
    )

    if (this.uploadStrategy === 'custom') {
      newItems.forEach((fileItem) => {
        this.dispatchEvent(
          new CustomEvent('file-upload-requested', {
            detail: {
              fileId: fileItem.fileId,
              file: fileItem.file,
              attributes: fileItem.attributes,
            },
            bubbles: true,
            composed: true,
          }),
        )
      })
    }
  }

  protected getCurrentFiles(): FileItem[] {
    return this.isControlled ? (this.value ?? []) : this.files
  }

  protected cancelTransfer(fileId: string) {
    if (this.disabled) return
    const currentItem = this.getCurrentFiles().find((item) => item.fileId === fileId)
    if (!currentItem) return

    this.removeFileItem(fileId)

    // CE-native deviation from React callback props: emit a bubbling event for host-driven cancel/delete logic.
    this.dispatchEvent(
      new CustomEvent<TTransferCancelledDetail>('transfer-cancelled', {
        detail: {
          fileId: currentItem.fileId,
          file: currentItem.file,
          attributes: currentItem.attributes,
        },
        bubbles: true,
        composed: true,
      }),
    )
  }

  protected removeFileItem(fileId: string) {
    if (this.disabled) return
    const currentFiles = this.getCurrentFiles()
    const nextFiles = currentFiles.filter((item) => item.fileId !== fileId)
    if (nextFiles.length === currentFiles.length) return
    this.commitFiles(nextFiles, 'remove', [fileId])
  }

  // CE-native deviation: this is a method-based update hook instead of React callback props.
  // It keeps queue lifecycle explicit without introducing React-specific abstractions.
  public updateFileItem(fileId: string, updates: Partial<FileItem>) {
    const currentFiles = this.getCurrentFiles()
    const nextFiles = currentFiles.map((item) =>
      item.fileId === fileId ? { ...item, ...updates } : item,
    )
    this.commitFiles(nextFiles, 'update', [fileId])
  }

  protected commitFiles(
    nextFiles: FileItem[],
    reason: TFilesChangedReason,
    changedFileIds?: string[],
  ) {
    if (!this.isControlled) {
      this.files = nextFiles
      this.syncThumbnailUrls(nextFiles)
    }
    if (this.validationErrorMessage === this.requiredSelectionMessage && nextFiles.length > 0) {
      this.validationErrorMessage = null
    }
    this.dispatchEvent(
      new CustomEvent('files-changed', {
        detail: { files: nextFiles, reason, changedFileIds },
        bubbles: true,
        composed: true,
      }),
    )
  }

  protected getThumbnailUrl(file: FileItem): string | undefined {
    return this.thumbnailUrls[file.fileId]
  }

  protected getTransferProgress(fileId: string): TTransferProgress {
    const transfer = this.transfers.find((item) => item.fileId === fileId)
    if (transfer) return transfer.progress
    return this.uploadStrategy === 'form' ? 'done' : 'queued'
  }

  protected getTransferForFile(fileId: string): TFileTransfer | undefined {
    return this.transfers.find((item) => item.fileId === fileId)
  }

  protected isImageFile(file: FileItem): boolean {
    return isImageFileLike(file)
  }

  protected formatFileSize(bytes: number): string {
    return formatFileSize(bytes)
  }

  protected getResolvedAcceptValue(): string {
    return resolveAcceptAttribute(this.accept, this.allowedFormats)
  }

  protected getSupportedFormatsText(): string {
    return sharedGetSupportedFormatsText(this.allowedFormats, this.accept)
  }

  protected getSortedFilesAndTransfers() {
    return mergeFilesAndTransfers(this.getCurrentFiles(), this.transfers, this.uploadStrategy)
  }

  private validateFile(file: File): string | null {
    const baseError = sharedValidateFile(file, {
      allowedFormats: this.allowedFormats,
      maxFileSize: this.maxFileSize,
      formatErrorMessage: this.formatErrorMessage,
      sizeErrorMessage: this.sizeErrorMessage,
      onFileValidation: this.onFileValidation,
    })
    if (baseError) return baseError

    const eventDetail: TFileValidateDetail = { file, errorMessage: null }
    // CE-native deviation from React: callbacks cannot be passed declaratively in HTML,
    // so we expose an event hook where consumers can set detail.errorMessage.
    const validationEvent = new CustomEvent<TFileValidateDetail>('file-validate', {
      detail: eventDetail,
      bubbles: true,
      composed: true,
      cancelable: true,
    })
    this.dispatchEvent(validationEvent)

    if (eventDetail.errorMessage) return eventDetail.errorMessage
    if (validationEvent.defaultPrevented) return defaultFileUploadStrings.genericValidationRejection
    return null
  }

  private syncThumbnailUrls(nextFiles: FileItem[]) {
    const supportsObjectUrl =
      typeof URL !== 'undefined' &&
      typeof URL.createObjectURL === 'function' &&
      typeof URL.revokeObjectURL === 'function'

    if (!supportsObjectUrl) {
      this.thumbnailUrls = {}
      this.thumbnailFileById.clear()
      return
    }

    const nextIds = new Set(nextFiles.map((item) => item.fileId))
    const nextUrlMap: Record<string, string> = {}

    for (const [fileId, url] of Object.entries(this.thumbnailUrls)) {
      if (!nextIds.has(fileId)) {
        URL.revokeObjectURL(url)
        this.thumbnailFileById.delete(fileId)
      }
    }

    for (const item of nextFiles) {
      const previousUrl = this.thumbnailUrls[item.fileId]
      const previousFile = this.thumbnailFileById.get(item.fileId)

      if (!item.file || !this.isImageFile(item)) {
        if (previousUrl) {
          URL.revokeObjectURL(previousUrl)
          this.thumbnailFileById.delete(item.fileId)
        }
        continue
      }

      if (previousUrl && previousFile === item.file) {
        nextUrlMap[item.fileId] = previousUrl
        continue
      }

      if (previousUrl && previousFile !== item.file) {
        URL.revokeObjectURL(previousUrl)
      }

      const nextUrl = URL.createObjectURL(item.file)
      this.thumbnailFileById.set(item.fileId, item.file)
      nextUrlMap[item.fileId] = nextUrl
    }

    this.thumbnailUrls = nextUrlMap
  }

  private revokeAllThumbnailUrls() {
    if (typeof URL === 'undefined' || typeof URL.revokeObjectURL !== 'function') {
      this.thumbnailUrls = {}
      this.thumbnailFileById.clear()
      return
    }

    Object.values(this.thumbnailUrls).forEach((url) => URL.revokeObjectURL(url))
    this.thumbnailUrls = {}
    this.thumbnailFileById.clear()
  }

  /** Polite summary: prefer the transient "file added" announcement, then fall back to upload completion. */
  protected getSrUploadedMessage(): string {
    if (this.addedAnnouncement) return this.addedAnnouncement
    const { totalCount, uploadedCount } = countFileTransferStates(
      this.getCurrentFiles(),
      this.transfers,
    )
    if (totalCount === 0 || uploadedCount === 0) return ''
    return defaultFileUploadStrings.srFilesUploadedOfTotal(
      uploadedCount,
      totalCount,
      defaultFileUploadStrings.fileLabel(totalCount),
    )
  }

  /** Assertive summary for transfer errors (parity with React `FileUpload`). */
  protected getSrErrorsMessage(): string {
    const { totalCount, failedCount } = countFileTransferStates(
      this.getCurrentFiles(),
      this.transfers,
    )
    if (totalCount === 0 || failedCount === 0) return ''
    return defaultFileUploadStrings.srFilesFailedOfTotal(
      failedCount,
      totalCount,
      defaultFileUploadStrings.fileLabel(totalCount),
    )
  }

  private syncFormSubmitListener() {
    const nextForm = this.closest('form')
    if (this.connectedForm === nextForm) return

    this.teardownFormSubmitListener()
    this.connectedForm = nextForm

    if (this.connectedForm) {
      this.connectedForm.addEventListener('submit', this.onHostFormSubmit)
    }
  }

  private teardownFormSubmitListener() {
    if (!this.connectedForm) return
    this.connectedForm.removeEventListener('submit', this.onHostFormSubmit)
    this.connectedForm = null
  }

  private onHostFormSubmit = (event: Event) => {
    if (!this.required || this.uploadStrategy !== 'custom') return
    if (this.getCurrentFiles().length > 0) return

    this.validationErrorMessage = this.requiredSelectionMessage
    event.preventDefault()
  }
}
