/**
 * JsonPathBrowser - Interactive JSON tree browser with JSONPath support
 * @countermeasure/web-components/visualization/json-path-browser
 */

import { resolveContainer, uniqueId } from '../../primitives/utils'
import { escapeHtml } from '../../utils/sanitize'
import type { JsonPathBrowserConfig } from './types'

export type { JsonPathBrowserConfig } from './types'

/** Determine the display type of a value */
function getValueType(value: unknown): string {
  if (value === null) {
    return 'null'
  }
  if (Array.isArray(value)) {
    return 'array'
  }
  return typeof value
}

/** Format a value for display */
function formatValue(value: unknown): string {
  if (value === null) {
    return 'null'
  }
  if (typeof value === 'string') {
    return `"${escapeHtml(value)}"`
  }
  if (typeof value === 'number' || typeof value === 'boolean') {
    return String(value)
  }
  return ''
}

/** Get the type badge text */
function getTypeBadge(value: unknown): string {
  if (value === null) {
    return 'null'
  }
  if (Array.isArray(value)) {
    return `[${String(value.length)}]`
  }
  if (typeof value === 'object') {
    return `{${String(Object.keys(value).length)}}`
  }
  if (typeof value === 'string') {
    return 'str'
  }
  if (typeof value === 'number') {
    return '123'
  }
  if (typeof value === 'boolean') {
    return 'bool'
  }
  return typeof value
}

/** Check whether a value is a composite (object or array) */
function isComposite(value: unknown): value is Record<string, unknown> | unknown[] {
  return value !== null && typeof value === 'object'
}

/** Safely copy text to clipboard */
function copyText(text: string): void {
  void navigator.clipboard.writeText(text).catch(() => {
    // Fallback: silent fail in non-secure contexts
  })
}

interface TreeNodeState {
  expanded: boolean
}

export class JsonPathBrowser {
  private readonly container: HTMLElement
  private readonly config: JsonPathBrowserConfig
  private readonly instanceId: string
  private data: unknown
  private pathBarEl: HTMLElement | null = null
  private searchInputEl: HTMLInputElement | null = null
  private treeEl: HTMLElement | null = null
  private nodeStates = new Map<string, TreeNodeState>()
  private searchQuery = ''
  private selectedPath = ''

  constructor(config: JsonPathBrowserConfig) {
    this.container = resolveContainer(config.container)
    this.config = config
    this.instanceId = uniqueId('cmm-json-path')
    this.data = config.data

    if (config.expanded === true) {
      this.initExpandedStates(this.data, '$')
    }

    this.render()
  }

  /** Replace the JSON data and re-render */
  update(data: unknown): void {
    this.data = data
    this.nodeStates.clear()
    this.searchQuery = ''
    this.selectedPath = ''

    if (this.config.expanded === true) {
      this.initExpandedStates(this.data, '$')
    }

    this.render()
  }

  /** Expand all nodes */
  expandAll(): void {
    this.initExpandedStates(this.data, '$')
    this.renderTree()
  }

  /** Collapse all nodes */
  collapseAll(): void {
    this.nodeStates.clear()
    this.renderTree()
  }

  /** Set the search query and highlight matches */
  search(query: string): void {
    this.searchQuery = query.toLowerCase()

    if (this.searchInputEl !== null) {
      this.searchInputEl.value = query
    }

    // Auto-expand nodes that contain matches
    if (this.searchQuery.length > 0) {
      this.expandMatchingPaths(this.data, '$')
    }

    this.renderTree()
  }

  /** Remove the component from the DOM and clean up */
  destroy(): void {
    this.container.innerHTML = ''
    this.pathBarEl = null
    this.searchInputEl = null
    this.treeEl = null
    this.nodeStates.clear()
  }

  /** Recursively set all composite nodes as expanded */
  private initExpandedStates(value: unknown, path: string): void {
    if (!isComposite(value)) {
      return
    }

    this.nodeStates.set(path, { expanded: true })

    if (Array.isArray(value)) {
      for (let i = 0; i < value.length; i++) {
        const child = value[i]
        this.initExpandedStates(child, `${path}[${String(i)}]`)
      }
    } else {
      const entries = Object.entries(value)
      for (const [key, child] of entries) {
        this.initExpandedStates(child, `${path}.${key}`)
      }
    }
  }

  /** Expand paths that contain matching keys or values */
  private expandMatchingPaths(value: unknown, path: string): boolean {
    if (!isComposite(value)) {
      const valStr = formatValue(value).toLowerCase()
      return valStr.includes(this.searchQuery)
    }

    let hasMatch = false

    if (Array.isArray(value)) {
      for (let i = 0; i < value.length; i++) {
        const child = value[i]
        const childPath = `${path}[${String(i)}]`
        if (this.expandMatchingPaths(child, childPath)) {
          hasMatch = true
        }
      }
    } else {
      const entries = Object.entries(value)
      for (const [key, child] of entries) {
        const childPath = `${path}.${key}`
        if (key.toLowerCase().includes(this.searchQuery)) {
          hasMatch = true
        }
        if (this.expandMatchingPaths(child, childPath)) {
          hasMatch = true
        }
      }
    }

    if (hasMatch) {
      this.nodeStates.set(path, { expanded: true })
    }

    return hasMatch
  }

  /** Whether a node is expanded */
  private isExpanded(path: string): boolean {
    const state = this.nodeStates.get(path)
    return state?.expanded === true
  }

  /** Toggle the expanded state of a node */
  private toggleNode(path: string): void {
    const current = this.nodeStates.get(path)
    if (current !== undefined) {
      current.expanded = !current.expanded
    } else {
      this.nodeStates.set(path, { expanded: true })
    }
    this.renderTree()
  }

  /** Check if a key or value matches the search */
  private matchesSearch(key: string, value: unknown): boolean {
    if (this.searchQuery.length === 0) {
      return false
    }

    if (key.toLowerCase().includes(this.searchQuery)) {
      return true
    }

    if (!isComposite(value)) {
      const valStr = formatValue(value).toLowerCase()
      return valStr.includes(this.searchQuery)
    }

    return false
  }

  /** Render the entire component */
  private render(): void {
    this.container.innerHTML = ''

    const wrapper = document.createElement('div')
    wrapper.className = 'cmm-json-path'
    wrapper.id = this.instanceId

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

    // Toolbar
    const toolbar = document.createElement('div')
    toolbar.className = 'cmm-json-path__toolbar'

    // Expand / collapse buttons
    const expandBtn = document.createElement('button')
    expandBtn.className = 'cmm-json-path__btn'
    expandBtn.textContent = 'Expand All'
    expandBtn.addEventListener('click', () => {
      this.expandAll()
    })
    toolbar.appendChild(expandBtn)

    const collapseBtn = document.createElement('button')
    collapseBtn.className = 'cmm-json-path__btn'
    collapseBtn.textContent = 'Collapse All'
    collapseBtn.addEventListener('click', () => {
      this.collapseAll()
    })
    toolbar.appendChild(collapseBtn)

    // Search input
    if (this.config.searchable !== false) {
      const searchInput = document.createElement('input')
      searchInput.className = 'cmm-json-path__search'
      searchInput.type = 'text'
      searchInput.placeholder = 'Search keys or values...'
      searchInput.value = this.searchQuery
      searchInput.addEventListener('input', () => {
        this.search(searchInput.value)
      })
      this.searchInputEl = searchInput
      toolbar.appendChild(searchInput)
    }

    wrapper.appendChild(toolbar)

    // Path bar
    if (this.config.showPath !== false) {
      const pathBar = document.createElement('div')
      pathBar.className = 'cmm-json-path__path-bar'
      pathBar.textContent = this.selectedPath.length > 0 ? this.selectedPath : '$'
      pathBar.addEventListener('click', () => {
        const pathText = this.selectedPath.length > 0 ? this.selectedPath : '$'
        copyText(pathText)
        pathBar.classList.add('cmm-json-path__path-bar--copied')
        setTimeout(() => {
          pathBar.classList.remove('cmm-json-path__path-bar--copied')
        }, 1000)
      })
      pathBar.title = 'Click to copy path'
      this.pathBarEl = pathBar
      wrapper.appendChild(pathBar)
    }

    // Tree container
    const treeEl = document.createElement('div')
    treeEl.className = 'cmm-json-path__tree'
    this.treeEl = treeEl
    wrapper.appendChild(treeEl)

    this.container.appendChild(wrapper)

    this.renderTree()
  }

  /** Render only the tree portion */
  private renderTree(): void {
    if (this.treeEl === null) {
      return
    }

    this.treeEl.innerHTML = ''
    const rootNode = this.buildNode('$', '', this.data, 0)
    this.treeEl.appendChild(rootNode)
  }

  /** Build a tree node element recursively */
  private buildNode(path: string, key: string, value: unknown, depth: number): HTMLElement {
    const nodeEl = document.createElement('div')
    nodeEl.className = 'cmm-json-path__node'
    nodeEl.style.paddingLeft = `${String(depth * 16)}px`

    const isMatch = this.matchesSearch(key, value)
    if (isMatch) {
      nodeEl.classList.add('cmm-json-path__node--highlight')
    }

    if (isComposite(value)) {
      return this.buildCompositeNode(nodeEl, path, key, value, depth)
    }

    return this.buildLeafNode(nodeEl, path, key, value)
  }

  /** Build a composite (object/array) node */
  private buildCompositeNode(
    nodeEl: HTMLElement,
    path: string,
    key: string,
    value: Record<string, unknown> | unknown[],
    depth: number
  ): HTMLElement {
    const expanded = this.isExpanded(path)

    const rowEl = document.createElement('div')
    rowEl.className = 'cmm-json-path__row cmm-json-path__row--composite'

    // Toggle arrow
    const arrow = document.createElement('span')
    arrow.className = 'cmm-json-path__arrow'
    arrow.classList.add(
      expanded ? 'cmm-json-path__arrow--expanded' : 'cmm-json-path__arrow--collapsed'
    )
    arrow.textContent = expanded ? '\u25BE' : '\u25B8'
    arrow.addEventListener('click', () => {
      this.toggleNode(path)
    })
    rowEl.appendChild(arrow)

    // Key label
    if (key.length > 0) {
      const keyEl = document.createElement('span')
      keyEl.className = 'cmm-json-path__key'
      keyEl.textContent = key
      keyEl.addEventListener('click', () => {
        this.selectPath(path, value)
      })
      rowEl.appendChild(keyEl)

      const colonEl = document.createElement('span')
      colonEl.className = 'cmm-json-path__colon'
      colonEl.textContent = ': '
      rowEl.appendChild(colonEl)
    }

    // Type badge
    if (this.config.showTypes !== false) {
      const badge = document.createElement('span')
      badge.className =
        'cmm-json-path__type-badge cmm-json-path__type-badge--' + getValueType(value)
      badge.textContent = getTypeBadge(value)
      rowEl.appendChild(badge)
    }

    nodeEl.appendChild(rowEl)

    // Children
    if (expanded) {
      const childrenEl = document.createElement('div')
      childrenEl.className = 'cmm-json-path__children'

      if (Array.isArray(value)) {
        for (let i = 0; i < value.length; i++) {
          const child = value[i]
          const childPath = `${path}[${String(i)}]`
          childrenEl.appendChild(this.buildNode(childPath, String(i), child, depth + 1))
        }
      } else {
        const entries = Object.entries(value)
        for (const [childKey, childValue] of entries) {
          const childPath = `${path}.${childKey}`
          childrenEl.appendChild(this.buildNode(childPath, childKey, childValue, depth + 1))
        }
      }

      nodeEl.appendChild(childrenEl)
    }

    return nodeEl
  }

  /** Build a leaf (primitive) node */
  private buildLeafNode(
    nodeEl: HTMLElement,
    path: string,
    key: string,
    value: unknown
  ): HTMLElement {
    const rowEl = document.createElement('div')
    rowEl.className = 'cmm-json-path__row cmm-json-path__row--leaf'

    // Spacer for alignment with arrows
    const spacer = document.createElement('span')
    spacer.className = 'cmm-json-path__arrow-spacer'
    rowEl.appendChild(spacer)

    // Key
    if (key.length > 0) {
      const keyEl = document.createElement('span')
      keyEl.className = 'cmm-json-path__key'
      keyEl.textContent = key
      keyEl.addEventListener('click', () => {
        this.selectPath(path, value)
      })
      rowEl.appendChild(keyEl)

      const colonEl = document.createElement('span')
      colonEl.className = 'cmm-json-path__colon'
      colonEl.textContent = ': '
      rowEl.appendChild(colonEl)
    }

    // Value
    const valueType = getValueType(value)
    const valueEl = document.createElement('span')
    valueEl.className = `cmm-json-path__value cmm-json-path__value--${valueType}`
    valueEl.textContent = formatValue(value)
    valueEl.addEventListener('click', () => {
      this.selectPath(path, value)
      const text =
        typeof value === 'object'
          ? JSON.stringify(value)
          : String(value as string | number | boolean)
      copyText(text)
      valueEl.classList.add('cmm-json-path__value--copied')
      setTimeout(() => {
        valueEl.classList.remove('cmm-json-path__value--copied')
      }, 800)
    })
    valueEl.title = 'Click to copy value'
    rowEl.appendChild(valueEl)

    // Type badge
    if (this.config.showTypes !== false) {
      const badge = document.createElement('span')
      badge.className = `cmm-json-path__type-badge cmm-json-path__type-badge--${valueType}`
      badge.textContent = getTypeBadge(value)
      rowEl.appendChild(badge)
    }

    nodeEl.appendChild(rowEl)
    return nodeEl
  }

  /** Select a node path and update the path bar */
  private selectPath(path: string, value: unknown): void {
    this.selectedPath = path

    if (this.pathBarEl !== null) {
      this.pathBarEl.textContent = path
    }

    this.config.onSelect?.(path, value)
  }
}
