/**
 * FileTree - Hierarchical file/folder tree with expand/collapse and drag-and-drop
 * @countermeasure/web-components/visualization/file-tree
 */

import { resolveContainer, uniqueId } from '../../primitives/utils'
import type { FileTreeConfig, FileTreeNode } from './types'
import { setSafeIconHtml } from '../../utils/sanitize'

export type { FileTreeConfig, FileTreeNode } from './types'

/** SVG icon for a closed folder */
const FOLDER_ICON = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M1.5 2.5h4.25l1.25 1.5h7.5v9.5h-13z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
</svg>`

/** SVG icon for an open folder */
const FOLDER_OPEN_ICON = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M1.5 2.5h4.25l1.25 1.5h7.5v2H3.5l-2 6.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
  <path d="M3.5 6.5h11l-2 7h-11z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
</svg>`

/** SVG icon for a file */
const FILE_ICON = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M3 1.5h6.5L13 5v9.5H3z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
  <path d="M9.5 1.5v3.5H13" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
</svg>`

/** SVG chevron for expand/collapse */
const CHEVRON_ICON = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`

/** Flattened representation of a visible node for keyboard navigation */
interface FlatNode {
  node: FileTreeNode
  depth: number
  rowEl: HTMLElement
}

export class FileTree {
  private readonly id: string
  private readonly container: HTMLElement
  private readonly config: FileTreeConfig
  private readonly indent: number
  private readonly showIcons: boolean

  private data: FileTreeNode[]
  private wrapperEl: HTMLElement | null = null
  private selectedIds = new Set<string>()
  private flatNodes: FlatNode[] = []
  private focusedIndex = -1

  /** Expansion state stored by node id */
  private readonly expandedIds = new Set<string>()

  /** Drag state */
  private dragSourceNode: FileTreeNode | null = null

  constructor(config: FileTreeConfig) {
    this.id = uniqueId('cmm-file-tree')
    this.container = resolveContainer(config.container)
    this.config = config
    this.data = this.cloneData(config.data)
    this.indent = config.indent ?? 16
    this.showIcons = config.showIcons !== false

    // Initialize expanded state from data
    this.initExpandedState(this.data)

    this.render()
  }

  /** Replace tree data and re-render */
  update(data: FileTreeNode[]): void {
    this.data = this.cloneData(data)
    this.initExpandedState(this.data)
    this.render()
  }

  /** Get all currently selected nodes */
  getSelected(): FileTreeNode[] {
    const result: FileTreeNode[] = []
    this.collectSelected(this.data, result)
    return result
  }

  /** Expand all folders */
  expandAll(): void {
    this.setAllExpanded(this.data, true)
    this.render()
  }

  /** Collapse all folders */
  collapseAll(): void {
    this.setAllExpanded(this.data, false)
    this.render()
  }

  /** Clean up the component and remove from DOM */
  destroy(): void {
    this.container.innerHTML = ''
    this.wrapperEl = null
    this.flatNodes = []
    this.selectedIds.clear()
    this.expandedIds.clear()
  }

  // ---------------------------------------------------------------------------
  // Data helpers
  // ---------------------------------------------------------------------------

  /** Deep clone data to avoid mutating the caller's objects */
  private cloneData(data: FileTreeNode[]): FileTreeNode[] {
    return data.map(n => this.cloneNode(n))
  }

  private cloneNode(node: FileTreeNode): FileTreeNode {
    const cloned: FileTreeNode = {
      id: node.id,
      name: node.name,
      type: node.type,
    }
    if (node.icon !== undefined) {
      cloned.icon = node.icon
    }
    if (node.data !== undefined) {
      cloned.data = { ...node.data }
    }
    if (node.expanded !== undefined) {
      cloned.expanded = node.expanded
    }
    if (node.selected !== undefined) {
      cloned.selected = node.selected
    }
    if (node.children !== undefined) {
      cloned.children = node.children.map(c => this.cloneNode(c))
    }
    return cloned
  }

  /** Initialize the expandedIds set from node data */
  private initExpandedState(nodes: FileTreeNode[]): void {
    for (const node of nodes) {
      if (node.type === 'folder' && node.expanded === true) {
        this.expandedIds.add(node.id)
      }
      if (node.selected === true) {
        this.selectedIds.add(node.id)
      }
      if (node.children !== undefined) {
        this.initExpandedState(node.children)
      }
    }
  }

  /** Set all folders expanded or collapsed */
  private setAllExpanded(nodes: FileTreeNode[], expanded: boolean): void {
    for (const node of nodes) {
      if (node.type === 'folder') {
        if (expanded) {
          this.expandedIds.add(node.id)
        } else {
          this.expandedIds.delete(node.id)
        }
      }
      if (node.children !== undefined) {
        this.setAllExpanded(node.children, expanded)
      }
    }
  }

  /** Collect selected nodes from the tree */
  private collectSelected(nodes: FileTreeNode[], result: FileTreeNode[]): void {
    for (const node of nodes) {
      if (this.selectedIds.has(node.id)) {
        result.push(node)
      }
      if (node.children !== undefined) {
        this.collectSelected(node.children, result)
      }
    }
  }

  // ---------------------------------------------------------------------------
  // Rendering
  // ---------------------------------------------------------------------------

  /** Main render method */
  private render(): void {
    this.container.innerHTML = ''
    this.flatNodes = []

    const wrapper = document.createElement('div')
    wrapper.className = 'cmm-file-tree'
    wrapper.id = this.id
    wrapper.setAttribute('role', 'tree')
    wrapper.setAttribute('tabindex', '0')

    if (this.config.className !== undefined) {
      wrapper.classList.add(this.config.className)
    }

    if (this.data.length === 0) {
      const empty = document.createElement('div')
      empty.className = 'cmm-file-tree__empty'
      empty.textContent = 'No files'
      wrapper.appendChild(empty)
    } else {
      const list = document.createElement('ul')
      list.className = 'cmm-file-tree__list cmm-file-tree__list--root'
      list.setAttribute('role', 'group')
      this.renderNodes(this.data, list, 0)
      wrapper.appendChild(list)
    }

    // Keyboard navigation
    wrapper.addEventListener('keydown', this.handleKeyDown.bind(this))

    this.container.appendChild(wrapper)
    this.wrapperEl = wrapper
  }

  /** Recursively render tree nodes */
  private renderNodes(nodes: FileTreeNode[], parentEl: HTMLElement, depth: number): void {
    for (const node of nodes) {
      const isFolder = node.type === 'folder'
      const isExpanded = this.expandedIds.has(node.id)
      const isSelected = this.selectedIds.has(node.id)

      const item = document.createElement('li')
      item.className = 'cmm-file-tree__item'
      item.setAttribute('role', 'treeitem')
      item.setAttribute('aria-expanded', isFolder ? String(isExpanded) : 'false')
      item.setAttribute('data-node-id', node.id)

      // Row
      const row = document.createElement('div')
      row.className = 'cmm-file-tree__row'

      if (isSelected) {
        row.classList.add('cmm-file-tree__row--selected')
      }

      row.style.paddingLeft = `${String(depth * this.indent + 8)}px`

      // Chevron
      const chevron = document.createElement('span')
      chevron.className = 'cmm-file-tree__chevron'
      if (isFolder) {
        chevron.innerHTML = CHEVRON_ICON
        if (isExpanded) {
          chevron.classList.add('cmm-file-tree__chevron--expanded')
        }
      } else {
        chevron.classList.add('cmm-file-tree__chevron--hidden')
      }
      row.appendChild(chevron)

      // Icon
      if (this.showIcons) {
        const icon = document.createElement('span')
        icon.className = 'cmm-file-tree__icon'

        if (node.icon !== undefined) {
          setSafeIconHtml(icon, node.icon)
        } else if (isFolder) {
          icon.classList.add('cmm-file-tree__icon--folder')
          icon.innerHTML = isExpanded ? FOLDER_OPEN_ICON : FOLDER_ICON
        } else {
          icon.innerHTML = FILE_ICON
        }
        row.appendChild(icon)
      }

      // Label
      const label = document.createElement('span')
      label.className = 'cmm-file-tree__label'
      label.textContent = node.name
      row.appendChild(label)

      // Click handler
      row.addEventListener('click', (e: MouseEvent) => {
        this.handleNodeClick(node, e)
      })

      // Drag-and-drop
      if (this.config.draggable === true) {
        row.draggable = true

        row.addEventListener('dragstart', (e: DragEvent) => {
          this.dragSourceNode = node
          if (e.dataTransfer !== null) {
            e.dataTransfer.effectAllowed = 'move'
            e.dataTransfer.setData('text/plain', node.id)
          }
        })

        row.addEventListener('dragover', (e: DragEvent) => {
          if (
            this.dragSourceNode !== null &&
            this.dragSourceNode.id !== node.id &&
            node.type === 'folder'
          ) {
            e.preventDefault()
            if (e.dataTransfer !== null) {
              e.dataTransfer.dropEffect = 'move'
            }
            row.classList.add('cmm-file-tree__row--drag-over')
          }
        })

        row.addEventListener('dragleave', () => {
          row.classList.remove('cmm-file-tree__row--drag-over')
        })

        row.addEventListener('drop', (e: DragEvent) => {
          e.preventDefault()
          row.classList.remove('cmm-file-tree__row--drag-over')

          if (
            this.dragSourceNode !== null &&
            this.dragSourceNode.id !== node.id &&
            node.type === 'folder'
          ) {
            if (this.config.onDrop !== undefined) {
              this.config.onDrop(this.dragSourceNode, node)
            }
          }
          this.dragSourceNode = null
        })

        row.addEventListener('dragend', () => {
          this.dragSourceNode = null
        })
      }

      item.appendChild(row)

      // Track flat node for keyboard navigation
      const flatIndex = this.flatNodes.length
      this.flatNodes.push({ node, depth, rowEl: row })

      // Focus tracking
      if (flatIndex === this.focusedIndex) {
        row.classList.add('cmm-file-tree__row--focused')
      }

      // Children
      if (isFolder && node.children !== undefined && node.children.length > 0) {
        const childList = document.createElement('ul')
        childList.className = 'cmm-file-tree__children'
        childList.setAttribute('role', 'group')

        if (!isExpanded) {
          childList.classList.add('cmm-file-tree__children--collapsed')
        }

        this.renderNodes(node.children, childList, depth + 1)
        item.appendChild(childList)
      }

      parentEl.appendChild(item)
    }
  }

  // ---------------------------------------------------------------------------
  // Interaction handlers
  // ---------------------------------------------------------------------------

  /** Handle click on a tree node row */
  private handleNodeClick(node: FileTreeNode, e: MouseEvent): void {
    // Toggle expansion for folders
    if (node.type === 'folder') {
      if (this.expandedIds.has(node.id)) {
        this.expandedIds.delete(node.id)
      } else {
        this.expandedIds.add(node.id)
      }

      if (this.config.onExpand !== undefined) {
        this.config.onExpand(node)
      }
    }

    // Handle selection
    if (this.config.selectable !== false) {
      const isMulti = this.config.multiSelect === true && (e.ctrlKey || e.metaKey)

      if (isMulti) {
        if (this.selectedIds.has(node.id)) {
          this.selectedIds.delete(node.id)
        } else {
          this.selectedIds.add(node.id)
        }
      } else {
        this.selectedIds.clear()
        this.selectedIds.add(node.id)
      }

      if (this.config.onSelect !== undefined) {
        this.config.onSelect(node)
      }
    }

    // Update focused index
    const idx = this.flatNodes.findIndex(fn => fn.node.id === node.id)
    if (idx >= 0) {
      this.focusedIndex = idx
    }

    this.render()
  }

  /** Handle keyboard navigation */
  private handleKeyDown(e: KeyboardEvent): void {
    if (this.flatNodes.length === 0) {
      return
    }

    switch (e.key) {
      case 'ArrowDown': {
        e.preventDefault()
        this.focusedIndex = Math.min(this.focusedIndex + 1, this.flatNodes.length - 1)
        this.render()
        this.scrollFocusedIntoView()
        break
      }
      case 'ArrowUp': {
        e.preventDefault()
        this.focusedIndex = Math.max(this.focusedIndex - 1, 0)
        this.render()
        this.scrollFocusedIntoView()
        break
      }
      case 'ArrowRight': {
        e.preventDefault()
        const current = this.flatNodes[this.focusedIndex]
        if (current !== undefined && current.node.type === 'folder') {
          if (!this.expandedIds.has(current.node.id)) {
            this.expandedIds.add(current.node.id)
            if (this.config.onExpand !== undefined) {
              this.config.onExpand(current.node)
            }
            this.render()
          } else {
            // Move to first child
            if (this.focusedIndex < this.flatNodes.length - 1) {
              this.focusedIndex += 1
              this.render()
            }
          }
        }
        break
      }
      case 'ArrowLeft': {
        e.preventDefault()
        const currentNode = this.flatNodes[this.focusedIndex]
        if (currentNode?.node.type === 'folder' && this.expandedIds.has(currentNode.node.id)) {
          this.expandedIds.delete(currentNode.node.id)
          if (this.config.onExpand !== undefined) {
            this.config.onExpand(currentNode.node)
          }
          this.render()
        } else if (currentNode !== undefined && currentNode.depth > 0) {
          // Move to parent
          for (let i = this.focusedIndex - 1; i >= 0; i--) {
            const candidate = this.flatNodes[i]
            if (candidate !== undefined && candidate.depth < currentNode.depth) {
              this.focusedIndex = i
              this.render()
              break
            }
          }
        }
        break
      }
      case 'Enter':
      case ' ': {
        e.preventDefault()
        const selected = this.flatNodes[this.focusedIndex]
        if (selected !== undefined) {
          this.handleNodeClick(selected.node, new MouseEvent('click'))
        }
        break
      }
    }
  }

  /** Scroll the focused row into view */
  private scrollFocusedIntoView(): void {
    if (this.wrapperEl === null || this.focusedIndex < 0) {
      return
    }

    const flat = this.flatNodes[this.focusedIndex]
    if (flat !== undefined) {
      flat.rowEl.scrollIntoView({ block: 'nearest' })
    }
  }
}
