/**
 * TreeView - Accessible expandable tree component with lazy loading and search
 * @countermeasure/web-components/components/tree-view
 *
 * Implements ARIA Tree pattern with full keyboard navigation.
 * WCAG 2.1 AA compliant.
 */

import { type TreeNode, type TreeViewConfig } from './types'
import { loadChildren } from './lazy'
import { uniqueId } from '../../primitives/utils'
import { escapeHtml } from '../../utils/sanitize'
import { announce, createLiveRegion, type LiveRegion } from '../../utils/a11y'
import { Keys, createTypeAhead } from '../../utils/keyboard'

export type { TreeNode, TreeViewConfig } from './types'

/** Resolve a container reference to an HTMLElement */
function resolveContainer(container: HTMLElement | string): HTMLElement {
  if (typeof container === 'string') {
    const el = document.querySelector<HTMLElement>(container)
    if (!el) {
      throw new Error(`TreeView container not found: ${container}`)
    }
    return el
  }
  return container
}

/** Flatten tree nodes with their depth for keyboard navigation */
interface FlatNode {
  node: TreeNode
  depth: number
  parent: TreeNode | null
  index: number
}

export class TreeView {
  private readonly container: HTMLElement
  private readonly config: TreeViewConfig
  private data: TreeNode[] = []
  private selectedNode: TreeNode | null = null
  private focusedNode: TreeNode | null = null
  private wrapperEl: HTMLElement | null = null
  private highlightQuery = ''
  private liveRegion: LiveRegion | null = null

  // Accessibility state
  private readonly treeId: string
  private readonly labelId: string
  private flatNodes: FlatNode[] = []
  private typeAhead: ReturnType<typeof createTypeAhead> | null = null

  constructor(config: TreeViewConfig) {
    this.container = resolveContainer(config.container)
    this.config = config
    this.treeId = uniqueId('cmm-tree')
    this.labelId = uniqueId('cmm-tree-label')

    if (config.data !== undefined) {
      this.data = config.data
    }

    // Create live region for announcements
    this.liveRegion = createLiveRegion('polite')

    this.render()
  }

  /** Replace all nodes and re-render */
  setData(data: TreeNode[]): void {
    this.data = data
    this.selectedNode = null
    this.focusedNode = null
    this.render()
    this.announceUpdate(`Tree updated with ${String(this.countNodes(data))} items`)
  }

  /** Expand all nodes recursively */
  expandAll(): void {
    this.setExpandedRecursive(this.data, true)
    this.render()
    this.announceUpdate('All nodes expanded')
  }

  /** Collapse all nodes recursively */
  collapseAll(): void {
    this.setExpandedRecursive(this.data, false)
    this.render()
    this.announceUpdate('All nodes collapsed')
  }

  /** Expand a specific node by id */
  expandNode(id: string): void {
    const node = this.findNodeById(this.data, id)
    if (node) {
      node.expanded = true
      this.render()
      this.announceUpdate(`${node.label} expanded`)
    }
  }

  /** Collapse a specific node by id */
  collapseNode(id: string): void {
    const node = this.findNodeById(this.data, id)
    if (node) {
      node.expanded = false
      this.render()
      this.announceUpdate(`${node.label} collapsed`)
    }
  }

  /** Highlight nodes matching the given query string */
  highlight(query: string): void {
    this.highlightQuery = query.toLowerCase()
    this.render()
  }

  /** Get the last selected node, or null */
  getSelectedNode(): TreeNode | null {
    return this.selectedNode
  }

  /** Remove the tree from the DOM and clean up */
  destroy(): void {
    this.container.innerHTML = ''
    this.wrapperEl = null
    this.selectedNode = null
    this.focusedNode = null
    this.liveRegion?.destroy()
    this.liveRegion = null
  }

  /** Announce an update to screen readers */
  private announceUpdate(message: string): void {
    this.liveRegion?.announce(message)
  }

  /** Count total nodes in tree */
  private countNodes(nodes: TreeNode[]): number {
    let count = 0
    for (const node of nodes) {
      count++
      if (node.children !== undefined) {
        count += this.countNodes(node.children)
      }
    }
    return count
  }

  /** Set expanded state on all nodes recursively */
  private setExpandedRecursive(nodes: TreeNode[], expanded: boolean): void {
    for (const node of nodes) {
      if (
        node.expandable !== false &&
        (node.children !== undefined || node.lazyLoad !== undefined)
      ) {
        node.expanded = expanded
      }
      if (node.children !== undefined) {
        this.setExpandedRecursive(node.children, expanded)
      }
    }
  }

  /** Find a node by id in the tree */
  private findNodeById(nodes: TreeNode[], id: string): TreeNode | null {
    for (const node of nodes) {
      if (node.id === id) {
        return node
      }
      if (node.children !== undefined) {
        const found = this.findNodeById(node.children, id)
        if (found) {
          return found
        }
      }
    }
    return null
  }

  /** Check if a node or any descendant matches the current search */
  private nodeMatchesSearch(node: TreeNode, query: string): boolean {
    if (query === '') {
      return true
    }

    if (node.label.toLowerCase().includes(query)) {
      return true
    }

    if (node.children !== undefined) {
      for (const child of node.children) {
        if (this.nodeMatchesSearch(child, query)) {
          return true
        }
      }
    }

    return false
  }

  /** Build flat list of visible nodes for keyboard navigation */
  private buildFlatNodes(): void {
    this.flatNodes = []
    this.flattenNodes(this.data, 0, null)

    // Update type-ahead with labels
    const items = this.flatNodes.map(fn => ({ label: fn.node.label }))
    this.typeAhead = createTypeAhead({ items })
  }

  /** Recursively flatten visible nodes */
  private flattenNodes(nodes: TreeNode[], depth: number, parent: TreeNode | null): void {
    for (const node of nodes) {
      if (this.highlightQuery !== '' && !this.nodeMatchesSearch(node, this.highlightQuery)) {
        continue
      }

      this.flatNodes.push({
        node,
        depth,
        parent,
        index: this.flatNodes.length,
      })

      if (node.expanded === true && node.children !== undefined && node.children.length > 0) {
        this.flattenNodes(node.children, depth + 1, node)
      }
    }
  }

  /** Get the flat node info for a tree node */
  private getFlatNode(node: TreeNode): FlatNode | null {
    return this.flatNodes.find(fn => fn.node.id === node.id) ?? null
  }

  /** Get visible nodes (for aria-setsize) at same level */
  private getSiblingsAtLevel(node: TreeNode): TreeNode[] {
    const flatNode = this.getFlatNode(node)
    if (flatNode === null) {
      return []
    }

    const parent = flatNode.parent
    if (parent === null) {
      // Root level
      return this.data.filter(
        n => this.highlightQuery === '' || this.nodeMatchesSearch(n, this.highlightQuery)
      )
    }

    // Get parent's children
    return (parent.children ?? []).filter(
      n => this.highlightQuery === '' || this.nodeMatchesSearch(n, this.highlightQuery)
    )
  }

  /** Handle keyboard navigation within the tree */
  private handleKeyDown(event: KeyboardEvent): void {
    if (this.flatNodes.length === 0 || this.focusedNode === null) {
      return
    }

    const currentFlatNode = this.getFlatNode(this.focusedNode)
    if (currentFlatNode === null) {
      return
    }

    const currentIndex = currentFlatNode.index
    const node = this.focusedNode
    const hasChildren = node.children !== undefined || node.lazyLoad !== undefined
    const isExpanded = node.expanded === true

    switch (event.key) {
      case Keys.ARROW_DOWN: {
        event.preventDefault()
        // Move to next visible node
        if (currentIndex < this.flatNodes.length - 1) {
          const nextFlatNode = this.flatNodes[currentIndex + 1]
          if (nextFlatNode !== undefined) {
            this.focusNode(nextFlatNode.node)
          }
        }
        break
      }
      case Keys.ARROW_UP: {
        event.preventDefault()
        // Move to previous visible node
        if (currentIndex > 0) {
          const prevFlatNode = this.flatNodes[currentIndex - 1]
          if (prevFlatNode !== undefined) {
            this.focusNode(prevFlatNode.node)
          }
        }
        break
      }
      case Keys.ARROW_RIGHT: {
        event.preventDefault()
        if (hasChildren) {
          if (!isExpanded) {
            // Expand closed node
            void this.handleToggle(node)
          } else if (node.children !== undefined && node.children.length > 0) {
            // Move to first child
            const firstChild = node.children[0]
            if (firstChild !== undefined) {
              this.focusNode(firstChild)
            }
          }
        }
        break
      }
      case Keys.ARROW_LEFT: {
        event.preventDefault()
        if (isExpanded && hasChildren) {
          // Collapse expanded node
          void this.handleToggle(node)
        } else if (currentFlatNode.parent !== null) {
          // Move to parent
          this.focusNode(currentFlatNode.parent)
        }
        break
      }
      case Keys.HOME: {
        event.preventDefault()
        // Move to first visible node
        const firstFlatNode = this.flatNodes[0]
        if (firstFlatNode !== undefined) {
          this.focusNode(firstFlatNode.node)
        }
        break
      }
      case Keys.END: {
        event.preventDefault()
        // Move to last visible node
        const lastFlatNode = this.flatNodes[this.flatNodes.length - 1]
        if (lastFlatNode !== undefined) {
          this.focusNode(lastFlatNode.node)
        }
        break
      }
      case Keys.ENTER:
      case Keys.SPACE: {
        event.preventDefault()
        // Select/activate the node
        this.selectNode(node)
        break
      }
      case '*': {
        event.preventDefault()
        // Expand all siblings at this level
        const siblings = this.getSiblingsAtLevel(node)
        for (const sibling of siblings) {
          if (sibling.expandable !== false && sibling.children !== undefined) {
            sibling.expanded = true
          }
        }
        this.render()
        this.announceUpdate('All siblings expanded')
        break
      }
      default: {
        // Type-ahead: focus node starting with typed character
        if (event.key.length === 1 && !event.ctrlKey && !event.altKey && !event.metaKey) {
          const matchIndex = this.typeAhead?.handleKey(event.key)
          if (matchIndex !== undefined && matchIndex >= 0) {
            const matchedFlatNode = this.flatNodes[matchIndex]
            if (matchedFlatNode !== undefined) {
              this.focusNode(matchedFlatNode.node)
            }
          }
        }
        break
      }
    }
  }

  /** Focus a specific node */
  private focusNode(node: TreeNode): void {
    this.focusedNode = node
    this.updateFocusState()

    // Announce the node
    const hasChildren = node.children !== undefined || node.lazyLoad !== undefined
    const isExpanded = node.expanded === true
    let announcement = node.label

    if (hasChildren) {
      announcement += isExpanded ? ', expanded' : ', collapsed'
    }

    announce(announcement, 'polite')
  }

  /** Select a node (activate it) */
  private selectNode(node: TreeNode): void {
    this.selectedNode = node
    this.focusedNode = node
    this.config.onSelect?.(node)
    this.render()
    this.announceUpdate(`${node.label} selected`)
  }

  /** Update focus state in the DOM */
  private updateFocusState(): void {
    if (!this.wrapperEl) {
      return
    }

    // Remove focus from all nodes
    const allItems = this.wrapperEl.querySelectorAll<HTMLElement>('[role="treeitem"]')
    for (const item of allItems) {
      item.setAttribute('tabindex', '-1')
    }

    // Set focus on current node
    if (this.focusedNode !== null) {
      const focusedEl = this.wrapperEl.querySelector<HTMLElement>(
        `[data-node-id="${this.focusedNode.id}"] > [role="treeitem"]`
      )
      if (focusedEl !== null) {
        focusedEl.setAttribute('tabindex', '0')
        focusedEl.focus()
      }
    }
  }

  /** Render the full tree into the container */
  private render(): void {
    this.container.innerHTML = ''

    const wrapper = document.createElement('div')
    wrapper.className = 'cmm-tree-view'
    this.wrapperEl = wrapper

    // Accessible label (hidden)
    const label = document.createElement('span')
    label.id = this.labelId
    label.className = 'cmm-sr-only'
    label.textContent =
      'Tree navigation. Use arrow keys to navigate. Press Enter or Space to select.'
    wrapper.appendChild(label)

    // Search input
    if (this.config.searchable === true) {
      const searchBox = document.createElement('div')
      searchBox.className = 'cmm-tree-view__search'

      const searchLabel = document.createElement('label')
      searchLabel.className = 'cmm-sr-only'
      const searchId = uniqueId('cmm-tree-search')
      searchLabel.setAttribute('for', searchId)
      searchLabel.textContent = 'Filter tree'
      searchBox.appendChild(searchLabel)

      const input = document.createElement('input')
      input.type = 'text'
      input.id = searchId
      input.className = 'cmm-tree-view__search-input'
      input.placeholder = 'Filter...'
      input.value = this.highlightQuery
      input.setAttribute('aria-label', 'Filter tree items')
      input.addEventListener('input', () => {
        this.highlightQuery = input.value.toLowerCase()
        this.renderNodes()
      })

      searchBox.appendChild(input)
      wrapper.appendChild(searchBox)
    }

    // Tree container
    const treeContainer = document.createElement('div')
    treeContainer.className = 'cmm-tree-view__nodes'
    treeContainer.id = this.treeId
    treeContainer.setAttribute('role', 'tree')
    treeContainer.setAttribute('aria-labelledby', this.labelId)
    treeContainer.dataset.role = 'nodes'

    // Add keyboard handler
    treeContainer.addEventListener('keydown', e => {
      this.handleKeyDown(e)
    })

    wrapper.appendChild(treeContainer)
    this.container.appendChild(wrapper)
    this.renderNodes()
  }

  /** Re-render only the nodes portion of the tree */
  private renderNodes(): void {
    if (!this.wrapperEl) {
      return
    }

    const nodesContainer = this.wrapperEl.querySelector<HTMLElement>('[data-role="nodes"]')
    if (!nodesContainer) {
      return
    }

    nodesContainer.innerHTML = ''

    // Build flat nodes for keyboard navigation
    this.buildFlatNodes()

    // Get visible root nodes for aria-setsize
    const visibleRootNodes = this.data.filter(
      n => this.highlightQuery === '' || this.nodeMatchesSearch(n, this.highlightQuery)
    )

    let posInSet = 0
    for (const node of this.data) {
      if (this.highlightQuery !== '' && !this.nodeMatchesSearch(node, this.highlightQuery)) {
        continue
      }
      posInSet++
      const el = this.buildNodeElement(node, 0, posInSet, visibleRootNodes.length)
      nodesContainer.appendChild(el)
    }

    // Focus first node if none focused
    if (this.focusedNode === null && this.flatNodes.length > 0) {
      const firstNode = this.flatNodes[0]?.node
      if (firstNode !== undefined) {
        this.focusedNode = firstNode
      }
    }

    this.updateFocusState()
  }

  /** Build a DOM element for a single tree node (recursive) */
  private buildNodeElement(
    node: TreeNode,
    depth: number,
    posInSet: number,
    setSize: number
  ): HTMLElement {
    const nodeEl = document.createElement('div')
    nodeEl.className = 'cmm-tree-node'
    nodeEl.dataset.nodeId = node.id

    if (node.expanded === true) {
      nodeEl.classList.add('cmm-tree-node--expanded')
    }

    if (this.selectedNode !== null && this.selectedNode.id === node.id) {
      nodeEl.classList.add('cmm-tree-node--selected')
    }

    // The row is the treeitem
    const row = document.createElement('div')
    row.className = 'cmm-tree-node__row'
    row.setAttribute('role', 'treeitem')
    row.setAttribute('aria-level', String(depth + 1))
    row.setAttribute('aria-setsize', String(setSize))
    row.setAttribute('aria-posinset', String(posInSet))
    row.style.paddingLeft = `${String(depth * 16 + 8)}px`

    // Determine if node is focusable
    const isFocused = this.focusedNode !== null && this.focusedNode.id === node.id
    row.setAttribute('tabindex', isFocused ? '0' : '-1')

    // Handle expand/collapse state
    const hasChildren = node.children !== undefined || node.lazyLoad !== undefined
    const isExpandable = node.expandable !== false && hasChildren

    if (isExpandable) {
      row.setAttribute('aria-expanded', node.expanded === true ? 'true' : 'false')
    }

    // Selection state
    if (this.selectedNode !== null && this.selectedNode.id === node.id) {
      row.setAttribute('aria-selected', 'true')
    } else {
      row.setAttribute('aria-selected', 'false')
    }

    // Toggle button (for visual indication)
    const toggle = document.createElement('span')
    toggle.className = 'cmm-tree-node__toggle'
    toggle.setAttribute('aria-hidden', 'true')

    if (isExpandable) {
      toggle.textContent = node.expanded === true ? '\u25BE' : '\u25B8'
      toggle.addEventListener('click', (e: Event) => {
        e.stopPropagation()
        void this.handleToggle(node)
      })
    } else {
      toggle.textContent = ' '
      toggle.style.visibility = 'hidden'
    }

    row.appendChild(toggle)

    // Icon
    if (node.icon !== undefined) {
      const icon = document.createElement('span')
      icon.className = 'cmm-tree-node__icon'
      icon.setAttribute('aria-hidden', 'true')
      icon.textContent = node.icon
      row.appendChild(icon)
    }

    // Label
    const label = document.createElement('span')
    label.className = 'cmm-tree-node__label'

    if (this.highlightQuery !== '' && node.label.toLowerCase().includes(this.highlightQuery)) {
      label.innerHTML = this.highlightText(node.label, this.highlightQuery)
    } else {
      label.textContent = node.label
    }

    row.appendChild(label)

    // Click handlers
    row.addEventListener('click', () => {
      this.selectNode(node)
    })

    row.addEventListener('dblclick', () => {
      this.config.onDoubleClick?.(node)
    })

    nodeEl.appendChild(row)

    // Children (nested group)
    if (node.expanded === true && node.children !== undefined && node.children.length > 0) {
      const childrenEl = document.createElement('div')
      childrenEl.className = 'cmm-tree-node__children'
      childrenEl.setAttribute('role', 'group')

      // Get visible children for setsize
      const visibleChildren = node.children.filter(
        n => this.highlightQuery === '' || this.nodeMatchesSearch(n, this.highlightQuery)
      )

      let childPosInSet = 0
      for (const child of node.children) {
        if (this.highlightQuery !== '' && !this.nodeMatchesSearch(child, this.highlightQuery)) {
          continue
        }
        childPosInSet++
        childrenEl.appendChild(
          this.buildNodeElement(child, depth + 1, childPosInSet, visibleChildren.length)
        )
      }

      nodeEl.appendChild(childrenEl)
    }

    return nodeEl
  }

  /** Handle expand/collapse toggle, including lazy loading */
  private async handleToggle(node: TreeNode): Promise<void> {
    if (node.expanded === true) {
      node.expanded = false
      this.announceUpdate(`${node.label} collapsed`)
    } else {
      node.expanded = true
      if (
        node.lazyLoad !== undefined &&
        (node.children === undefined || node.children.length === 0)
      ) {
        this.announceUpdate(`Loading ${node.label}`)
        await loadChildren(node)
      }
      this.announceUpdate(`${node.label} expanded`)
      this.config.onExpand?.(node)
    }
    this.render()
  }

  /** Highlight matching text within a label */
  private highlightText(text: string, query: string): string {
    const lowerText = text.toLowerCase()
    const idx = lowerText.indexOf(query)
    if (idx < 0) {
      return escapeHtml(text)
    }

    const before = text.slice(0, idx)
    const match = text.slice(idx, idx + query.length)
    const after = text.slice(idx + query.length)

    return `${escapeHtml(before)}<mark class="cmm-tree-node__highlight">${escapeHtml(match)}</mark>${escapeHtml(after)}`
  }
}
