import * as plugins from '../00plugins.js';
import { demoFunc } from './dees-table.demo.js';
import { customElement, html, DeesElement, property, type TemplateResult, directives } from '@design.estate/dees-element';

import { DeesContextmenu } from '../dees-contextmenu/dees-contextmenu.js';
import * as domtools from '@design.estate/dees-domtools';
import { type TIconKey } from '../dees-icon/dees-icon.js';
import { tableStyles } from './styles.js';
import type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';
import {
  computeColumnsFromDisplayFunction as computeColumnsFromDisplayFunctionFn,
  computeEffectiveColumns as computeEffectiveColumnsFn,
  getCellValue as getCellValueFn,
  getViewData as getViewDataFn,
} from './data.js';
import { compileLucenePredicate } from './lucene.js';
import { themeDefaultStyles } from '../00theme.js';

export type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';

declare global {
  interface HTMLElementTagNameMap {
    'dees-table': DeesTable<any>;
  }
}

// interfaces moved to ./types.ts and re-exported above

// the table implementation
@customElement('dees-table')
export class DeesTable<T> extends DeesElement {
  public static demo = demoFunc;

  // INSTANCE
  @property({
    type: String,
  })
  accessor heading1: string = 'heading 1';

  @property({
    type: String,
  })
  accessor heading2: string = 'heading 2';

  @property({
    type: Array,
  })
  accessor data: T[] = [];

  // dees-form compatibility -----------------------------------------
  @property({
    type: String,
  })
  accessor key: string;

  @property({
    type: String,
  })
  accessor label: string;

  @property({
    type: Boolean,
  })
  accessor disabled: boolean = false;

  @property({
    type: Boolean,
  })
  accessor required: boolean = false;

  get value() {
    return this.data;
  }
  set value(_valueArg) {}
  public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<DeesTable<T>>();
  // end dees-form compatibility -----------------------------------------

  /**
   * What does a row of data represent?
   */
  @property({
    type: String,
    reflect: true,
  })
  accessor dataName: string;


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

  @property({
    type: Array,
  })
  accessor dataActions: ITableAction<T>[] = [];

  // schema-first columns API
  @property({ attribute: false })
  accessor columns: Column<T>[] = [];

  /**
   * Stable row identity for selection and updates. If provided as a function,
   * it is only usable as a property (not via attribute).
   */
  @property({ attribute: false })
  accessor rowKey: keyof T | ((row: T) => string) | undefined = undefined;

  /**
   * When true and columns are provided, merge any missing columns discovered
   * via displayFunction into the effective schema.
   */
  @property({ type: Boolean })
  accessor augmentFromDisplayFunction: boolean = false;

  @property({
    attribute: false,
  })
  accessor displayFunction: TDisplayFunction = (itemArg: T) => itemArg as any;

  @property({
    attribute: false,
  })
  accessor reverseDisplayFunction: (itemArg: any) => T = (itemArg: any) => itemArg as T;

  @property({
    type: Object,
  })
  accessor selectedDataRow: T;

  @property({
    type: Array,
  })
  accessor editableFields: string[] = [];

  @property({
    type: Boolean,
    reflect: true,
    attribute: 'show-vertical-lines'
  })
  accessor showVerticalLines: boolean = false;

  @property({
    type: Boolean,
    reflect: true,
    attribute: 'show-horizontal-lines'
  })
  accessor showHorizontalLines: boolean = false;

  @property({
    type: Boolean,
    reflect: true,
    attribute: 'show-grid'
  })
  accessor showGrid: boolean = true;

  public files: File[] = [];
  public fileWeakMap = new WeakMap();

  public dataChangeSubject = new domtools.plugins.smartrx.rxjs.Subject();

  // simple client-side sorting (Phase 1)
  @property({ attribute: false })
  accessor sortKey: string | undefined = undefined;
  @property({ attribute: false })
  accessor sortDir: 'asc' | 'desc' | null = null;

  // simple client-side filtering (Phase 1)
  @property({ type: String })
  accessor filterText: string = '';
  // per-column quick filters
  @property({ attribute: false })
  accessor columnFilters: Record<string, string> = {};
  @property({ type: Boolean, attribute: 'show-column-filters' })
  accessor showColumnFilters: boolean = false;
  @property({ type: Boolean, reflect: true, attribute: 'sticky-header' })
  accessor stickyHeader: boolean = false;

  // search row state
  @property({ type: String })
  accessor searchMode: 'table' | 'data' | 'server' = 'table';
  private __searchTextSub?: { unsubscribe?: () => void };
  private __searchModeSub?: { unsubscribe?: () => void };

  // selection (Phase 1)
  @property({ type: String })
  accessor selectionMode: 'none' | 'single' | 'multi' = 'none';
  @property({ attribute: false })
  accessor selectedIds: Set<string> = new Set();
  private _rowIdMap = new WeakMap<object, string>();
  private _rowIdCounter = 0;

  constructor() {
    super();
  }

  public static styles = tableStyles;

  public render(): TemplateResult {
    const usingColumns = Array.isArray(this.columns) && this.columns.length > 0;
    const effectiveColumns: Column<T>[] = usingColumns
      ? computeEffectiveColumnsFn(this.columns, this.augmentFromDisplayFunction, this.displayFunction, this.data)
      : computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data);

    const lucenePred = compileLucenePredicate<T>(
      this.filterText,
      this.searchMode === 'data' ? 'data' : 'table',
      effectiveColumns
    );

    const viewData = getViewDataFn(
      this.data,
      effectiveColumns,
      this.sortKey,
      this.sortDir,
      this.filterText,
      this.columnFilters,
      this.searchMode === 'data' ? 'data' : 'table',
      lucenePred || undefined
    );
    (this as any)._lastViewData = viewData;
    return html`
      <div class="mainbox">
        <!-- the heading part -->
        <div class="header">
          <div class="headingContainer">
            <div class="heading heading1">${this.label || this.heading1}</div>
            <div class="heading heading2">${this.heading2}</div>
          </div>
          <div class="headerActions">
            ${directives.resolveExec(async () => {
              const resultArray: TemplateResult[] = [];
              for (const action of this.dataActions) {
                if (!action.type.includes('header')) continue;
                resultArray.push(
                  html`<div
                    class="headerAction"
                    @click=${() => {
                      action.actionFunc({
                        item: this.selectedDataRow,
                        table: this,
                      });
                    }}
                  >
                    ${action.iconName
                      ? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
                          ${action.name}`
                      : action.name}
                  </div>`
                );
              }
              return resultArray;
            })}
          </div>
        </div>
        <div class="headingSeparation"></div>
        <div class="searchGrid hidden">
          <dees-input-text
            .label=${'lucene syntax search'}
            .description=${`
              You can use the lucene syntax to search for data, e.g.:
              
              \`\`\`
              name: "john" AND age: 18
              \`\`\`
              
            `}
          ></dees-input-text>
          <dees-input-multitoggle
            .label=${'search mode'}
            .options=${['table', 'data', 'server']}
            .selectedOption=${'table'}
            .description=${`
              There are three basic modes:
              
              * table: only searches data already in the table
              * data: searches original data, ignoring table transforms
              * server: searches data on the server
              
            `}
          ></dees-input-multitoggle>
        </div>

        <!-- the actual table -->
        <style></style>
        ${this.data.length > 0
          ? html`
              <div class="tableScroll">
              <table>
                <thead>
                  <tr>
                    ${this.selectionMode !== 'none'
                      ? html`
                          <th style="width:42px; text-align:center;">
                            ${this.selectionMode === 'multi'
                              ? html`
                                  <dees-input-checkbox
                                    .value=${this.areAllVisibleSelected()}
                                    .indeterminate=${this.isVisibleSelectionIndeterminate()}
                                    @newValue=${(e: CustomEvent<boolean>) => {
                                      e.stopPropagation();
                                      this.setSelectVisible(e.detail === true);
                                    }}
                                  ></dees-input-checkbox>
                                `
                              : html``}
                          </th>
                        `
                      : html``}
                    ${effectiveColumns
                      .filter((c) => !c.hidden)
                      .map((col) => {
                        const isSortable = !!col.sortable;
                        const ariaSort = this.getAriaSort(col);
                        return html`
                          <th
                            role="columnheader"
                            aria-sort=${ariaSort}
                            style="${isSortable ? 'cursor: pointer;' : ''}"
                            @click=${() => (isSortable ? this.toggleSort(col) : null)}
                          >
                            ${col.header ?? (col.key as any)}
                            ${this.renderSortIndicator(col)}
                          </th>`;
                      })}
                    ${(() => {
                      if (this.dataActions && this.dataActions.length > 0) {
                        return html` <th class="actionsCol">Actions</th> `;
                      }
                    })()}
                  </tr>
                  ${this.showColumnFilters
                    ? html`<tr class="filtersRow">
                        ${this.selectionMode !== 'none'
                          ? html`<th style="width:42px;"></th>`
                          : html``}
                        ${effectiveColumns
                          .filter((c) => !c.hidden)
                          .map((col) => {
                            const key = String(col.key);
                            if (col.filterable === false) return html`<th></th>`;
                            return html`<th>
                              <input type="text" placeholder="Filter..." .value=${this.columnFilters[key] || ''}
                                @input=${(e: Event) => this.setColumnFilter(key, (e.target as HTMLInputElement).value)} />
                            </th>`;
                          })}
                        ${(() => {
                          if (this.dataActions && this.dataActions.length > 0) {
                            return html` <th></th> `;
                          }
                        })()}
                      </tr>`
                    : html``}
                </thead>
                <tbody>
                  ${viewData.map((itemArg, rowIndex) => {
                    const getTr = (elementArg: HTMLElement): HTMLElement => {
                      if (elementArg.tagName === 'TR') {
                        return elementArg;
                      } else {
                        return getTr(elementArg.parentElement);
                      }
                    };
                    return html`
                      <tr
                        @click=${() => {
                          this.selectedDataRow = itemArg;
                          if (this.selectionMode === 'single') {
                            const id = this.getRowId(itemArg);
                            this.selectedIds.clear();
                            this.selectedIds.add(id);
                            this.emitSelectionChange();
                            this.requestUpdate();
                          }
                        }}
                        @dragenter=${async (eventArg: DragEvent) => {
                          eventArg.preventDefault();
                          eventArg.stopPropagation();
                          const realTarget = getTr(eventArg.target as HTMLElement);
                          setTimeout(() => {
                            realTarget.classList.add('hasAttachment');
                          }, 0);
                        }}
                        @dragleave=${async (eventArg: DragEvent) => {
                          eventArg.preventDefault();
                          eventArg.stopPropagation();
                          const realTarget = getTr(eventArg.target as HTMLElement);
                          realTarget.classList.remove('hasAttachment');
                        }}
                        @dragover=${async (eventArg: DragEvent) => {
                          eventArg.preventDefault();
                        }}
                        @drop=${async (eventArg: DragEvent) => {
                          eventArg.preventDefault();
                          const newFiles = [];
                          for (const file of Array.from(eventArg.dataTransfer.files)) {
                            this.files.push(file);
                            newFiles.push(file);
                            this.requestUpdate();
                          }
                          const result: File[] = this.fileWeakMap.get(itemArg as object);
                          if (!result) {
                            this.fileWeakMap.set(itemArg as object, newFiles);
                          } else {
                            result.push(...newFiles);
                          }
                        }}
                        @contextmenu=${async (eventArg: MouseEvent) => {
                          DeesContextmenu.openContextMenuWithOptions(
                            eventArg,
                            this.getActionsForType('contextmenu').map((action) => {
                              const menuItem: plugins.tsclass.website.IMenuItem = {
                                name: action.name,
                                iconName: action.iconName as any,
                                action: async () => {
                                  await action.actionFunc({
                                    item: itemArg,
                                    table: this,
                                  });
                                  return null;
                                },
                              };
                              return menuItem;
                            })
                          );
                        }}
                        class="${itemArg === this.selectedDataRow ? 'selected' : ''}"
                      >
                        ${this.selectionMode !== 'none'
                          ? html`<td style="width:42px; text-align:center;">
                              <dees-input-checkbox
                                .value=${this.isRowSelected(itemArg)}
                                @newValue=${(e: CustomEvent<boolean>) => {
                                  e.stopPropagation();
                                  this.setRowSelected(itemArg, e.detail === true);
                                }}
                              ></dees-input-checkbox>
                            </td>`
                          : html``}
                        ${effectiveColumns
                          .filter((c) => !c.hidden)
                          .map((col, colIndex) => {
                            const value = getCellValueFn(itemArg, col, this.displayFunction);
                            const content = col.renderer
                              ? col.renderer(value, itemArg, { rowIndex, colIndex, column: col })
                              : value;
                            const editKey = String(col.key);
                            return html`
                              <td
                                @dblclick=${(e: Event) => {
                                  const dblAction = this.dataActions.find((actionArg) =>
                                    actionArg.type.includes('doubleClick')
                                  );
                                  if (this.editableFields.includes(editKey)) {
                                    this.handleCellEditing(e, itemArg, editKey);
                                  } else if (dblAction) {
                                    dblAction.actionFunc({ item: itemArg, table: this });
                                  }
                                }}
                              >
                                <div class="innerCellContainer">${content}</div>
                              </td>
                            `;
                          })}
                        ${(() => {
                          if (this.dataActions && this.dataActions.length > 0) {
                            return html`
                              <td class="actionsCol">
                                <div class="actionsContainer">
                                  ${this.getActionsForType('inRow').map(
                                    (actionArg) => html`
                                      <div
                                        class="action"
                                        @click=${() =>
                                          actionArg.actionFunc({
                                            item: itemArg,
                                            table: this,
                                          })}
                                      >
                                        ${actionArg.iconName
                                          ? html` <dees-icon .icon=${actionArg.iconName}></dees-icon> `
                                          : actionArg.name}
                                      </div>
                                    `
                                  )}
                                </div>
                              </td>
                            `;
                          }
                        })()}
                      </tr>`;
                  })}
                </tbody>
              </table>
              </div>
            `
          : html` <div class="noDataSet">No data set!</div> `}
        <div class="footer">
          <div class="tableStatistics">
            ${this.data.length} ${this.dataName || 'data rows'} (total) |
            ${this.selectedDataRow ? '# ' + `${this.data.indexOf(this.selectedDataRow) + 1}` : `No`}
            selected
          </div>
          <div class="footerActions">
            ${directives.resolveExec(async () => {
              const resultArray: TemplateResult[] = [];
              for (const action of this.dataActions) {
                if (!action.type.includes('footer')) continue;
                resultArray.push(
                  html`<div
                    class="footerAction"
                    @click=${() => {
                      action.actionFunc({
                        item: this.selectedDataRow,
                        table: this,
                      });
                    }}
                  >
                    ${action.iconName
                      ? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
                          ${action.name}`
                      : action.name}
                  </div>`
                );
              }
              return resultArray;
            })}
          </div>
        </div>
      </div>
    `;
  }

  public async firstUpdated() {
    
  }

  public async updated(changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
    super.updated(changedProperties);
    this.determineColumnWidths();
    if (this.searchable) {
      const existing = this.dataActions.find((actionArg) => actionArg.type.includes('header') && actionArg.name === 'Search');
      if (!existing) {
        this.dataActions.unshift({
          name: 'Search',
          iconName: 'magnifyingGlass',
          type: ['header'],
          actionFunc: async () => {
            console.log('open search');
            const searchGrid = this.shadowRoot.querySelector('.searchGrid');
            searchGrid.classList.toggle('hidden');
          }
        });
        console.log(this.dataActions);
        this.requestUpdate();
      };
      // wire search inputs
      this.wireSearchInputs();
    }
  }

  private __debounceTimer?: any;
  private debounceRun(fn: () => void, ms = 200) {
    if (this.__debounceTimer) clearTimeout(this.__debounceTimer);
    this.__debounceTimer = setTimeout(fn, ms);
  }

  private wireSearchInputs() {
    const searchTextEl: any = this.shadowRoot?.querySelector('.searchGrid dees-input-text');
    const searchModeEl: any = this.shadowRoot?.querySelector('.searchGrid dees-input-multitoggle');
    if (searchTextEl && !this.__searchTextSub) {
      this.__searchTextSub = searchTextEl.changeSubject.subscribe((el: any) => {
        const val: string = el?.value ?? '';
        this.debounceRun(() => {
          if (this.searchMode === 'server') {
            this.dispatchEvent(
              new CustomEvent('searchRequest', {
                detail: { query: val, mode: 'server' },
                bubbles: true,
              })
            );
          } else {
            this.setFilterText(val);
          }
        });
      });
    }
    if (searchModeEl && !this.__searchModeSub) {
      this.__searchModeSub = searchModeEl.changeSubject.subscribe((el: any) => {
        const mode: string = el?.selectedOption || el?.value || 'table';
        if (mode === 'table' || mode === 'data' || mode === 'server') {
          this.searchMode = mode as any;
          // When switching modes, re-apply current text input
          const val: string = searchTextEl?.value ?? '';
          this.debounceRun(() => {
            if (this.searchMode === 'server') {
              this.dispatchEvent(new CustomEvent('searchRequest', { detail: { query: val, mode: 'server' }, bubbles: true }));
            } else {
              this.setFilterText(val);
            }
          });
        }
      });
    }
  }

  public async determineColumnWidths() {
    const domtools = await this.domtoolsPromise;
    await domtools.convenience.smartdelay.delayFor(0);
    // Get the table element
    const table = this.shadowRoot.querySelector('table');
    if (!table) return;

    // Get the first row's cells to measure the widths
    const cells = table.rows[0].cells;

    const handleColumnByIndex = async (i: number, waitForRenderArg: boolean = false) => {
      const done = plugins.smartpromise.defer();
      const cell = cells[i];

      // Get computed width
      const width = window.getComputedStyle(cell).width;
      if (cell.textContent.includes('Actions')) {
        const neededWidth =
          this.dataActions.filter((actionArg) => actionArg.type.includes('inRow')).length * 36;
        cell.style.width = `${Math.max(neededWidth, 68)}px`;
      } else {
        cell.style.width = width;
      }
      if (waitForRenderArg) {
        requestAnimationFrame(() => {
          done.resolve();
        });
        await done.promise;
      }
    };

    if (cells[cells.length - 1].textContent.includes('Actions')) {
      await handleColumnByIndex(cells.length - 1, true);
    }

    for (let i = 0; i < cells.length; i++) {
      if (cells[i].textContent.includes('Actions')) {
        continue;
      }
      await handleColumnByIndex(i);
    }
    table.style.tableLayout = 'fixed';
  }

  // compute helpers moved to ./data.ts

  private toggleSort(col: Column<T>) {
    const key = String(col.key);
    if (this.sortKey !== key) {
      this.sortKey = key;
      this.sortDir = 'asc';
    } else {
      if (this.sortDir === 'asc') this.sortDir = 'desc';
      else if (this.sortDir === 'desc') {
        this.sortDir = null;
        this.sortKey = undefined;
      } else this.sortDir = 'asc';
    }
    this.dispatchEvent(
      new CustomEvent('sortChange', {
        detail: { key: this.sortKey, dir: this.sortDir },
        bubbles: true,
      })
    );
    this.requestUpdate();
  }

  private getAriaSort(col: Column<T>): 'none' | 'ascending' | 'descending' {
    if (String(col.key) !== this.sortKey || !this.sortDir) return 'none';
    return this.sortDir === 'asc' ? 'ascending' : 'descending';
  }

  private renderSortIndicator(col: Column<T>) {
    if (String(col.key) !== this.sortKey || !this.sortDir) return html``;
    return html`<span style="margin-left:6px; opacity:0.7;">${this.sortDir === 'asc' ? '▲' : '▼'}</span>`;
  }

  // filtering helpers
  public setFilterText(value: string) {
    const prev = this.filterText;
    this.filterText = value ?? '';
    if (prev !== this.filterText) {
      this.dispatchEvent(
        new CustomEvent('filterChange', {
          detail: { text: this.filterText, columns: { ...this.columnFilters } },
          bubbles: true,
        })
      );
      this.requestUpdate();
    }
  }

  public setColumnFilter(key: string, value: string) {
    this.columnFilters = { ...this.columnFilters, [key]: value };
    this.dispatchEvent(
      new CustomEvent('filterChange', {
        detail: { text: this.filterText, columns: { ...this.columnFilters } },
        bubbles: true,
      })
    );
    this.requestUpdate();
  }

  // selection helpers
  private getRowId(row: T): string {
    if (this.rowKey) {
      if (typeof this.rowKey === 'function') return this.rowKey(row);
      return String((row as any)[this.rowKey]);
    }
    const key = row as any as object;
    if (!this._rowIdMap.has(key)) {
      this._rowIdMap.set(key, String(++this._rowIdCounter));
    }
    return this._rowIdMap.get(key);
  }

  private isRowSelected(row: T): boolean {
    return this.selectedIds.has(this.getRowId(row));
  }

  private toggleRowSelected(row: T) {
    const id = this.getRowId(row);
    if (this.selectionMode === 'single') {
      this.selectedIds.clear();
      this.selectedIds.add(id);
    } else if (this.selectionMode === 'multi') {
      if (this.selectedIds.has(id)) this.selectedIds.delete(id);
      else this.selectedIds.add(id);
    }
    this.emitSelectionChange();
    this.requestUpdate();
  }

  private setRowSelected(row: T, checked: boolean) {
    const id = this.getRowId(row);
    if (this.selectionMode === 'single') {
      this.selectedIds.clear();
      if (checked) this.selectedIds.add(id);
    } else if (this.selectionMode === 'multi') {
      if (checked) this.selectedIds.add(id);
      else this.selectedIds.delete(id);
    }
    this.emitSelectionChange();
    this.requestUpdate();
  }

  private areAllVisibleSelected(): boolean {
    const view: T[] = (this as any)._lastViewData || [];
    if (view.length === 0) return false;
    for (const r of view) {
      if (!this.selectedIds.has(this.getRowId(r))) return false;
    }
    return true;
  }

  private isVisibleSelectionIndeterminate(): boolean {
    const view: T[] = (this as any)._lastViewData || [];
    if (view.length === 0) return false;
    let count = 0;
    for (const r of view) {
      if (this.selectedIds.has(this.getRowId(r))) count++;
    }
    return count > 0 && count < view.length;
  }

  private setSelectVisible(checked: boolean) {
    const view: T[] = (this as any)._lastViewData || [];
    if (checked) {
      for (const r of view) this.selectedIds.add(this.getRowId(r));
    } else {
      for (const r of view) this.selectedIds.delete(this.getRowId(r));
    }
    this.emitSelectionChange();
    this.requestUpdate();
  }

  private emitSelectionChange() {
    const selectedIds = Array.from(this.selectedIds);
    const selectedRows = this.data.filter((r) => this.selectedIds.has(this.getRowId(r)));
    this.dispatchEvent(
      new CustomEvent('selectionChange', {
        detail: { selectedIds, selectedRows },
        bubbles: true,
      })
    );
  }

  getActionsForType(typeArg: ITableAction['type'][0]) {
    const actions: ITableAction[] = [];
    for (const action of this.dataActions) {
      if (!action.type.includes(typeArg)) continue;
      actions.push(action);
    }
    return actions;
  }

  async handleCellEditing(event: Event, itemArg: T, key: string) {
    await this.domtoolsPromise;
    const target = event.target as HTMLElement;
    const originalColor = target.style.color;
    target.style.color = 'transparent';
    const transformedItem = this.displayFunction(itemArg);
    const initialValue = ((transformedItem as any)[key] ?? (itemArg as any)[key] ?? '') as string;
    // Create an input element
    const input = document.createElement('input');
    input.type = 'text';
    input.value = initialValue;

    const blurInput = async (blurArg = true, saveArg = false) => {
      if (blurArg) {
        input.blur();
      }
      if (saveArg) {
        itemArg[key] = input.value as any; // Convert string to T (you might need better type casting depending on your data structure)
        this.changeSubject.next(this);
      }
      input.remove();
      target.style.color = originalColor;
      this.requestUpdate();
    };

    // When the input loses focus or the Enter key is pressed, update the data
    input.addEventListener('blur', () => {
      blurInput(false, false);
    });
    input.addEventListener('keydown', (e: KeyboardEvent) => {
      if (e.key === 'Enter') {
        blurInput(true, true); // This will trigger the blur event handler above
      }
    });

    // Replace the cell's content with the input
    target.appendChild(input);
    input.focus();
  }
}
