import React from 'react'
import { getCaClassFromParams, supportsCaClass } from './caRuntime.js'
import type { CaEngine, JsonObject, PresetNode } from './types.js'

interface PresetNodeTreeProps {
  assignedNodeId?: string
  engines?: CaEngine[]
  nodes: PresetNode[]
  selectedNodeId?: string
  usedNodeIds: Set<string>
  onCreateChild: (node: PresetNode) => void
  onCreateGroup: (node: PresetNode) => void
  onDelete: (node: PresetNode) => boolean | void | Promise<boolean | void>
  onMove: (node: PresetNode, direction: 1 | -1) => void
  onRename: (node: PresetNode, name: string) => void | Promise<void>
  onSelect: (node: PresetNode) => void
  onSet: (node: PresetNode, path: string[], value: unknown) => void
  onUnset: (node: PresetNode, path: string[]) => void
}

interface TreeItem {
  node: PresetNode
  children: TreeItem[]
  siblingIndex: number
  siblingCount: number
}

interface VisibleTreeItem {
  item: TreeItem
  inheritedParams: JsonObject
  parentId: string | null
}

function isPlainObject(value: unknown): value is JsonObject {
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}

function valuesEqual(left: unknown, right: unknown): boolean {
  return JSON.stringify(left) === JSON.stringify(right)
}

function getParamPath(value: JsonObject, path: string[]) {
  let current: unknown = value
  for (const segment of path) {
    if (!isPlainObject(current) || !(segment in current)) return undefined
    current = current[segment]
  }
  return current
}

function formatParamValue(path: string[], value: unknown) {
  if (path.join('.') === 'simulation.initialCondition' && isPlainObject(value)) {
    const cells = Array.isArray(value.cells) ? value.cells.length : 0
    return `${cells} live cells`
  }

  if (Array.isArray(value)) return `${value.length} items`
  if (isPlainObject(value)) return 'object'
  return String(value)
}

function mergeParamObjects(base: JsonObject, patch: JsonObject): JsonObject {
  const merged: JsonObject = { ...base }

  for (const [key, value] of Object.entries(patch)) {
    const current = merged[key]
    merged[key] = isPlainObject(current) && isPlainObject(value) ? mergeParamObjects(current, value) : value
  }

  return merged
}

function compareNodesByOrder(left: PresetNode, right: PresetNode) {
  return left.sort_order - right.sort_order || left.name.localeCompare(right.name) || left.slug.localeCompare(right.slug)
}

function buildTree(nodes: PresetNode[]): TreeItem[] {
  const byParent = new Map<string | null, PresetNode[]>()
  for (const node of nodes) {
    const siblings = byParent.get(node.parent_id) ?? []
    siblings.push(node)
    byParent.set(node.parent_id, siblings)
  }

  function build(parentId: string | null): TreeItem[] {
    const siblings = (byParent.get(parentId) ?? []).sort(compareNodesByOrder)
    return siblings.map((node, index) => ({
      node,
      children: build(node.id),
      siblingIndex: index,
      siblingCount: siblings.length
    }))
  }

  return build(null)
}

function flattenVisibleTree(
  items: TreeItem[],
  expandedNodeIds: Set<string>,
  inheritedParams: JsonObject = {},
  parentId: string | null = null
): VisibleTreeItem[] {
  const visible: VisibleTreeItem[] = []
  for (const item of items) {
    visible.push({ item, inheritedParams, parentId })
    if (expandedNodeIds.has(item.node.id)) {
      const resolvedParams = mergeParamObjects(inheritedParams, item.node.params)
      visible.push(...flattenVisibleTree(item.children, expandedNodeIds, resolvedParams, item.node.id))
    }
  }
  return visible
}

export function PresetNodeTree({
  assignedNodeId,
  engines = [],
  nodes,
  selectedNodeId,
  usedNodeIds,
  onCreateChild,
  onCreateGroup,
  onDelete,
  onMove,
  onRename,
  onSelect,
  onSet,
  onUnset
}: PresetNodeTreeProps) {
  const tree = React.useMemo(() => buildTree(nodes), [nodes])
  const [expandedNodeIds, setExpandedNodeIds] = React.useState<Set<string>>(
    () => new Set(nodes.map((node) => node.id))
  )
  const [editingNodeId, setEditingNodeId] = React.useState('')
  const [openMenuNodeId, setOpenMenuNodeId] = React.useState('')
  const visibleItems = React.useMemo(
    () => flattenVisibleTree(tree, expandedNodeIds),
    [expandedNodeIds, tree]
  )

  React.useEffect(() => {
    setExpandedNodeIds((current) => {
      const next = new Set([...current].filter((id) => nodes.some((node) => node.id === id)))
      for (const node of nodes) {
        if (!current.has(node.id)) next.add(node.id)
      }
      return next
    })
  }, [nodes])

  React.useEffect(() => {
    if (!openMenuNodeId) return

    function closeMenu(event: PointerEvent) {
      const target = event.target
      if (!(target instanceof Element)) return
      if (target.closest('.tree-node-action-menu') || target.closest('.node-action-trigger')) return
      setOpenMenuNodeId('')
    }

    function closeOnEscape(event: KeyboardEvent) {
      if (event.key === 'Escape') setOpenMenuNodeId('')
    }

    document.addEventListener('pointerdown', closeMenu)
    document.addEventListener('keydown', closeOnEscape)
    return () => {
      document.removeEventListener('pointerdown', closeMenu)
      document.removeEventListener('keydown', closeOnEscape)
    }
  }, [openMenuNodeId])

  function toggleNode(nodeId: string, open?: boolean) {
    setExpandedNodeIds((current) => {
      const next = new Set(current)
      const shouldOpen = open ?? !next.has(nodeId)
      if (shouldOpen) next.add(nodeId)
      else next.delete(nodeId)
      return next
    })
  }

  function handleTreeKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
    if (!['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'Enter'].includes(event.key)) return
    const selectedIndex = visibleItems.findIndex(({ item }) => item.node.id === selectedNodeId)
    const currentIndex = selectedIndex === -1 ? 0 : selectedIndex
    const current = visibleItems[currentIndex]
    if (!current) return

    if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
      event.preventDefault()
      const offset = event.key === 'ArrowDown' ? 1 : -1
      const next = visibleItems[Math.min(Math.max(currentIndex + offset, 0), visibleItems.length - 1)]
      if (next) onSelect(next.item.node)
      return
    }

    if (event.key === 'ArrowRight') {
      event.preventDefault()
      if (current.item.children.length && !expandedNodeIds.has(current.item.node.id)) {
        toggleNode(current.item.node.id, true)
      } else if (current.item.children[0]) {
        onSelect(current.item.children[0].node)
      }
      return
    }

    if (event.key === 'ArrowLeft') {
      event.preventDefault()
      if (expandedNodeIds.has(current.item.node.id) && current.item.children.length) {
        toggleNode(current.item.node.id, false)
      } else if (current.parentId) {
        const parent = nodes.find((node) => node.id === current.parentId)
        if (parent) onSelect(parent)
      }
      return
    }

    event.preventDefault()
    setEditingNodeId(current.item.node.id)
  }

  if (tree.length === 0) {
    return <p className="empty">No nodes loaded.</p>
  }

  return (
    <div className="preset-node-tree" role="tree" tabIndex={0} onKeyDown={handleTreeKeyDown}>
      {tree.map((node) => (
        <TreeNodeView
          key={node.node.id}
          item={node}
          assignedNodeId={assignedNodeId}
          engines={engines}
          inheritedParams={{}}
          selectedNodeId={selectedNodeId}
          usedNodeIds={usedNodeIds}
          expandedNodeIds={expandedNodeIds}
          editingNodeId={editingNodeId}
          openMenuNodeId={openMenuNodeId}
          onCreateChild={onCreateChild}
          onCreateGroup={onCreateGroup}
          onDelete={onDelete}
          onEdit={setEditingNodeId}
          onMenuToggle={(nodeId) => setOpenMenuNodeId((current) => (current === nodeId ? '' : nodeId))}
          onMove={onMove}
          onRename={onRename}
          onSelect={onSelect}
          onSet={onSet}
          onToggle={toggleNode}
          onUnset={onUnset}
        />
      ))}
    </div>
  )
}

function TreeNodeView({
  item,
  assignedNodeId,
  engines,
  inheritedParams,
  selectedNodeId,
  usedNodeIds,
  expandedNodeIds,
  editingNodeId,
  openMenuNodeId,
  onCreateChild,
  onCreateGroup,
  onDelete,
  onEdit,
  onMenuToggle,
  onMove,
  onRename,
  onSelect,
  onSet,
  onToggle,
  onUnset
}: {
  item: TreeItem
  assignedNodeId?: string
  engines: CaEngine[]
  inheritedParams: JsonObject
  selectedNodeId?: string
  usedNodeIds: Set<string>
  expandedNodeIds: Set<string>
  editingNodeId: string
  openMenuNodeId: string
  onCreateChild: (node: PresetNode) => void
  onCreateGroup: (node: PresetNode) => void
  onDelete: (node: PresetNode) => boolean | void | Promise<boolean | void>
  onEdit: (nodeId: string) => void
  onMenuToggle: (nodeId: string) => void
  onMove: (node: PresetNode, direction: 1 | -1) => void
  onRename: (node: PresetNode, name: string) => void | Promise<void>
  onSelect: (node: PresetNode) => void
  onSet: (node: PresetNode, path: string[], value: unknown) => void
  onToggle: (nodeId: string, open?: boolean) => void
  onUnset: (node: PresetNode, path: string[]) => void
}) {
  const open = expandedNodeIds.has(item.node.id)
  const menuOpen = openMenuNodeId === item.node.id
  const selected = item.node.id === selectedNodeId
  const assigned = item.node.id === assignedNodeId
  const used = usedNodeIds.has(item.node.id)
  const usedDescendantCount = React.useMemo(() => {
    function countUsedDescendants(children: TreeItem[]): number {
      return children.reduce(
        (total, child) =>
          total + (usedNodeIds.has(child.node.id) ? 1 : 0) + countUsedDescendants(child.children),
        0
      )
    }
    return countUsedDescendants(item.children)
  }, [item.children, usedNodeIds])
  const resolvedParams = mergeParamObjects(inheritedParams, item.node.params)
  const kindLabel = item.node.kind === 'preset_root' ? 'root' : item.node.kind === 'chapter' ? 'group' : item.node.kind
  const usageLabel = assigned
    ? `${kindLabel} · assigned`
    : used
      ? `${kindLabel} · used by slides`
      : usedDescendantCount > 0
        ? `${kindLabel} · contains ${usedDescendantCount} used preset${usedDescendantCount === 1 ? '' : 's'}`
        : `${kindLabel} · unused`

  async function deleteFromMenu() {
    onMenuToggle(item.node.id)
    if (!window.confirm(`Delete preset "${item.node.name}"? This cannot be undone.`)) return
    await onDelete(item.node)
  }

  return (
    <div className="tree-item">
      <div className={`tree-node${selected ? ' active' : ''}${assigned ? ' assigned' : ''}${used || usedDescendantCount > 0 ? ' used' : ' unused'}`}>
        <button
          aria-label={item.children.length ? `${open ? 'Collapse' : 'Expand'} ${item.node.name}` : `${item.node.name} has no children`}
          className="tree-node-arrow"
          disabled={!item.children.length}
          type="button"
          onClick={() => onToggle(item.node.id)}
        >
          {item.children.length ? (open ? '▾' : '▸') : '•'}
        </button>
        <button
          aria-current={selected ? 'true' : undefined}
          className="tree-node-main"
          role="treeitem"
          type="button"
          onClick={() => onSelect(item.node)}
        >
          <strong>{item.node.name}</strong>
          <small>{usageLabel}</small>
        </button>
        <div className="tree-node-action-shell">
          <button
            aria-expanded={menuOpen}
            aria-haspopup="menu"
            aria-label={`Actions for ${item.node.name}`}
            className="icon-button tree-node-edit node-action-trigger"
            title="Preset actions"
            type="button"
            onClick={(event) => {
              event.stopPropagation()
              onMenuToggle(item.node.id)
            }}
          >
            ⋯
          </button>
          {menuOpen ? (
            <div className="tree-node-action-menu" role="menu">
              <button role="menuitem" type="button" onClick={() => {
                onMenuToggle(item.node.id)
                onEdit(item.node.id)
              }}>
                Edit
              </button>
              <button role="menuitem" type="button" onClick={() => {
                onMenuToggle(item.node.id)
                onCreateChild(item.node)
              }}>
                Create child preset
              </button>
              <button role="menuitem" type="button" onClick={() => {
                onMenuToggle(item.node.id)
                onCreateGroup(item.node)
              }}>
                Create child group
              </button>
              <button disabled={item.siblingIndex === 0} role="menuitem" type="button" onClick={() => {
                onMenuToggle(item.node.id)
                onMove(item.node, -1)
              }}>
                Move up
              </button>
              <button disabled={item.siblingIndex >= item.siblingCount - 1} role="menuitem" type="button" onClick={() => {
                onMenuToggle(item.node.id)
                onMove(item.node, 1)
              }}>
                Move down
              </button>
              <button className="danger" role="menuitem" type="button" onClick={() => void deleteFromMenu()}>
                Delete
              </button>
            </div>
          ) : null}
        </div>
      </div>
      {editingNodeId === item.node.id ? (
        <NodePropertiesModal
          engines={engines}
          inheritedParams={inheritedParams}
          node={item.node}
          resolvedParams={resolvedParams}
          onClose={() => onEdit('')}
          onDelete={onDelete}
          onRename={onRename}
          onSet={onSet}
          onUnset={onUnset}
        />
      ) : null}
      {open && item.children.length ? (
        <div className="tree-children">
          {item.children.map((child) => (
            <TreeNodeView
              assignedNodeId={assignedNodeId}
              engines={engines}
              inheritedParams={resolvedParams}
              item={child}
              key={child.node.id}
              usedNodeIds={usedNodeIds}
              expandedNodeIds={expandedNodeIds}
              editingNodeId={editingNodeId}
              openMenuNodeId={openMenuNodeId}
              onCreateChild={onCreateChild}
              onCreateGroup={onCreateGroup}
              onDelete={onDelete}
              onEdit={onEdit}
              onMenuToggle={onMenuToggle}
              onMove={onMove}
              onRename={onRename}
              onSelect={onSelect}
              onSet={onSet}
              onToggle={onToggle}
              onUnset={onUnset}
              selectedNodeId={selectedNodeId}
            />
          ))}
        </div>
      ) : null}
    </div>
  )
}

function collectResolvedParams(value: unknown, prefix: string[] = []): Array<{ path: string[]; value: unknown }> {
  if (!isPlainObject(value)) return []

  const entries: Array<{ path: string[]; value: unknown }> = []
  for (const [key, child] of Object.entries(value)) {
    const path = [...prefix, key]
    if (path.join('.') === 'simulation.initialCondition' || !isPlainObject(child)) {
      entries.push({ path, value: child })
    } else {
      entries.push(...collectResolvedParams(child, path))
    }
  }
  return entries
}

function isGenericEditableProperty(path: string[]) {
  const joinedPath = path.join('.')
  return (
    joinedPath !== 'simulation.initialCondition' &&
    joinedPath !== 'simulation.engineId' &&
    joinedPath !== 'simulation.neighborhoodId' &&
    !joinedPath.startsWith('caClass.')
  )
}

function engineParamPath(key: string) {
  if (key === 'rendererId') return ['renderer', 'id']
  if (key === 'ruleId') return ['simulation', 'ruleId']
  if (key.startsWith('grid.')) return ['simulation', ...key.split('.')]
  if (key.includes('.')) return key.split('.')
  return ['simulation', key]
}

function engineSettingKeys(engine: CaEngine | undefined) {
  return engine ? Object.keys(engine.params_schema) : []
}

function isEngineSettingPath(path: string[], engine: CaEngine | undefined) {
  const joinedPath = path.join('.')
  return engineSettingKeys(engine).some((key) => engineParamPath(key).join('.') === joinedPath)
}

function parsePropertyDraft(value: string) {
  const trimmed = value.trim()
  if (trimmed === '') return ''
  try {
    return JSON.parse(trimmed) as unknown
  } catch {
    return value
  }
}

function formatPropertyDraft(value: unknown) {
  if (typeof value === 'string') return value
  return JSON.stringify(value, null, 2)
}

function NodePropertiesModal({
  engines,
  inheritedParams,
  node,
  resolvedParams,
  onClose,
  onDelete,
  onRename,
  onSet,
  onUnset
}: {
  engines: CaEngine[]
  inheritedParams: JsonObject
  node: PresetNode
  resolvedParams: JsonObject
  onClose: () => void
  onDelete: (node: PresetNode) => boolean | void | Promise<boolean | void>
  onRename: (node: PresetNode, name: string) => void | Promise<void>
  onSet: (node: PresetNode, path: string[], value: unknown) => void
  onUnset: (node: PresetNode, path: string[]) => void
}) {
  const [nameDraft, setNameDraft] = React.useState(node.name)
  const resolvedEngineId = getParamPath(resolvedParams, ['simulation', 'engineId'])
  const caClass = getCaClassFromParams(resolvedParams)
  const resolvedEngine =
    typeof resolvedEngineId === 'string'
      ? engines.find((engine) => engine.engine_kind === resolvedEngineId)
      : undefined
  const resolvedEngineCompatible = resolvedEngine ? supportsCaClass(resolvedEngine, caClass) : false
  const activeEngine = resolvedEngineCompatible ? resolvedEngine : undefined
  const properties = collectResolvedParams(resolvedParams).filter(
    (property) => isGenericEditableProperty(property.path) && !isEngineSettingPath(property.path, activeEngine)
  )

  React.useEffect(() => {
    setNameDraft(node.name)
  }, [node.name])

  async function saveName() {
    const name = nameDraft.trim()
    if (!name) {
      setNameDraft(node.name)
      return
    }
    if (name !== node.name) await onRename(node, name)
  }

  async function deleteNode() {
    if (!window.confirm(`Delete preset "${node.name}"? This cannot be undone.`)) return
    const deleted = await onDelete(node)
    if (deleted === false) return
    onClose()
  }

  return (
    <div className="modal-backdrop" role="presentation" onMouseDown={onClose}>
      <section aria-modal="true" className="property-modal" role="dialog" onMouseDown={(event) => event.stopPropagation()}>
        <header className="property-modal-header">
          <div>
            <p className="eyebrow">Preset Editor</p>
            <h2>Edit preset</h2>
          </div>
          <span className="win95-window-controls has-close">
            <span className="window-control-decor">_</span>
            <span className="window-control-decor">□</span>
            <button
              aria-label="Close"
              className="window-control-button window-close-button"
              title="Close"
              type="button"
              onClick={onClose}
            >
              ×
            </button>
          </span>
        </header>
        <div className="property-modal-list">
          <section className="preset-modal-stage preset-modal-identity">
            <div className="preset-modal-stage-heading">
              <span>i</span>
              <div>
                <h3>Preset identity</h3>
                <p>Edit CA space, engine, and engine settings in the inspector.</p>
              </div>
            </div>
            <label>
              Preset name
              <input
                autoFocus
                value={nameDraft}
                onChange={(event) => setNameDraft(event.target.value)}
                onKeyDown={(event) => {
                  if (event.key === 'Enter') {
                    event.preventDefault()
                    void saveName()
                  }
                }}
              />
            </label>
            <button disabled={!nameDraft.trim() || nameDraft.trim() === node.name} type="button" onClick={() => void saveName()}>
              Save name
            </button>
          </section>

          {properties.map((property) => {
            const localValue = getParamPath(node.params, property.path)
            const inheritedValue = getParamPath(inheritedParams, property.path)
            const isLocal = localValue !== undefined && !valuesEqual(localValue, inheritedValue)
            return (
              <PropertyEditorRow
                inheritedValue={inheritedValue}
                isLocal={isLocal}
                key={property.path.join('.')}
                node={node}
                path={property.path}
                value={property.value}
                onSet={onSet}
                onUnset={onUnset}
              />
            )
          })}
          {properties.length === 0 ? <p className="empty">No properties are resolved for this node.</p> : null}
          <footer className="preset-modal-danger">
            <div>
              <strong>Delete preset</strong>
              <small>Slides referencing this preset must be reassigned first.</small>
            </div>
            <button className="danger" type="button" onClick={() => void deleteNode()}>
              Delete
            </button>
          </footer>
        </div>
      </section>
    </div>
  )
}

function PropertyEditorRow({
  inheritedValue,
  isLocal,
  node,
  path,
  value,
  onSet,
  onUnset
}: {
  inheritedValue: unknown
  isLocal: boolean
  node: PresetNode
  path: string[]
  value: unknown
  onSet: (node: PresetNode, path: string[], value: unknown) => void
  onUnset: (node: PresetNode, path: string[]) => void
}) {
  const [draft, setDraft] = React.useState(formatPropertyDraft(value))

  React.useEffect(() => {
    setDraft(formatPropertyDraft(value))
  }, [value])

  return (
    <div className="property-row">
      <div className="property-row-meta">
        <strong>{path.join('.')}</strong>
        <span className={isLocal ? 'local' : 'inherited'}>{isLocal ? 'local override' : 'inherited'}</span>
        {!isLocal && inheritedValue !== undefined ? <small>from parent: {formatParamValue(path, inheritedValue)}</small> : null}
      </div>
      <textarea value={draft} onChange={(event) => setDraft(event.target.value)} />
      <div className="property-row-actions">
        <button type="button" onClick={() => onSet(node, path, parsePropertyDraft(draft))}>
          Save
        </button>
        <button className="danger" disabled={!isLocal} type="button" onClick={() => onUnset(node, path)}>
          🗑
        </button>
      </div>
    </div>
  )
}
