import { html, LitElement, type TemplateResult } from 'lit'
import { property, state } from 'lit/decorators.js'
import { CSVOptions, CsvImporter, type ParseResult } from '@api-client/core/modeling/importers/CsvImporter.js'
import { type InMemorySchema } from '@api-client/core/modeling/importers/JsonSchemaImporter.js'
import { FilteringJsonSchemaImporter } from '@api-client/core/modeling/importers/FilteringJsonSchemaImporter.js'
import { FileDropMixin } from '../../mixins/FileDropMixin.js'
import type { DataDomain } from '@api-client/core/modeling/DataDomain.js'
import type { TabSelectionDetail } from '../../md/tabs/ui-tabs.js'
import { fromError } from '../../exceptions/builder.js'
import type { DomainSelectionInfo } from '../types.js'
import type { UiSelectChangeEvent } from '../../md/select/ui-select.js'

const Base = FileDropMixin(LitElement)

enum ImportType {
  unknown,
  CSV,
  JSON,
}

enum ProcessState {
  idle,
  reading,
  previewing,
}

interface FileImportData {
  file: File
  type: ImportType
  data?: ParseResult
  processed?: boolean
}

/**
 * A component that allows the user to import files that describe data schemas.
 * Supports CSV files (with preview) and JSON schema files.
 *
 * @fires openentity - Fired when an entity is selected.
 * @fires openmodel - Fired when a model is selected.
 */
export default class DomainFileImport extends Base {
  /**
   * The instance of the root DataNamespace.
   */
  @property({ type: Object }) accessor root: DataDomain | undefined

  /**
   * The type of the files to be imported.
   */
  @state() accessor importType = ImportType.unknown

  /**
   * The state of the file import process.
   */
  @state() accessor processState = ProcessState.idle

  /**
   * The files that are currently being processed.
   */
  @state() accessor importFiles: FileImportData[] = []

  /**
   * The index of the currently selected file in the importFiles array.
   * This is used to keep track of which file preview is being rendered.
   */
  @state() accessor selectedFileIndex = 0

  /**
   * The user options for the CSV parser.
   * This is used to configure the CSV parser.
   */
  protected cvsOptions: CSVOptions | undefined

  constructor() {
    super()
    this.dropTarget = this
  }

  /**
   * Reports an error to the element.
   * It will dispatch an 'error' event with the Exception object as the detail.
   * It unifies the error handling across the application.
   * @param error The error to report.
   */
  reportError(error: Error): void {
    const e = fromError(error)
    this.dispatchEvent(
      new CustomEvent('error', {
        bubbles: true,
        composed: true,
        detail: e,
      })
    )
  }

  handleFileChange(e: Event): void {
    const inputElement = e.target as HTMLInputElement
    const fileList: FileList | null = inputElement.files
    if (fileList && fileList.length > 0) {
      this.processDroppedFiles(fileList)
    }
  }

  handleTriggerFile(): void {
    const input = this.shadowRoot?.querySelector<HTMLInputElement>('input[type="file"]')
    input?.click()
  }

  override async processDroppedFiles(files: FileList): Promise<void> {
    if (!files.length) {
      return
    }

    this.processState = ProcessState.reading
    this.importFiles = []
    this.selectedFileIndex = 0

    const fileData: FileImportData[] = []
    let detectedType = ImportType.unknown

    // 1. Analyze files and determine types
    for (const file of Array.from(files)) {
      const fileType = this.detectFileType(file)
      if (fileType === ImportType.unknown) {
        continue
      }

      // Ensure all files are of the same type
      if (detectedType === ImportType.unknown) {
        detectedType = fileType
      } else if (detectedType !== fileType) {
        // eslint-disable-next-line no-console
        console.error('Mixed file types are not supported')
        this.processState = ProcessState.idle
        return
      }

      fileData.push({
        file,
        type: fileType,
        processed: false,
      })
    }

    this.importType = detectedType
    this.importFiles = fileData

    try {
      if (detectedType === ImportType.CSV) {
        await this.handleMultipleCsvFiles()
      } else if (detectedType === ImportType.JSON) {
        await this.handleMultipleJsonFiles()
      }
    } catch (e) {
      this.reportError(e as Error)
      this.processState = ProcessState.idle
    }
  }

  /**
   * Detects the file type based on file extension and MIME type.
   */
  private detectFileType(file: File): ImportType {
    const fileName = file.name.toLowerCase()
    const mimeType = file.type

    if (fileName.endsWith('.csv') || mimeType === 'text/csv') {
      return ImportType.CSV
    }

    if (fileName.endsWith('.json') || mimeType === 'application/json') {
      return ImportType.JSON
    }

    return ImportType.unknown
  }

  /**
   * Handles multiple CSV files by parsing the first one for preview.
   */
  async handleMultipleCsvFiles(): Promise<void> {
    if (!this.importFiles.length || !this.root) {
      return
    }

    for (const fileData of this.importFiles) {
      if (fileData.processed) {
        continue
      }
      try {
        await this.processCsvFileData(fileData)
        fileData.processed = true
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error(e)
        throw new Error(`Error processing CSV file ${fileData.file.name}: ${(e as Error).message}`)
      }
    }
    this.processState = ProcessState.previewing
  }

  /**
   * Handles multiple JSON schema files by processing them directly.
   */
  async handleMultipleJsonFiles(): Promise<void> {
    if (!this.importFiles.length || !this.root) {
      return
    }

    // @TODO: we should do this in batches to avoid memory issues with large files,
    // but also API payload size limits. To do it, we should split it into several phases:
    // 1. Import entities only.
    // 2. Import properties only.
    // 3. Import associations only.
    // 4. Import parents metadata.
    const importer = new FilteringJsonSchemaImporter(this.root, {
      excludeConceptualMarkers: true,
    })
    const input: InMemorySchema[] = []

    for (const fileData of this.importFiles) {
      try {
        const text = await fileData.file.text()
        const contents = JSON.parse(text)
        input.push({ contents, path: fileData.file.name })
        fileData.processed = true
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error(e)
        throw new Error(`Error processing JSON file ${fileData.file.name}: ${(e as Error).message}`)
      }
    }

    const report = await importer.import(input, 'JSON Schema Import')
    this.dispatchEvent(
      new CustomEvent<DomainSelectionInfo>('openmodel', {
        bubbles: true,
        composed: true,
        detail: { key: report.model.key, kind: report.model.kind, domain: this.root.key },
      })
    )
    this.root.notifyChange()
    this.processState = ProcessState.idle
  }

  /**
   * Processes a single CSV file data with the current settings.
   */
  async processCsvFileData(fileData: FileImportData): Promise<void> {
    if (!this.root) {
      return
    }

    const opts = this.cvsOptions || {}
    if (!opts.maxRows) {
      opts.maxRows = 10
    }

    const parser = new CsvImporter(this.root, opts)
    fileData.data = await parser.parse(fileData.file)
  }

  /**
   * Reprocesses all CSV files with current options.
   */
  async reprocessCsvFiles(): Promise<void> {
    if (!this.root) {
      return
    }

    this.processState = ProcessState.reading
    for (const fileData of this.importFiles) {
      if (fileData.type === ImportType.CSV) {
        await this.processCsvFileData(fileData)
      }
    }

    this.processState = ProcessState.previewing
  }

  protected handleCsvOptionChange(e: CustomEvent<UiSelectChangeEvent>): void {
    const node = e.currentTarget as HTMLSelectElement
    const { value, name } = node
    if (!value) {
      return
    }
    if (name !== 'delimiter' && name !== 'quote') {
      return
    }
    if (!this.cvsOptions) {
      this.cvsOptions = {}
    }
    this.cvsOptions[name] = value
    this.reprocessCsvFiles()
  }

  protected async handleCsvSave(): Promise<void> {
    const { root } = this
    if (!root) {
      return
    }

    const parser = new CsvImporter(root)
    const files = this.importFiles.map((info) => info.data as ParseResult).filter(Boolean)
    const result = await parser.importMany(files, 'CSV Import')

    this.dispatchEvent(
      new CustomEvent<DomainSelectionInfo>('openmodel', {
        bubbles: true,
        composed: true,
        detail: { key: result.model.key, kind: result.model.kind, domain: root.key },
      })
    )

    root.notifyChange()
    this.processState = ProcessState.idle
  }

  protected handlePreviewTabChange(e: CustomEvent<TabSelectionDetail>): void {
    const { detail } = e
    this.selectedFileIndex = detail.index
    this.requestUpdate()
  }

  protected override render(): TemplateResult {
    const { processState } = this
    switch (processState) {
      case ProcessState.reading:
        return this.renderLoadingState()
      case ProcessState.previewing:
        return this.renderDataPreview()
      default:
        return this.renderDropTarget()
    }
  }

  protected renderHeader(): TemplateResult {
    let message = ''
    if (this.processState === ProcessState.previewing) {
      if (this.importType === ImportType.CSV) {
        const fileCount = this.importFiles.length || 1
        message = `Configure CSV file import (${fileCount} file${fileCount > 1 ? 's' : ''})`
      }
    } else {
      message = 'You can upload files that describe your data. Supported formats: CSV and JSON schema files.'
    }
    return html`
      <div class="header">
        <h2 class="title-large">Import schema</h2>
        <p class="body-medium">${message}</p>
      </div>
    `
  }

  protected renderDropTarget(): TemplateResult {
    return html`
      ${this.renderHeader()}
      <div class="file-import drop-info">
        <button class="trigger-button" type="button" @click=${this.handleTriggerFile}>
          <ui-icon class="trigger-icon">description</ui-icon>
        </button>
        <p class="body-large">Drop files here.</p>
        <p class="body-medium">Supported: *.csv, *.json (JSON Schema) files.</p>
        <input hidden type="file" @change=${this.handleFileChange} accept=".csv,.json" multiple />
      </div>
    `
  }

  protected renderLoadingState(): TemplateResult {
    return html`
      <div class="import-loader">
        <p class="body-large">Processing files...</p>
      </div>
    `
  }

  protected renderDataPreview(): TemplateResult {
    switch (this.importType) {
      case ImportType.CSV:
        return this.renderCsvPreview()
      default:
        return html`<p>Unsupported file type</p>`
    }
  }

  protected renderCsvPreview(): TemplateResult {
    const previewData = this.importFiles[this.selectedFileIndex]?.data
    const typed = previewData as ParseResult
    if (!typed) {
      return html`<p>No data available</p>`
    }

    const fileCount = this.importFiles.length || 1

    return html`
      ${this.renderHeader()} ${this.renderCsvImportOptions()} ${this.renderPreviewOrTabs()}
      <div class="table-wrapper">
        <table class="data-table">
          <thead class="label-large">
            <tr>
              ${typed.format.map((row) => html` <th scope="col">${row.name}</th> `)}
            </tr>
          </thead>
          <tbody class="body-medium">
            ${typed.values.map(
              (row) => html`
                <tr>
                  ${row.map((item) => html` <td>${item}</td> `)}
                </tr>
              `
            )}
          </tbody>
        </table>
      </div>
      <div class="actions">
        <ui-button color="filled" class="action-button" @click="${this.handleCsvSave}">
          Import ${fileCount} file${fileCount > 1 ? 's' : ''}
        </ui-button>
      </div>
    `
  }

  protected renderPreviewOrTabs(): TemplateResult {
    if (this.importFiles.length <= 1) {
      return html`<h3 class="title-medium">Preview</h3>`
    }

    return html`
      <ui-tabs @change="${this.handlePreviewTabChange}">
        ${this.importFiles.map(
          (fileData, index) => html`
            <ui-tab aria-controls="preview${index}" data-index="${index}" ?selected=${this.selectedFileIndex === index}
              >${fileData.file.name}</ui-tab
            >
          `
        )}
      </ui-tabs>
    `
  }

  protected renderCsvImportOptions(): TemplateResult {
    const { cvsOptions = {} } = this
    const { quote = '"', delimiter = ',' } = cvsOptions
    const delimiters = [',', ';', '|']
    const quotes = ['"', "'", '`']
    return html`<div class="csv-options">
      <h3 class="title-medium">Import options</h3>
      <div class="options">
        <ui-select
          label="Separator character"
          class="input"
          name="delimiter"
          .value="${delimiter}"
          @change="${this.handleCsvOptionChange}"
        >
          ${delimiters.map(
            (item) => html`
              <ui-option value="${item}">
                <div>${item}</div>
              </ui-option>
            `
          )}
        </ui-select>
        <ui-select
          label="Quote character"
          class="input"
          name="quote"
          .value="${quote}"
          @change="${this.handleCsvOptionChange}"
        >
          ${quotes.map(
            (item) => html`
              <ui-option value="${item}">
                <div>${item}</div>
              </ui-option>
            `
          )}
        </ui-select>
      </div>
    </div>`
  }
}
