import {
  Component,
  AfterContentInit,
  ChangeDetectorRef,
  AfterViewInit,
  OnDestroy,
  ViewChildren,
  QueryList,
  ViewEncapsulation,
  ViewChild
} from '@angular/core'
import { SasStoreService } from '../services/sas-store.service'
import { Subscription } from 'rxjs'

import { Router } from '@angular/router'
import { ActivatedRoute } from '@angular/router'
import { globals } from '../_globals'

import { EventService } from '../services/event.service'
import { HelperService } from '../services/helper.service'
import { HotTableComponent } from '@handsontable/angular-wrapper'
import { SasService } from '../services/sas.service'
import { SASjsConfig } from '@sasjs/adapter'
import { QueryComponent } from '../query/query.component'

import { FilterGroup, FilterQuery } from '../models/FilterQuery'
import { HotTableInterface } from '../models/HotTable.interface'
import { buildExportMenuItem } from '../shared/hot-export/hot-export.util'
import { LoggerService } from '../services/logger.service'
import Handsontable from 'handsontable'
import {
  $DataFormats,
  DSMeta,
  Version
} from '../models/sas/editors-getdata.model'
import { mergeColsRules } from '../shared/dc-validator/utils/mergeColsRules'
import { makeNumberFormatRenderer } from '../editor/utils/renderers.utils'
import { PublicViewtablesServiceResponse } from '../models/sas/public-viewtables.model'
import { PublicViewlibsServiceResponse } from '../models/sas/public-viewlibs.model'
import { PublicRefreshlibinfoServiceResponse } from '../models/sas/public-refreshlibinfo.model'
import { DataFormat } from '../models/sas/common/DateFormat'
import { Libinfo } from '../models/sas/common/Libinfo'
import { LicenceService } from '../services/licence.service'
import { Location } from '@angular/common'

@Component({
  selector: 'app-viewer',
  templateUrl: './viewer.component.html',
  styleUrls: ['./viewer.component.scss'],
  host: {
    class: 'content-container'
  },
  encapsulation: ViewEncapsulation.None,
  standalone: false
})
export class ViewerComponent
  implements AfterContentInit, AfterViewInit, OnDestroy
{
  @ViewChildren('queryFilter')
  queryFilterCompList: QueryList<QueryComponent> = new QueryList()

  @ViewChild('hotInstance', { static: false })
  hotInstanceViewChild!: Handsontable

  public libraries!: Array<any>
  public librariesPaging: boolean = false
  public librariesSearch: string = ''
  public libraryTablesRef: string = ''
  public lib: any
  public librariesTreeExpanded: boolean = false
  public tables: any
  public tableTitle: any
  public libinfo: Libinfo[] | null = null
  public table: any
  public tableuri: string | null = null
  public filter: boolean = false
  public filterCols: any = []
  public nullVariables: boolean = false
  public abortActive: boolean = false
  public queryFilter: any
  public urlFilterPk: number | null = null
  public whereString!: string
  public clauses: any
  public libds!: string
  public libTab!: string
  public queryText: string = ''
  public webQueryText: string = ''
  public submitLoading!: boolean
  public queryErr: boolean = false
  public queryErrMessage!: string
  public libDataset: any
  public tableFlag: boolean = true
  public librariesLoading: boolean = false
  public loadingTableView: boolean = false
  public switchFlag: boolean = false
  public noData: boolean = false
  public noDataReqErr: boolean = false
  public tableDisable: boolean = false
  public actionDisable: boolean = false
  public openDownload: boolean = false
  public webQuery: boolean = false
  public webQueryTab: boolean = false
  public downloadFormat: string = 'CSV'
  public sasjsConfig: SASjsConfig = new SASjsConfig()
  public searchLoading: boolean = false
  public searchNumeric: boolean = false
  @ViewChild(HotTableComponent, { static: false })
  hotTableComponent!: HotTableComponent

  public hotTableSettings: Handsontable.GridSettings = {}

  private updateHotTableSettings(): void {
    this.hotTableSettings = {
      multiColumnSorting: true,
      viewportRowRenderingOffset: 30,
      colHeaders: this.hotTable.colHeaders,
      columns: this.hotTable.columns,
      copyPaste: this.hotTable.copyPaste,
      contextMenu: this.hotTable.contextMenu,
      filters: true,
      dropdownMenu: this.hotTable.dropdownMenu,
      height: this.hotTable.height,
      stretchH: 'all',
      modifyColWidth: this.maxWidthCheker,
      cells: this.hotTable.cells,
      hiddenColumns: { columns: this.hiddenViewColumns, indicators: false },
      maxRows: this.hotTable.maxRows,
      manualColumnResize: true,
      afterGetColHeader: this.hotTable.afterGetColHeader,
      rowHeaders: this.hotTable.rowHeaders,
      rowHeaderWidth: this.hotTable.rowHeaderWidth,
      rowHeights: this.hotTable.rowHeights,
      licenseKey: this.hotTable.licenseKey,
      className: 'htDark',
      theme: 'ht-theme-classic'
    }
  }
  public numberOfRows: number | null = null
  public headerPks: string[] = []
  public $dataFormats: $DataFormats | null = null
  // Physical column indexes to hide in the viewer (from HIDDEN DQ rules)
  public hiddenViewColumns: number[] = []
  public datasetInfo: boolean = false
  public dsmeta: DSMeta[] = []
  public versions: Version[] = []
  public dsNote = ''

  public licenceState = this.licenceService.licenceState
  public Infinity = Infinity

  private ariaObserver: MutationObserver | undefined
  private ariaCheckInterval: any | undefined

  public hotTable: HotTableInterface = {
    data: [],
    colHeaders: [],
    columns: [],
    height: 'calc(100vh - 182px)',
    maxRows: this.licenceState.value.viewer_rows_allowed || Infinity,
    settings: {},
    licenseKey: undefined,
    rowHeaders: (index: number) => {
      return ' '
    },
    afterGetColHeader: (col: number, th: any, headerLevel: number) => {
      // CRITICAL: Prevent "colToProp method cannot be called because this Handsontable instance has been destroyed" error
      // This callback can be triggered even after the instance is destroyed during rapid table switching
      if (!this.hotInstance || this.hotInstance.isDestroyed) {
        // Graceful fallback: apply only dark mode styling when instance is unavailable
        th.classList.add(globals.handsontable.darkTableHeaderClass)
        return
      }

      try {
        const column = this.hotInstance.colToProp(col) as string

        const isPKCol = column && this.headerPks.indexOf(column) > -1
        if (isPKCol) th.classList.add('primaryKeyHeaderStyle')

        // Apply dark mode styling to all headers
        th.classList.add(globals.handsontable.darkTableHeaderClass)
      } catch (error) {
        // Safety net: if colToProp() fails, still apply basic styling
        th.classList.add(globals.handsontable.darkTableHeaderClass)
      }
    },
    rowHeaderWidth: 15,
    rowHeights: 20,
    contextMenu: {
      items: {
        copy_with_column_headers: {},
        copy_column_headers_only: {},
        export_file: buildExportMenuItem(() => this.tableTitle || 'export')
      }
    },
    copyPaste: {
      copyColumnHeaders: true,
      copyColumnHeadersOnly: true
    },
    dropdownMenu: {
      items: {
        make_read_only: {
          name: 'make_read_only'
        },
        alignment: {
          name: 'alignment'
        },
        sp1: {
          name: '---------'
        },
        info: {
          name: 'test info',
          renderer: (
            hot: Handsontable.Core,
            wrapper: HTMLElement,
            row: number,
            col: number,
            prop: string | number,
            itemValue: string
          ) => {
            const elem = document.createElement('span')
            let colInfo: DataFormat | undefined
            let textInfo = 'No info found'

            if (
              this.hotInstance &&
              !this.hotInstance.isDestroyed &&
              !this.isTableSwitching
            ) {
              try {
                const hotSelected: [number, number, number, number][] =
                  this.hotInstance.getSelected() || []
                const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
                const colName = this.hotInstance.colToProp(selectedCol)
                colInfo = this.$dataFormats?.vars[colName]
              } catch (error) {
                // Ignore errors during table switching
                colInfo = undefined
              }

              if (colInfo)
                textInfo = `LABEL: ${colInfo?.label}<br>TYPE: ${colInfo?.type}<br>LENGTH: ${colInfo?.length}<br>FORMAT: ${colInfo?.format}`
            }

            elem.innerHTML = textInfo

            return elem
          }
        }
      }
    }
  }

  private _query!: Subscription

  private hotInstance: Handsontable | null = null
  public hotInstanceClickListener: boolean = false

  // Race condition prevention for rapid table switching
  private isTableSwitching: boolean = false
  private switchingTimeout: any = null

  // Prevents duplicate setupHot() calls within short time windows
  private lastSetupTime: number = 0

  public viewboxOpen: boolean = false

  constructor(
    private licenceService: LicenceService,
    private sasStoreService: SasStoreService,
    private sasService: SasService,
    private router: Router,
    private route: ActivatedRoute,
    private eventService: EventService,
    private loggerService: LoggerService,
    private helperService: HelperService,
    private location: Location,
    private cdf: ChangeDetectorRef
  ) {
    this.sasjsConfig = this.sasService.getSasjsConfig()
  }

  ngOnInit() {
    if (
      globals.viewer.currentSelection !== '' &&
      Object.keys(this.route.snapshot.params).length === 0
    ) {
      this.router.navigate([globals.viewer.currentSelection], {
        queryParamsHandling: 'preserve'
      })
    }
    if (this.route.snapshot.params['libMem'] !== undefined) {
      globals.viewer.currentSelection = 'view/data'
    }

    this.licenceService.hot_license_key.subscribe(
      (hot_license_key: string | undefined) => {
        this.hotTable.licenseKey = hot_license_key
        this.updateHotTableSettings() // Update settings when license key changes
      }
    )
  }

  /**
   * Open viewboxes modal
   */
  public newViewbox() {
    this.viewboxOpen = true
  }

  /**
   * Resetting filter variables
   */
  public resetFilter() {
    if (this.queryFilterCompList.first) {
      this.queryFilterCompList.first.resetFilter()
    }
  }

  /**
   * Searching table against particular string, data is comming from backend.
   * There is also a toggle that will search for a numeric values
   *
   * @param inputElement input from which search string is captured
   */
  public async searchTable(inputElement: any) {
    this.searchLoading = true

    let searchValue = inputElement.value

    let libDataset = this.lib + '.' + this.table
    let filter_pk = parseInt(this.route.snapshot.params['filterId']) || 0
    await this.sasStoreService
      .viewDataSearch(searchValue, this.searchNumeric, libDataset, filter_pk)
      .then((res: any) => {
        if (!res.sasparams && !res.viewData) {
          this.searchLoading = false
          return
        }

        this.hotTable.data = res.viewdata
        this.$dataFormats = res.$viewdata
        this.dsmeta = res.dsmeta
        this.versions = res.versions || []
        this.setDSNote()
        this.numberOfRows = res.sasparams[0].NOBS
        this.queryText = res.sasparams[0].FILTER_TEXT
        this.headerPks = res.sasparams[0].PK_FIELDS.split(' ')

        if (this.hotTable.data.length === 0) {
          this.noData = true
        } else {
          this.noData = false
          this.tableFlag = false

          this.setupHot()
        }
      })
      .catch((err: any) => {
        this.loggerService.error(err)
      })

    this.searchLoading = false
  }

  /**
   * Re sending request to backend and re-setting data in the HOT
   */
  public reloadTableData() {
    this.viewData(this.urlFilterPk || 0)
  }

  /**
   * By default, SAS will send cached library information.
   * This function calls SAS to update the backend database.
   * It will also show the latest info on the page.
   */
  public async reloadLibInfo() {
    this.libinfo = null

    this.sasStoreService.refreshLibInfo(this.lib).then(
      async (res: PublicRefreshlibinfoServiceResponse) => {
        this.libinfo = res.libinfo
        globals.viewer.libinfo = this.libinfo

        const library = this.libraries.find((x) => x.LIBRARYREF === this.lib)

        if (library) library.libinfo = this.libinfo
      },
      (err: any) => {
        this.loggerService.error(err)

        if (this.libinfo === null) this.libinfo = []
      }
    )
  }

  /**
   * FIXME: Should be removed, not used
   */
  public filterFn(input: string) {
    let libraries = this.libraries
    this.libraries = libraries.filter(
      (item) =>
        item.LIBRARYNAME.toLowerCase().indexOf(input.toLocaleLowerCase()) !== -1
    )
  }

  /**
   * Downloads file from backend, against `getrawdata` service, link is created and open in new tab
   */
  public downloadData() {
    let storage = this.sasjsConfig.serverUrl
    let metaData = this.sasjsConfig.appLoc
    const path = this.sasService.getExecutionPath()
    let type = '&type=' + this.downloadFormat
    let table = '&table=' + this.tableTitle
    let contextname =
      this.sasjsConfig.serverType === 'SASVIYA'
        ? `&_contextname=${this.sasjsConfig.contextName}`
        : ''
    let filter_pk: number

    if (typeof this.route.snapshot.params['filterId'] === 'undefined') {
      filter_pk = 0
    } else {
      filter_pk = parseInt(this.route.snapshot.params['filterId'])
    }

    let filter = '&filter=' + filter_pk
    let downUrl =
      storage +
      path +
      '/?_program=' +
      metaData +
      '/services/public/getrawdata' +
      type +
      table +
      contextname +
      filter

    window.open(downUrl)
    this.openDownload = false
  }

  /**
   * Downloads file from backend, against `getddl` service, link is created and open in new tab
   */
  public downloadDDL() {
    let libref = this.lib
    let ds = this.table
    let flavour = this.downloadFormat.replace('_DDL', '')
    let storage = this.sasjsConfig.serverUrl
    let metaData = this.sasjsConfig.appLoc
    const path = this.sasService.getExecutionPath()
    let contextname =
      this.sasjsConfig.serverType === 'SASVIYA'
        ? `&_contextname=${this.sasjsConfig.contextName}`
        : ''

    let params = `&ds=${ds}&libref=${libref}&flavour=${flavour}${contextname}`

    let downUrl =
      storage +
      path +
      '/?_program=' +
      metaData +
      '/services/public/getddl' +
      params

    window.open(downUrl)
    this.openDownload = false
  }

  /**
   * When clicked on textarea in the Web Query Modal, this function will
   * select all text inside.
   * @param evt textarea which contains the web query text
   */
  public onCliCommandFocus(evt: any): void {
    evt.preventDefault()
    evt.target.select()
  }

  /**
   * Navigate to the edit page of a viewing table
   */
  public editTable() {
    this.router.navigateByUrl('/editor/' + this.libTab)
  }

  /**
   * Used to show/hide the edit table button
   * @returns Wheter currently viewed table is edtiable
   */
  public tableEditExists() {
    let editTables: any = {}
    editTables = globals.editor.libsAndTables
    let currentTable = this.libTab.split('.')[1]
    let currentLibrary = this.libTab.split('.')[0]

    // If this line is undefined, that means startupservice failed or similar.
    if (!editTables[currentLibrary]) return false

    return editTables[currentLibrary].includes(currentTable)
  }

  /**
   * Navigate to the lineage of a viewing table
   */
  public goToLineage() {
    let routeUri = this.tableuri!.split('\\')[1]
    let lineageUrl = `/view/lineage/${routeUri}/REVERSE`
    this.router.navigateByUrl(lineageUrl)
  }

  /**
   * Displays web query modal
   */
  public showWebQuery() {
    this.webQuery = true
    let filter_pk: number
    if (typeof this.route.snapshot.params['filterId'] === 'undefined') {
      filter_pk = 0
    } else {
      filter_pk = parseInt(this.route.snapshot.params['filterId'])
    }

    let port = window.location.port.length > 0 ? ':' + window.location.port : ''

    const sasPath = this.sasService.getExecutionPath()

    let filter = '&filter=' + filter_pk

    let downUrl = `${window.location.protocol}//${
      window.location.hostname
    }${port}/${sasPath}/?_program=${
      this.sasjsConfig.appLoc
    }/services/public/getrawdata&type=WEB${
      this.webQueryTab ? 'TAB' : 'CSV'
    }&table=${this.tableTitle}${filter}`

    this.webQueryText = downUrl.replace(/ /gim, '%20')
  }

  public copyToClip() {
    navigator.clipboard.writeText(this.webQueryText)
  }

  public goToViewer() {
    this.router.navigateByUrl('/view/data')
  }

  public showTableSelect() {
    this.tableFlag = !this.tableFlag
  }

  public checkExpand(event: any) {}

  public collapseLibraryItems(libraries: any, libraryToSkip: any) {
    libraries.forEach((library: any) => {
      if (library.LIBRARYREF !== libraryToSkip.LIBRARYREF) {
        library['expanded'] = false
      }
    })
  }

  public loadMoreLibraries() {
    if (!this.librariesPaging) {
      this.librariesPaging = true

      this.helperService.displayLibraries(this.libraries, true)

      this.librariesPaging = false
    }
  }

  public treeNodeClicked(event: any, library: any) {
    if (event.target.title === 'Collapse') {
      this.collapseLibraryItems(this.libraries, library)
    }
  }

  public libraryExpandedChange(expanded: any, library: any) {
    if (expanded) {
      this.collapseLibraryItems(this.libraries, library)
    }
  }

  public async libraryOnClick(lib: string, library: any, expandOnly?: boolean) {
    if (!library['tables']) {
      await this.selectTable(lib, false, library)
    } else {
      if (!!expandOnly) {
        library['expanded'] = true
      } else {
        library['expanded'] = !library['expanded']
      }
    }

    if (library['expanded']) {
      this.cdf.detectChanges()
      let libTreeSearchInput: HTMLElement | null = document.querySelector(
        `#search_${library.LIBRARYREF}`
      )

      if (libTreeSearchInput) libTreeSearchInput.focus()

      if (library && library.libinfo) this.libinfo = library.libinfo

      if (this.lib && this.table && !expandOnly) {
        this.router
          .navigate(['/view/data'], {
            skipLocationChange: true,
            queryParamsHandling: 'preserve'
          })
          .then(() => {
            this.router.navigate(['/view/data/' + this.lib], {
              queryParamsHandling: 'preserve'
            })
          })
      } else if (this.lib && !this.table) {
        this.location.replaceState('/view/data/' + this.lib)
      }
    }

    this.collapseLibraryItems(this.libraries, library)
  }

  public onTableClick(libTable: any, library: any) {
    // OPTIMIZATION: Prevent race conditions and destroyed instance errors during rapid table switching
    if (this.isTableSwitching) {
      return
    }

    // Clear any existing timeout to prevent stale operations
    if (this.switchingTimeout) {
      clearTimeout(this.switchingTimeout)
    }

    // PERFORMANCE: Debounce table switches to prevent rapid successive calls
    // This ensures only the final table selection is processed
    this.switchingTimeout = setTimeout(() => {
      this.lib = library.LIBRARYREF
      this.table = libTable
      this.selectLibTable(libTable)
      this.viewData(0)
    }, 50) // 50ms debounce - fast enough for good UX, slow enough to prevent issues
  }

  public async selectTable(lib: string, initial?: boolean, library?: any) {
    library['loadingTables'] = true

    this.table = false
    this.tableDisable = true
    if (lib !== 'Please select library') {
      if (globals.viewer.tablesSet && initial) {
        this.abortActive = false
        this.tableDisable = false
        this.tables = globals.viewer.tables
        this.libinfo = globals.viewer.libinfo
      } else {
        this.libinfo = null

        await this.sasStoreService
          .viewTables(lib)
          .then((res: PublicViewtablesServiceResponse) => {
            this.abortActive = false
            this.tableDisable = false

            let tables = res.mptables.map(function (item: any) {
              return item.MEMNAME
            })

            this.libinfo = res.libinfo || []
            this.tables = tables

            if (library) {
              library['tables'] = tables
              library['libinfo'] = this.libinfo
            }

            globals.viewer.libraries = this.libraries
            globals.viewer.library = this.lib
            globals.viewer.tables = this.tables
            globals.viewer.libinfo = this.libinfo
            globals.viewer.tablesSet = true
          })
          .catch((err: any) => {
            this.loggerService.error(err)

            this.abortActive = true
            this.noData = true
            this.tableTitle = ''
            this.tableDisable = true
          })
      }
    } else {
      this.tableDisable = true
    }
    // this.getTableState();

    globals.viewer.library = this.lib
    if (!initial) {
      this.clearGlobalsFilter()
    }

    library['loadingTables'] = false
    library['expanded']
      ? (library['expanded'] = false)
      : (library['expanded'] = true)
  }

  public selectLibTable(ev: string, initial?: boolean) {
    if (ev !== 'Please select table') {
      this.actionDisable = false
      this.libTab = this.lib + '.' + this.table
    } else {
      this.actionDisable = true
    }
    globals.viewer.table = ev
    if (!initial) {
      this.clearGlobalsFilter()
    }

    this.loggerService.log(this.libTab)
  }

  private clearGlobalsFilter() {
    globals.viewer.filter.libds = ''
    globals.viewer.filter.whereClause = ''
    globals.viewer.filter.groupLogic = ''
    globals.viewer.filter.clauses = []
    globals.viewer.filter.cols = []
    globals.viewer.filter.vals = []
  }

  public libTabActive(libraryRef: string, table: string) {
    let currentLibTab = libraryRef + '.' + table

    if (!this.libTab) {
      return false
    }

    return currentLibTab === this.libTab
  }

  public treeOnFilter(array: any, arrToFilter: string) {
    this.helperService.treeOnFilter(array, arrToFilter)
  }

  public libraryOnFilter() {
    this.helperService.libraryOnFilter(
      this.libraries,
      this.librariesSearch,
      'LIBRARYNAME'
    )

    globals.viewer.librariesSearch = this.librariesSearch
  }

  public libraryResetFilter() {
    this.helperService.resetArrayFilter(this.libraries)
  }

  public async viewData(filter_pk: number) {
    // CRITICAL: Set switching flag to prevent concurrent operations and race conditions
    // This prevents callbacks from accessing destroyed instances during table switching
    this.isTableSwitching = true

    this.loadingTableView = true

    let libDataset: any
    if (typeof this.libDataset === 'undefined') {
      libDataset = this.lib + '.' + this.table
    } else if (
      typeof this.lib === 'undefined' &&
      typeof this.table === 'undefined'
    ) {
      let ds = []
      ds = this.libDataset.split('.')

      await this.sasStoreService
        .viewLibs()
        .then((res: any) => {
          this.libraries = res.saslibs
        })
        .catch((err: any) => {
          this.loggerService.error(err)
        })

      this.lib = ds[0]

      if (globals.viewer.startupSet) {
        this.tables = globals.viewer.tables
      } else {
        let library = this.libraries.find(
          (lib) => lib.LIBRARYREF.toLowerCase() === this.lib.toLowerCase()
        )

        await this.sasStoreService
          .viewTables(this.lib)
          .then((response: any) => {
            this.tables = response.mptables.map(function (item: any) {
              return item.MEMNAME
            })

            library.tables = this.tables
            globals.viewer.libraries = this.libraries
            globals.viewer.tables = this.tables
            globals.viewer.startupSet = true
          })
          .catch((err: any) => {
            this.loggerService.error(err)
          })
      }

      this.table = ds[1]

      this.tableFlag = false

      libDataset = this.libDataset
      this.libTab = libDataset
    } else {
      await this.sasStoreService
        .viewLibs()
        .then((res: any) => {
          this.libraries = res.saslibs
        })
        .catch((err: any) => {
          this.loggerService.error(err)
        })

      if (typeof this.table !== 'undefined') {
        if (globals.viewer.startupSet) {
          this.tables = globals.viewer.tables
        } else {
          await this.sasStoreService
            .viewTables(this.lib)
            .then((response: PublicViewtablesServiceResponse) => {
              this.tables = response.mptables.map(function (item: any) {
                return item.MEMNAME
              })
            })
            .catch((err: any) => {
              this.loggerService.error(err)

              this.router.navigate(['/view/data'], {
                queryParamsHandling: 'preserve'
              })
            })
        }

        this.tableFlag = false

        libDataset = this.lib + '.' + this.table
        this.tableTitle = libDataset
        this.libTab = libDataset
      }
    }

    if (this.router.url.split('/').length > 3 && libDataset) {
      await this.sasStoreService
        .viewData(libDataset, filter_pk)
        .then((res: any) => {
          if (
            res.query.length > 0 &&
            globals.rootParam === 'view' &&
            globals.viewer.filter.clauses.length === 0
          ) {
            globals.viewer.filter.query = this.helperService.deepClone(
              res.query
            )
            globals.viewer.filter.libds = this.route.snapshot.params['libMem']
            this.sasStoreService.initializeGlobalFilterClause(
              'viewer',
              res.cols
            )
          }
          this.abortActive = false
          this.filterCols = res.cols

          // This will merge $formats types with `Cols` object.
          mergeColsRules(this.filterCols, [], res.$viewdata)

          this.numberOfRows = res.sasparams[0].NOBS
          this.headerPks = res.sasparams[0].PK_FIELDS.split(' ')

          if (this.sasjsConfig.serverType === 'SAS9') {
            let tableuri = res.sasparams[0].TABLEURI

            if (tableuri) {
              if (tableuri.length > 0) {
                this.tableuri = tableuri
              }
            }
          }

          this.hotTable.data = res.viewdata
          this.$dataFormats = res.$viewdata
          this.dsmeta = res.dsmeta
          this.versions = res.versions || []
          this.setDSNote()
          this.queryText = res.sasparams[0].FILTER_TEXT

          // Initialize columns only if we have data
          if (res.viewdata && res.viewdata.length > 0) {
            let columns: any[] = []
            let colArr = []

            for (let key in res.viewdata[0]) {
              if (key) {
                colArr.push(key)
              }
            }

            // NUMBER_FORMAT (display) and HIDDEN (visibility) are the DQ rules
            // that make sense in the read-only viewer; READONLY/ROUND are
            // editor-only. Build lookups for both from the response.
            const numberFormats: { [col: string]: string } = {}
            const hiddenCols = new Set<string>()
            if (Array.isArray(res.dqrules)) {
              for (const rule of res.dqrules) {
                if (rule.RULE_TYPE === 'NUMBER_FORMAT') {
                  numberFormats[rule.BASE_COL] = rule.RULE_VALUE
                }
                if (rule.RULE_TYPE === 'HIDDEN') {
                  hiddenCols.add(rule.BASE_COL)
                }
              }
            }

            const hiddenColumnIndexes: number[] = []
            for (let index = 0; index < colArr.length; index++) {
              const col = colArr[index]
              const colDef: any = { data: col }
              if (numberFormats[col]) {
                colDef.renderer = makeNumberFormatRenderer(numberFormats[col])
              }
              if (hiddenCols.has(col)) {
                hiddenColumnIndexes.push(index)
              }
              columns.push(colDef)
            }

            this.hotTable.colHeaders = colArr
            this.hotTable.columns = columns
            this.hiddenViewColumns = hiddenColumnIndexes
          } else {
            // Set empty arrays if no data
            this.hotTable.colHeaders = []
            this.hotTable.columns = []
            this.hiddenViewColumns = []
          }

          // Set cells function
          this.hotTable.cells = () => {
            return { readOnly: true }
          }

          // Update hot table settings after data is loaded
          this.updateHotTableSettings()

          this.tableFlag = false
          let ds = []
          ds = libDataset.split('.')
          this.lib = ds[0]
          this.table = ds[1]

          if (this.hotTable.data.length === 0) {
            this.noData = true
            this.tableFlag = true
          } else {
            this.noData = false
            this.tableFlag = false
          }

          this.noDataReqErr = false
        })
        .catch((err: any) => {
          this.loggerService.error(err)

          this.abortActive = true
          this.noDataReqErr = true
        })
    }

    // If we move away from the viewer page, we don't renavigate to table view
    if (this.router.url.includes('/data') && libDataset) {
      if (filter_pk !== 0) {
        this.router.navigate(['/view/data/' + libDataset + '/' + filter_pk], {
          queryParamsHandling: 'preserve'
        })
      } else {
        this.router.navigate(['/view/data/' + libDataset], {
          queryParamsHandling: 'preserve'
        })
      }
    }

    let currentTable = this.table
    let libraryObject = this.libraries.find(
      (lib) => lib.LIBRARYREF.toLowerCase() === this.lib.toLowerCase()
    )

    if (libraryObject) {
      if (globals.viewer.libraries.length > 0) {
        this.libraries = globals.viewer.libraries
        this.librariesSearch = globals.viewer.librariesSearch
        this.libraryOnClick(this.lib, libraryObject, true)
      } else {
        this.libraryOnClick(this.lib, libraryObject, true)
      }
    } else {
      this.libinfo = []
    }

    this.table = currentTable ? currentTable : this.table

    if (this.libraries) {
      this.helperService.displayLibraries(this.libraries)
    }

    if (this.router.url.includes('/data')) {
      this.eventService.closeSidebar()
    }

    this.loadingTableView = false

    // Setup Handsontable after async operations complete
    // Original issue: setupHot() called before API responses populated headerPks array
    // Solution: Delay ensures both API paths (lines 328 & 886) have chance to set headerPks
    setTimeout(() => {
      if (!this.noData && !this.noDataReqErr && libDataset) {
        this.setupHot()
      }
    }, 50) // Optimized from 100ms - fast enough for API completion, slow enough to prevent race conditions

    // RACE CONDITION PREVENTION: Reset switching flag after setup completion
    // This allows new table switches after current operation finishes
    setTimeout(() => {
      this.isTableSwitching = false
    }, 300) // Optimized from 700ms to match reduced setup times

    // Fix ARIA accessibility issues after data loading
    setTimeout(() => {
      this.fixAriaAccessibility()
    }, 500)

    /**
     * This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area.
     * Without it, hot table does not steal focus, so it doesn't close the nav.
     */
    if (!this.hotInstanceClickListener) {
      setTimeout(() => {
        let hotInstanceEl = document.getElementById('hotInstance')

        if (hotInstanceEl) {
          hotInstanceEl.addEventListener('mousedown', (event) => {
            setTimeout(() => {
              let menuDebugItem: any =
                document.querySelector('.debug-switch-item') || undefined
              if (menuDebugItem) menuDebugItem.click()
            }, 100)
          })
          this.hotInstanceClickListener = true
        }
      }, 1000)
    }
  }

  public maxWidthCheker(width: any, col: any) {
    if (width > 200) return 200
    else return width
  }

  public openQb() {
    this.filter = true
    this.cdf.detectChanges()

    let libDataset = this.lib + '.' + this.table
    this.sasStoreService.setQueryVariables(libDataset, this.filterCols)
  }

  public async sendClause() {
    this.submitLoading = true
    let nullVariableArr = []
    let emptyVariablesArr = []
    // to check number of empty clauses
    if (typeof this.clauses === 'undefined') {
      this.nullVariables = true
      this.submitLoading = false
      return
    } else {
      let query = this.clauses.queryObj

      if (query[0].elements.length < 1) {
        // Clear cached filtering data
        if (globals.rootParam === 'view') {
          globals.viewer.filter.clauses = []
          globals.viewer.filter.query = []
          globals.viewer.filter.groupLogic = ''
        }

        this.router.navigate(['/view/data/' + this.libds], {
          queryParamsHandling: 'preserve'
        })
        return
      }

      for (let index = 0; index < query.length; index++) {
        const el = query[index].elements
        nullVariableArr = el.filter(function (item: any) {
          return item.variable === null
        })
        if (nullVariableArr.length) {
          emptyVariablesArr.push(el)
        }
      }
    }

    if (emptyVariablesArr.length) {
      // not allow to submit if some of clauses are empty
      this.nullVariables = true
      this.submitLoading = false
      return
    } else {
      const filterQuery: FilterQuery = {
        groupLogic: this.clauses.groupLogic,
        filterGroups: []
      }
      this.clauses.queryObj.forEach((group: any) => {
        const filterGroup: FilterGroup = {
          filterClauses: []
        }
        group.elements.forEach((clause: any) => {
          filterGroup.filterClauses.push(this.helperService.deepClone(clause))
        })
        filterGroup.clauseLogic = group.clauseLogic
        filterQuery.filterGroups.push(this.helperService.deepClone(filterGroup))
      })

      const filterQueryClauseTable =
        this.sasStoreService.createFilterQueryTable(filterQuery)
      await this.sasStoreService
        .saveQuery(this.libds, filterQueryClauseTable)
        .then((res: any) => {
          this.queryText = res.result[0].FILTER_TEXT
          let id = res.result[0].FILTER_RK
          this.router.navigate(['/view/data/' + this.libds + '/' + id], {
            queryParamsHandling: 'preserve'
          })

          this.viewData(id)

          this.filter = false
        })
        .catch((err: any) => {
          this.loggerService.error(err)
        })

      this.submitLoading = false
    }
  }

  public removeQuery() {
    this.sasStoreService.removeClause()
  }

  public datasetInfoModalRowClicked(value: Version | DSMeta) {
    if ((<Version>value).LOAD_REF !== undefined) {
      // Type is Version
      const row = value as Version
      const url = `/stage/${row.LOAD_REF}`

      this.router.navigate([url])
    }
  }

  private setDSNote() {
    const notes = this.dsmeta.find((item) => item.NAME === 'NOTES')
    const longDesc = this.dsmeta.find((item) => item.NAME === 'DD_LONGDESC')
    const shortDesc = this.dsmeta.find((item) => item.NAME === 'DD_SHORTDESC')

    if (notes && notes.VALUE) {
      this.dsNote = notes.VALUE
    } else if (longDesc && longDesc.VALUE) {
      this.dsNote = longDesc.VALUE
    } else if (shortDesc && shortDesc.VALUE) {
      this.dsNote = shortDesc.VALUE
    } else {
      this.dsNote = ''
    }
  }

  /**
   * CRITICAL CLEANUP (workaround needed for HOT version 16 and above): Safely destroys Handsontable instances
   *
   * Purpose: Prevents "instance destroyed" errors and memory leaks during table switching
   *
   * Called from:
   * - ngOnDestroy() - component cleanup
   *
   * Safety features:
   * - Checks if instance exists and is not already destroyed
   * - Try-catch prevents errors during destruction
   * - Sets instance to null to prevent stale references
   */
  private cleanupHotInstance() {
    if (this.hotInstance && !this.hotInstance.isDestroyed) {
      try {
        this.hotInstance.destroy()
      } catch (error) {
        console.warn('Error destroying Handsontable instance:', error)
      }
    }
    this.hotInstance = null
    this.hooksAttached = false
  }

  /**
   * PERFORMANCE: Configures Handsontable with enhanced error handling (workaround needed for HOT version 16 and above)
   *
   * 1. Duplicate call prevention (500ms window)
   * 2. Multiple validation checks to prevent race conditions
   * 3. Forced render for immediate primary key styling
   *
   * Instance lifecycle is managed by Angular's hot-table component via [data] and [settings] bindings.
   * This method only applies additional config that can't go through bindings (hooks, PK styling).
   */
  private setupHot() {
    const now = Date.now()
    if (now - this.lastSetupTime < 500) {
      return
    }
    this.lastSetupTime = now

    // VALIDATION: Don't setup if we're currently switching tables or still loading
    if (this.loadingTableView || !this.libDataset) {
      return
    }

    if (!this.hotInstance || this.hotInstance.isDestroyed) {
      this.hotInstance = this.hotTableComponent?.hotInstance
    }

    if (this.hotInstance && !this.hotInstance.isDestroyed) {
      this.configureHotInstance()
      return
    }

    // Instance not ready yet — Angular may still be creating the component
    setTimeout(() => {
      if (this.isTableSwitching || this.loadingTableView || !this.libDataset) {
        return
      }

      this.hotInstance = this.hotTableComponent?.hotInstance
      this.configureHotInstance()
    }, 250)
  }

  private hooksAttached = false

  /**
   * Applies settings that can't go through Angular [settings] binding:
   * - Primary key column header styling
   * - Column width cap
   * - ARIA accessibility hooks (attached once per instance)
   */
  private configureHotInstance() {
    if (!this.hotInstance || this.hotInstance.isDestroyed) return

    this.hotInstance.updateSettings({
      height: this.hotTable.height,
      modifyColWidth: (width: any, col: any) => {
        if (width > 500) return 500
        else return width
      },
      afterGetColHeader: (col: number, th: any) => {
        // CRITICAL: Same safety checks as initial callback to prevent destroyed instance errors
        if (!this.hotInstance || this.hotInstance.isDestroyed) {
          th.classList.add(globals.handsontable.darkTableHeaderClass)
          return
        }

        try {
          const column = this.hotInstance.colToProp(col) as string

          // PRIMARY KEY STYLING: Apply special styling to PK columns (populated from API response)
          const isPKCol = column && this.headerPks.indexOf(column) > -1
          if (isPKCol) th.classList.add('primaryKeyHeaderStyle')

          // DARK MODE: Apply to all headers
          th.classList.add(globals.handsontable.darkTableHeaderClass)
        } catch (error) {
          // SAFETY NET: Ensure basic styling is always applied
          th.classList.add(globals.handsontable.darkTableHeaderClass)
        }
      }
    })

    // Add hooks for accessibility fixes
    // Hooks are attached once per instance to avoid accumulating duplicate listeners
    if (!this.hooksAttached) {
      this.hotInstance.addHook('afterRender', () => {
        // Fix ARIA accessibility issues after each render
        this.fixAriaAccessibility()
      })

      this.hotInstance.addHook('afterChange', () => {
        // Fix ARIA accessibility issues after any data change
        setTimeout(() => {
          this.fixAriaAccessibility()
        }, 50)
      })

      this.hooksAttached = true
    }

    // Force immediate render to apply primary key styling
    // Without this, styling would wait for ~2 seconds to be applied
    // (workaround needed for HOT version 16 and above)
    this.hotInstance.render()
  }

  async loadWithParameters() {
    this.switchFlag = true
    this.tableTitle = this.route.snapshot.params['libMem'] || 0
    let id = this.route.snapshot.params['filterId'] || '0'

    this.urlFilterPk = parseInt(id)

    let libds = this.route.snapshot.params['libMem']
    this.libDataset = libds

    if (!libds.includes('.')) this.lib = libds

    await this.viewData(this.urlFilterPk)

    if (this.noData) {
      setTimeout(() => {
        this.tableFlag = true
      }, 1200)
    } else {
      setTimeout(() => {
        this.tableFlag = false
      }, 1200)
    }
  }

  async loadWithoutParameters() {
    this.switchFlag = false
    this.librariesLoading = true

    if (globals.viewer.startupSet) {
      setTimeout(() => {
        this.libraries = globals.viewer.libraries
        this.librariesSearch = globals.viewer.librariesSearch
        this.lib = globals.viewer.library
        this.librariesTreeExpanded = true
        this.librariesLoading = false

        this.helperService.displayLibraries(this.libraries)
      }, 100)
    } else {
      this.noDataReqErr = false

      await this.sasStoreService
        .viewLibs()
        .then((res: PublicViewlibsServiceResponse) => {
          this.libraries = res.saslibs
          globals.viewer.libraries = this.libraries
          globals.viewer.startupSet = true

          this.librariesLoading = false

          this.helperService.displayLibraries(this.libraries)
        })
        .catch((err: any) => {
          this.loggerService.error(err)

          this.librariesLoading = false
          this.noDataReqErr = true
        })
    }
  }

  ngAfterViewInit() {
    // Fix ARIA accessibility issues after table initialization
    setTimeout(() => {
      this.fixAriaAccessibility()
    }, 1000)
  }

  ngOnDestroy() {
    // Proper component destruction to prevent memory leaks and errors

    // Prevent any new operations during cleanup
    this.isTableSwitching = true

    // Clear any pending debounced table switches
    if (this.switchingTimeout) {
      clearTimeout(this.switchingTimeout)
      this.switchingTimeout = null
    }

    // Safely destroy Handsontable instance
    this.cleanupHotInstance()

    // Clean up ARIA accessibility observers
    if (this.ariaObserver) {
      this.ariaObserver.disconnect()
      this.ariaObserver = undefined
    }

    // Clear ARIA check intervals
    if (this.ariaCheckInterval) {
      clearInterval(this.ariaCheckInterval)
      this.ariaCheckInterval = undefined
    }
  }

  /**
   * Fixes ARIA accessibility issues in the Handsontable component
   * This addresses the accessibility report issues with treegrid and presentation roles
   */
  private fixAriaAccessibility() {
    // Use a more aggressive approach to find and fix all ARIA issues
    const fixAriaIssues = () => {
      // Specifically target Handsontable wrapper elements that are causing issues
      const hotWrappers = document.querySelectorAll(
        '.ht-wrapper, .wtHolder, [id^="ht_"]'
      )

      hotWrappers.forEach((wrapper) => {
        // Remove problematic ARIA attributes from Handsontable wrappers
        wrapper.removeAttribute('role')
        wrapper.removeAttribute('aria-rowcount')
        wrapper.removeAttribute('aria-colcount')
        wrapper.removeAttribute('aria-multiselectable')
      })

      // Find all elements with problematic ARIA roles in the entire document
      const allTreegridElements = document.querySelectorAll('[role="treegrid"]')
      const allPresentationElements = document.querySelectorAll(
        '[role="presentation"]'
      )

      // Fix treegrid role issues - remove them completely as they're causing problems
      allTreegridElements.forEach((element) => {
        element.removeAttribute('role')
        element.removeAttribute('aria-rowcount')
        element.removeAttribute('aria-colcount')
        element.removeAttribute('aria-multiselectable')
      })

      // Fix presentation role issues - remove them if they contain interactive elements
      allPresentationElements.forEach((element) => {
        const hasInteractiveChildren =
          element.querySelectorAll(
            'button, input, select, textarea, [tabindex], [onclick], [contenteditable]'
          ).length > 0
        if (hasInteractiveChildren) {
          element.removeAttribute('role')
        }
      })

      // Also fix any elements with aria-rowcount="-1" which is problematic
      const negativeRowCountElements = document.querySelectorAll(
        '[aria-rowcount="-1"]'
      )
      negativeRowCountElements.forEach((element) => {
        element.removeAttribute('aria-rowcount')
      })

      // Ensure proper table structure
      const tableElements = document.querySelectorAll('table')
      tableElements.forEach((table) => {
        if (!table.getAttribute('role')) {
          table.setAttribute('role', 'table')
        }

        // Ensure table headers have proper scope
        const headerCells = table.querySelectorAll('th')
        headerCells.forEach((th) => {
          if (!th.getAttribute('scope')) {
            th.setAttribute('scope', 'col')
          }
        })
      })

      // Add proper ARIA labels to interactive elements
      const interactiveElements = document.querySelectorAll(
        'button, input, select, textarea, [contenteditable]'
      )
      interactiveElements.forEach((element) => {
        if (
          !element.getAttribute('aria-label') &&
          !element.getAttribute('aria-labelledby')
        ) {
          const textContent = element.textContent?.trim()
          if (textContent) {
            element.setAttribute('aria-label', textContent)
          }
        }
      })
    }

    // Run the fix immediately
    fixAriaIssues()

    // Run it again after a short delay to catch any dynamically created elements
    setTimeout(fixAriaIssues, 100)
    setTimeout(fixAriaIssues, 500)
    setTimeout(fixAriaIssues, 1000)
    setTimeout(fixAriaIssues, 2000)

    // Set up a periodic check to ensure accessibility fixes are maintained
    if (!this.ariaCheckInterval) {
      this.ariaCheckInterval = setInterval(fixAriaIssues, 3000)
    }

    // Set up a MutationObserver to continuously monitor for new problematic elements
    if (!this.ariaObserver) {
      this.ariaObserver = new MutationObserver((mutations) => {
        let shouldFix = false
        mutations.forEach((mutation) => {
          if (
            mutation.type === 'attributes' &&
            (mutation.attributeName === 'role' ||
              mutation.attributeName === 'aria-rowcount')
          ) {
            shouldFix = true
          }
          if (mutation.type === 'childList') {
            mutation.addedNodes.forEach((node) => {
              if (node.nodeType === Node.ELEMENT_NODE) {
                const element = node as Element
                if (
                  element.hasAttribute('role') ||
                  element.hasAttribute('aria-rowcount')
                ) {
                  shouldFix = true
                }
              }
            })
          }
        })

        if (shouldFix) {
          setTimeout(fixAriaIssues, 50)
        }
      })

      // Start observing the entire document for changes
      this.ariaObserver.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: [
          'role',
          'aria-rowcount',
          'aria-colcount',
          'aria-multiselectable'
        ]
      })
    }
  }

  async ngAfterContentInit() {
    if (this.hotTable.data.length > 0) {
      this.tableFlag = true
    }
    this._query = this.sasStoreService.query.subscribe((query: any) => {
      this.whereString = query.string
      this.clauses = query.obj
      this.libds = query.libds
    })

    if (typeof this.route.snapshot.params['libMem'] !== 'undefined') {
      this.loadWithParameters()
    } else {
      this.loadWithoutParameters()
    }
  }
}
