import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
import { demoFunc } from './demo.js';
import { fileuploadStyles } from './styles.js';
import '../../dees-icon/dees-icon.js';
import '../../dees-label/dees-label.js';

import {
  customElement,
  html,
  property,
  state,
  type TemplateResult,
} from '@design.estate/dees-element';

declare global {
  interface HTMLElementTagNameMap {
    'dees-input-fileupload': DeesInputFileupload;
  }
}

@customElement('dees-input-fileupload')
export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
  public static demo = demoFunc;
  public static demoGroup = 'Input';

  @property({ attribute: false })
  accessor value: File[] = [];

  @state()
  accessor state: 'idle' | 'dragOver' | 'dropped' | 'uploading' | 'completed' = 'idle';

  @state()
  accessor isLoading: boolean = false;

  @property({ type: String })
  accessor buttonText: string = 'Select files';

  @property({ type: String })
  accessor accept: string = '';

  @property({ type: Boolean })
  accessor multiple: boolean = true;

  @property({ type: Number })
  accessor maxSize: number = 0; // 0 means no limit

  @property({ type: Number })
  accessor maxFiles: number = 0; // 0 means no limit

  @property({ type: String, reflect: true })
  accessor validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;

  accessor validationMessage: string = '';

  private previewUrlMap: WeakMap<File, string> = new WeakMap();
  private dropArea: HTMLElement | null = null;

  public static styles = fileuploadStyles;

  public render(): TemplateResult {
    const acceptedSummary = this.getAcceptedSummary();
    const metaEntries: string[] = [
      this.multiple ? 'Multiple files supported' : 'Single file only',
      this.maxSize > 0 ? `Max ${this.formatFileSize(this.maxSize)}` : 'No size limit',
    ];

    if (acceptedSummary) {
      metaEntries.push(`Accepts ${acceptedSummary}`);
    }

    return html`
      <div class="input-wrapper">
        <dees-label
          .label=${this.label}
          .description=${this.description}
          .required=${this.required}
        ></dees-label>
        <div
          class="dropzone ${this.state === 'dragOver' ? 'dropzone--active' : ''} ${this.disabled ? 'dropzone--disabled' : ''} ${this.value.length > 0 ? 'dropzone--has-files' : ''}"
          role="button"
          tabindex=${this.disabled ? -1 : 0}
          aria-disabled=${this.disabled}
          aria-label=${`Select files${acceptedSummary ? ` (${acceptedSummary})` : ''}`}
          @click=${this.handleDropzoneClick}
          @keydown=${this.handleDropzoneKeydown}
        >
          <input
            class="file-input"
            style="position: absolute; opacity: 0; pointer-events: none; width: 1px; height: 1px; top: 0; left: 0; overflow: hidden;"
            type="file"
            ?multiple=${this.multiple}
            accept=${this.accept || ''}
            ?disabled=${this.disabled}
            @change=${this.handleFileInputChange}
            tabindex="-1"
          />
          <div class="dropzone__body">
            <div class="dropzone__icon">
              ${this.isLoading
                ? html`<span class="dropzone__loader" aria-hidden="true"></span>`
                : html`<dees-icon icon="lucide:FolderOpen"></dees-icon>`}
            </div>
            <div class="dropzone__content">
              <span class="dropzone__headline">${this.buttonText || 'Select files'}</span>
              <span class="dropzone__subline">
                Drag and drop files here or
                <button
                  type="button"
                  class="dropzone__browse"
                  @click=${this.handleBrowseClick}
                  ?disabled=${this.disabled}
                >
                  browse
                </button>
              </span>
            </div>
          </div>
          <div class="dropzone__meta">
            ${metaEntries.map((entry) => html`<span>${entry}</span>`)}
          </div>
          ${this.renderFileList()}
        </div>
        ${this.validationMessage
          ? html`<div class="validation-message" aria-live="polite">${this.validationMessage}</div>`
          : html``}
      </div>
    `;
  }

  private renderFileList(): TemplateResult {
    if (this.value.length === 0) {
      return html``;
    }

    return html`
      <div class="file-list">
        <div class="file-list__header">
          <span>${this.value.length} file${this.value.length === 1 ? '' : 's'} selected</span>
          ${this.value.length > 0
            ? html`<button type="button" class="file-list__clear" @click=${this.handleClearAll}>Clear ${this.value.length > 1 ? 'all' : ''}</button>`
            : html``}
        </div>
        <div class="file-list__items">
          ${this.value.map((file) => this.renderFileRow(file))}
        </div>
      </div>
    `;
  }

  private renderFileRow(file: File): TemplateResult {
    const fileType = this.getFileType(file);
    const previewUrl = this.canShowPreview(file) ? this.getPreviewUrl(file) : null;

    return html`
      <div class="file-row ${fileType}-file">
        <div class="file-thumb" aria-hidden="true">
          ${previewUrl
            ? html`<img class="thumb-image" src=${previewUrl} alt=${`Preview of ${file.name}`}>`
            : html`<dees-icon icon=${this.getFileIcon(file)}></dees-icon>`}
        </div>
        <div class="file-meta">
          <div class="file-name" title=${file.name}>${file.name}</div>
          <div class="file-details">
            <span class="file-size">${this.formatFileSize(file.size)}</span>
            ${fileType !== 'file' ? html`<span class="file-type">${fileType}</span>` : html``}
          </div>
        </div>
        <div class="file-actions">
          <button
            type="button"
            class="remove-button"
            @click=${() => this.removeFile(file)}
            aria-label=${`Remove ${file.name}`}
          >
            <dees-icon icon="lucide:X"></dees-icon>
          </button>
        </div>
      </div>
    `;
  }

  private handleFileInputChange = async (event: Event) => {
    this.isLoading = false;
    const target = event.target as HTMLInputElement;
    const files = Array.from(target.files ?? []);
    if (files.length > 0) {
      await this.addFiles(files);
    }
    target.value = '';
  };

  private handleDropzoneClick = (event: MouseEvent) => {
    if (this.disabled) {
      return;
    }
    // Don't open file selector if clicking on the browse button or file list
    if ((event.target as HTMLElement).closest('.dropzone__browse, .file-list')) {
      return;
    }
    this.openFileSelector();
  };

  private handleBrowseClick = (event: MouseEvent) => {
    if (this.disabled) {
      return;
    }
    event.stopPropagation(); // Stop propagation to prevent double trigger
    this.openFileSelector();
  };

  private handleDropzoneKeydown = (event: KeyboardEvent) => {
    if (this.disabled) {
      return;
    }
    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      this.openFileSelector();
    }
  };

  private handleClearAll = (event: MouseEvent) => {
    event.preventDefault();
    this.clearAll();
  };

  private handleDragEvent = async (event: DragEvent) => {
    event.preventDefault();
    event.stopPropagation();

    if (this.disabled) {
      return;
    }

    if (event.type === 'dragenter' || event.type === 'dragover') {
      if (event.dataTransfer) {
        event.dataTransfer.dropEffect = 'copy';
      }
      this.state = 'dragOver';
      return;
    }

    if (event.type === 'dragleave') {
      if (!this.dropArea) {
        this.state = 'idle';
        return;
      }
      const rect = this.dropArea.getBoundingClientRect();
      const { clientX = 0, clientY = 0 } = event;
      if (clientX <= rect.left || clientX >= rect.right || clientY <= rect.top || clientY >= rect.bottom) {
        this.state = 'idle';
      }
      return;
    }

    if (event.type === 'drop') {
      this.state = 'idle';
      const files = Array.from(event.dataTransfer?.files ?? []);
      if (files.length > 0) {
        await this.addFiles(files);
      }
    }
  };

  private attachDropListeners(): void {
    if (!this.dropArea) {
      return;
    }
    ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
      this.dropArea!.addEventListener(eventName, this.handleDragEvent);
    });
  }

  private detachDropListeners(): void {
    if (!this.dropArea) {
      return;
    }
    ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
      this.dropArea!.removeEventListener(eventName, this.handleDragEvent);
    });
  }

  private rebindInteractiveElements(): void {
    const newDropArea = this.shadowRoot?.querySelector('.dropzone') as HTMLElement | null;

    if (newDropArea !== this.dropArea) {
      this.detachDropListeners();
      this.dropArea = newDropArea;
      this.attachDropListeners();
    }
  }

  public formatFileSize(bytes: number): string {
    const units = ['Bytes', 'KB', 'MB', 'GB'];
    if (bytes === 0) return '0 Bytes';
    const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
    const size = bytes / Math.pow(1024, exponent);
    return `${Math.round(size * 100) / 100} ${units[exponent]}`;
  }

  public getFileType(file: File): string {
    const type = file.type.toLowerCase();
    if (type.startsWith('image/')) return 'image';
    if (type === 'application/pdf') return 'pdf';
    if (type.includes('word') || type.includes('document')) return 'doc';
    if (type.includes('sheet') || type.includes('excel')) return 'spreadsheet';
    if (type.includes('presentation') || type.includes('powerpoint')) return 'presentation';
    if (type.startsWith('video/')) return 'video';
    if (type.startsWith('audio/')) return 'audio';
    if (type.includes('zip') || type.includes('compressed')) return 'archive';
    return 'file';
  }

  public getFileIcon(file: File): string {
    const fileType = this.getFileType(file);
    const iconMap: Record<string, string> = {
      image: 'lucide:FileImage',
      pdf: 'lucide:FileText',
      doc: 'lucide:FileText',
      spreadsheet: 'lucide:FileSpreadsheet',
      presentation: 'lucide:FileBarChart',
      video: 'lucide:FileVideo',
      audio: 'lucide:FileAudio',
      archive: 'lucide:FileArchive',
      file: 'lucide:File',
    };
    return iconMap[fileType] ?? 'lucide:File';
  }

  public canShowPreview(file: File): boolean {
    return file.type.startsWith('image/') && file.size < 5 * 1024 * 1024;
  }

  private validateFile(file: File): boolean {
    if (this.maxSize > 0 && file.size > this.maxSize) {
      this.validationMessage = `File "${file.name}" exceeds the maximum size of ${this.formatFileSize(this.maxSize)}`;
      this.validationState = 'invalid';
      return false;
    }

    if (this.accept) {
      const acceptedTypes = this.accept
        .split(',')
        .map((entry) => entry.trim())
        .filter((entry) => entry.length > 0);

      if (acceptedTypes.length > 0) {
        let isAccepted = false;
        for (const acceptType of acceptedTypes) {
          if (acceptType.startsWith('.')) {
            if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) {
              isAccepted = true;
              break;
            }
          } else if (acceptType.endsWith('/*')) {
            const prefix = acceptType.slice(0, -2);
            if (file.type.startsWith(prefix)) {
              isAccepted = true;
              break;
            }
          } else if (file.type === acceptType) {
            isAccepted = true;
            break;
          }
        }

        if (!isAccepted) {
          this.validationMessage = `File type not accepted. Allowed: ${acceptedTypes.join(', ')}`;
          this.validationState = 'invalid';
          return false;
        }
      }
    }

    return true;
  }

  private getPreviewUrl(file: File): string {
    let url = this.previewUrlMap.get(file);
    if (!url) {
      url = URL.createObjectURL(file);
      this.previewUrlMap.set(file, url);
    }
    return url;
  }

  private releasePreview(file: File): void {
    const url = this.previewUrlMap.get(file);
    if (url) {
      URL.revokeObjectURL(url);
      this.previewUrlMap.delete(file);
    }
  }

  private getAcceptedSummary(): string | null {
    if (!this.accept) {
      return null;
    }

    const formatted = Array.from(
      new Set(
        this.accept
          .split(',')
          .map((token) => token.trim())
          .filter((token) => token.length > 0)
          .map((token) => this.formatAcceptToken(token))
      )
    ).filter(Boolean);

    if (formatted.length === 0) {
      return null;
    }

    if (formatted.length === 1) {
      return formatted[0];
    }

    if (formatted.length === 2) {
      return `${formatted[0]}, ${formatted[1]}`;
    }

    return `${formatted.slice(0, 2).join(', ')}…`;
  }

  private formatAcceptToken(token: string): string {
    if (token === '*/*') {
      return 'All files';
    }

    if (token.endsWith('/*')) {
      const family = token.split('/')[0];
      if (!family) {
        return 'All files';
      }
      return `${family.charAt(0).toUpperCase()}${family.slice(1)} files`;
    }

    if (token.startsWith('.')) {
      return token.slice(1).toUpperCase();
    }

    if (token.includes('pdf')) return 'PDF';
    if (token.includes('zip')) return 'ZIP';
    if (token.includes('json')) return 'JSON';
    if (token.includes('msword')) return 'DOC';
    if (token.includes('wordprocessingml')) return 'DOCX';
    if (token.includes('excel')) return 'XLS';
    if (token.includes('presentation')) return 'PPT';

    const segments = token.split('/');
    const lastSegment = segments.pop() ?? token;
    return lastSegment.toUpperCase();
  }

  private attachLifecycleListeners(): void {
    this.rebindInteractiveElements();
  }

  public firstUpdated(changedProperties: Map<string, unknown>) {
    super.firstUpdated(changedProperties);
    this.attachLifecycleListeners();
  }

  public updated(changedProperties: Map<string, unknown>) {
    super.updated(changedProperties);
    if (changedProperties.has('value')) {
      void this.validate();
    }
    this.rebindInteractiveElements();
  }

  public async disconnectedCallback(): Promise<void> {
    this.detachDropListeners();
    this.value.forEach((file) => this.releasePreview(file));
    this.previewUrlMap = new WeakMap();
    await super.disconnectedCallback();
  }

  public async openFileSelector() {
    if (this.disabled || this.isLoading) {
      return;
    }

    this.isLoading = true;

    // Ensure we have the latest reference to the file input
    const inputFile = this.shadowRoot?.querySelector('.file-input') as HTMLInputElement | null;

    if (!inputFile) {
      this.isLoading = false;
      return;
    }

    const handleFocus = () => {
      setTimeout(() => {
        if (!inputFile.files || inputFile.files.length === 0) {
          this.isLoading = false;
        }
        window.removeEventListener('focus', handleFocus);
      }, 300);
    };

    window.addEventListener('focus', handleFocus);

    // Click the input to open file selector
    inputFile.click();
  }

  public removeFile(file: File) {
    const index = this.value.indexOf(file);
    if (index > -1) {
      this.releasePreview(file);
      this.value.splice(index, 1);
      this.requestUpdate('value');
      void this.validate();
      this.changeSubject.next(this);
    }
  }

  public clearAll() {
    const existingFiles = [...this.value];
    this.value = [];
    existingFiles.forEach((file) => this.releasePreview(file));
    this.requestUpdate('value');
    void this.validate();
    this.changeSubject.next(this);
    this.buttonText = 'Select files';
  }

  public async updateValue(eventArg: Event) {
    const target = eventArg.target as HTMLInputElement;
    this.value = Array.from(target.files ?? []);
    this.changeSubject.next(this);
  }

  public setValue(value: File[]): void {
    this.value.forEach((file) => this.releasePreview(file));
    this.value = value;
    if (value.length > 0) {
      this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
    } else {
      this.buttonText = 'Select files';
    }
    this.requestUpdate('value');
    void this.validate();
  }

  public getValue(): File[] {
    return this.value;
  }

  private async addFiles(files: File[]) {
    const filesToAdd: File[] = [];

    for (const file of files) {
      if (this.validateFile(file)) {
        filesToAdd.push(file);
      }
    }

    if (filesToAdd.length === 0) {
      this.isLoading = false;
      return;
    }

    if (this.maxFiles > 0) {
      const totalFiles = this.value.length + filesToAdd.length;
      if (totalFiles > this.maxFiles) {
        const allowedCount = this.maxFiles - this.value.length;
        if (allowedCount <= 0) {
          this.validationMessage = `Maximum ${this.maxFiles} files allowed`;
          this.validationState = 'invalid';
          this.isLoading = false;
          return;
        }
        filesToAdd.splice(allowedCount);
        this.validationMessage = `Only ${allowedCount} more file(s) can be added`;
        this.validationState = 'warn';
      }
    }

    if (!this.multiple && filesToAdd.length > 0) {
      this.value.forEach((file) => this.releasePreview(file));
      this.value = [filesToAdd[0]];
    } else {
      this.value.push(...filesToAdd);
    }

    this.validationMessage = '';
    this.validationState = null;
    this.requestUpdate('value');
    await this.validate();
    this.changeSubject.next(this);
    this.isLoading = false;

    if (this.value.length > 0) {
      this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
    } else {
      this.buttonText = 'Select files';
    }
  }

  public async validate(): Promise<boolean> {
    this.validationMessage = '';

    if (this.required && this.value.length === 0) {
      this.validationState = 'invalid';
      this.validationMessage = 'Please select at least one file';
      return false;
    }

    for (const file of this.value) {
      if (!this.validateFile(file)) {
        return false;
      }
    }

    this.validationState = this.value.length > 0 ? 'valid' : null;
    return true;
  }
}
