import { createElement, useRef, useState } from 'react'
import { X, Edit, Trash2, ExternalLink, Plus, Pencil, Layers, Ungroup, Eye, EyeOff } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'

import { useCanvasStore, serviceStatusKey } from '@/stores/canvasStore'
import { NODE_TYPE_LABELS, STATUS_COLORS, type ServiceInfo, type ServiceStatus, type NodeData, type NodeProperty } from '@/types'
import { getServiceUrl } from '@/utils/serviceUrl'
import { splitIps } from '@/utils/maskIp'
import { PROPERTY_ICONS, PROPERTY_ICON_NAMES, resolvePropertyIcon } from '@/utils/propertyIcons'
import { formatTimestamp } from '@/utils/timeFormat'
import type { Node } from '@xyflow/react'

interface DetailPanelProps {
  onEdit: (id: string) => void
}

type SvcForm = { port: string; protocol: 'tcp' | 'udp'; service_name: string; path: string }
const EMPTY_FORM: SvcForm = { port: '', protocol: 'tcp', service_name: '', path: '' }

type PropForm = { key: string; value: string; icon: string | null; visible: boolean }
const EMPTY_PROP: PropForm = { key: '', value: '', icon: null, visible: true }

export function DetailPanel({ onEdit }: DetailPanelProps) {
  const { nodes, selectedNodeId, selectedNodeIds, setSelectedNode, deleteNode, updateNode, snapshotHistory, createGroup, ungroup, removeFromGroup, setNodeSize } = useCanvasStore()
  const serviceStatuses = useCanvasStore((s) => s.serviceStatuses)

  const [addingForNode, setAddingForNode] = useState<string | null>(null)
  const [newSvc, setNewSvc] = useState<SvcForm>(EMPTY_FORM)
  const [editingFor, setEditingFor] = useState<{ nodeId: string; index: number } | null>(null)
  const [editSvc, setEditSvc] = useState<SvcForm>(EMPTY_FORM)
  const [groupName, setGroupName] = useState('')
  const [creatingGroup, setCreatingGroup] = useState(false)

  // Properties state
  const [addingProp, setAddingProp] = useState(false)
  const [newProp, setNewProp] = useState<PropForm>(EMPTY_PROP)
  const [editingPropIndex, setEditingPropIndex] = useState<number | null>(null)
  const [editProp, setEditProp] = useState<PropForm>(EMPTY_PROP)

  // Multi-select panel
  const multiSelected = (selectedNodeIds ?? []).filter((id) => nodes.some((n) => n.id === id))

  if (multiSelected.length > 1) {
    return (
      <MultiSelectPanel
        nodeIds={multiSelected}
        nodes={nodes}
        groupName={groupName}
        setGroupName={setGroupName}
        creatingGroup={creatingGroup}
        setCreatingGroup={setCreatingGroup}
        onCreateGroup={(name) => { createGroup(multiSelected, name); setGroupName(''); setCreatingGroup(false) }}
        onClose={() => setSelectedNode(null)}
      />
    )
  }

  const node = nodes.find((n) => n.id === selectedNodeId)
  if (!node || node.data.type === 'groupRect') return null

  // Group detail panel
  if (node.data.type === 'group') {
    return (
      <GroupDetailPanel
        node={node}
        nodes={nodes}
        onUngroup={() => { ungroup(node.id) }}
        onRemoveChild={(id) => { snapshotHistory(); removeFromGroup(node.id, id) }}
        onChangeDescription={(value) => updateNode(node.id, { notes: value })}
        onSnapshotBeforeEdit={snapshotHistory}
        onToggleBorder={() => {
          snapshotHistory()
          updateNode(node.id, {
            custom_colors: {
              ...node.data.custom_colors,
              show_border: !(node.data.custom_colors?.show_border !== false),
            },
          })
        }}
        onClose={() => setSelectedNode(null)}
        onSelectChild={(id) => setSelectedNode(id)}
      />
    )
  }

  // Normal single-node panel
  const addingService = addingForNode === node.id
  const editingIndex = editingFor?.nodeId === node.id ? editingFor.index : null
  const { data } = node
  const services = data.services ?? []
  const statusColor = STATUS_COLORS[data.status]
  const ipAddresses = data.ip ? splitIps(data.ip) : []
  const host = ipAddresses[0] ?? data.hostname

  const handleDelete = () => {
    if (confirm(`Delete "${data.label}"?`)) {
      snapshotHistory()
      deleteNode(node.id)
    }
  }

  const handleAddService = () => {
    const trimmedPort = newSvc.port.trim()
    const port = trimmedPort === '' ? undefined : parseInt(trimmedPort, 10)
    if (!newSvc.service_name.trim()) return
    if (trimmedPort !== '' && (port == null || Number.isNaN(port) || port < 1 || port > 65535)) return
    snapshotHistory()
    const path = newSvc.path.trim()
    const svc: ServiceInfo = {
      ...(port != null ? { port } : {}),
      protocol: newSvc.protocol,
      service_name: newSvc.service_name.trim(),
      ...(path ? { path } : {}),
    }
    updateNode(node.id, { services: [...services, svc] })
    setNewSvc(EMPTY_FORM)
    setAddingForNode(null)
  }

  const handleRemoveService = (index: number) => {
    snapshotHistory()
    const updated = services.filter((_, i) => i !== index)
    updateNode(node.id, { services: updated })
    if (editingIndex === index) setEditingFor(null)
  }

  const handleStartEdit = (index: number) => {
    const svc = services[index]
    if (!svc) return
    setEditSvc({ port: svc.port != null ? String(svc.port) : '', protocol: svc.protocol, service_name: svc.service_name, path: svc.path ?? '' })
    setEditingFor({ nodeId: node.id, index })
    setAddingForNode(null)
  }

  const handleSaveEdit = () => {
    if (editingIndex === null) return
    const trimmedPort = editSvc.port.trim()
    const port = trimmedPort === '' ? undefined : parseInt(trimmedPort, 10)
    if (!editSvc.service_name.trim()) return
    if (trimmedPort !== '' && (port == null || Number.isNaN(port) || port < 1 || port > 65535)) return
    snapshotHistory()
    const path = editSvc.path.trim()
    const updated = services.map((svc, i) =>
      i === editingIndex
        ? {
            ...svc,
            protocol: editSvc.protocol,
            service_name: editSvc.service_name.trim(),
            ...(port != null ? { port } : { port: undefined }),
            ...(path ? { path } : { path: undefined }),
          }
        : svc
    )
    updateNode(node.id, { services: updated })
    setEditingFor(null)
  }

  // --- Property handlers ---
  const properties: NodeProperty[] = data.properties ?? []

  const handleAddProp = () => {
    if (!newProp.key.trim() || !newProp.value.trim()) return
    snapshotHistory()
    const prop: NodeProperty = { key: newProp.key.trim(), value: newProp.value.trim(), icon: newProp.icon, visible: newProp.visible }
    updateNode(node.id, { properties: [...properties, prop] })
    setNewProp(EMPTY_PROP)
    setAddingProp(false)
  }

  const handleRemoveProp = (index: number) => {
    snapshotHistory()
    updateNode(node.id, { properties: properties.filter((_, i) => i !== index) })
    if (editingPropIndex === index) setEditingPropIndex(null)
  }

  const handleTogglePropVisible = (index: number) => {
    snapshotHistory()
    updateNode(node.id, {
      properties: properties.map((p, i) => i === index ? { ...p, visible: !p.visible } : p),
    })
  }

  const handleStartEditProp = (index: number) => {
    const p = properties[index]
    if (!p) return
    setEditProp({ key: p.key, value: p.value, icon: p.icon, visible: p.visible })
    setEditingPropIndex(index)
    setAddingProp(false)
  }

  const handleSaveEditProp = () => {
    if (editingPropIndex === null || !editProp.key.trim() || !editProp.value.trim()) return
    snapshotHistory()
    updateNode(node.id, {
      properties: properties.map((p, i) =>
        i === editingPropIndex
          ? { key: editProp.key.trim(), value: editProp.value.trim(), icon: editProp.icon, visible: editProp.visible }
          : p
      ),
    })
    setEditingPropIndex(null)
  }

  return (
    <aside className="w-72 shrink-0 flex flex-col border-l border-border bg-[#161b22] overflow-y-auto">
      <div className="flex items-center justify-between px-4 py-3 border-b border-border">
        <span className="font-semibold text-sm text-foreground truncate">{data.label}</span>
        <button aria-label="Close panel" onClick={() => setSelectedNode(null)} className="text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
          <X size={16} />
        </button>
      </div>

      <div className="flex items-center gap-2 px-4 py-3 border-b border-border">
        <div className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: statusColor }} />
        <span className="text-sm capitalize" style={{ color: statusColor }}>{data.status}</span>
        {data.response_time_ms !== undefined && (
          <span className="ml-auto font-mono text-xs text-muted-foreground">{data.response_time_ms}ms</span>
        )}
      </div>

      <div className="flex flex-col gap-3 px-4 py-3 text-sm">
        <DetailRow label="Type" value={NODE_TYPE_LABELS[data.type]} />
        {data.hostname && (
          <div className="flex justify-between gap-2 items-baseline">
            <span className="text-muted-foreground text-xs shrink-0">Hostname</span>
            <a href={`http://${data.hostname}`} target="_blank" rel="noopener noreferrer" className="text-xs font-mono text-[#00d4ff] hover:underline truncate flex items-center gap-1" title={data.hostname}>
              {data.hostname}<ExternalLink size={10} className="shrink-0" />
            </a>
          </div>
        )}
        {ipAddresses.length > 0 && (
          <div className="flex justify-between gap-2 items-start">
            <span className="text-muted-foreground text-xs shrink-0">{ipAddresses.length > 1 ? 'IP Addresses' : 'IP Address'}</span>
            <div className="flex flex-wrap justify-end items-center gap-x-2 gap-y-1 max-w-[65%]">
              {ipAddresses.map((ip, index) => (
                <span key={`${ip}-${index}`} className="inline-flex items-center shrink-0 whitespace-nowrap">
                  <a
                    href={`http://${ip}`}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="text-xs font-mono text-[#00d4ff] hover:underline inline-flex items-center gap-1"
                    title={ip}
                  >
                    {ip}
                    <ExternalLink size={10} className="shrink-0" />
                  </a>
                </span>
              ))}
            </div>
          </div>
        )}
        {data.mac && <DetailRow label="MAC" value={data.mac} mono />}
        {data.os && <DetailRow label="OS" value={data.os} />}
        {data.check_method && <DetailRow label="Check" value={data.check_method} mono />}
        {data.last_seen && <DetailRow label="Last Seen" value={formatTimestamp(data.last_seen)} />}
        {data.last_scan && <DetailRow label="Last Scan" value={formatTimestamp(data.last_scan)} />}
        {data.created_at && <DetailRow label="Created" value={formatTimestamp(data.created_at)} />}
        {data.updated_at && <DetailRow label="Last Modified" value={formatTimestamp(data.updated_at)} />}
      </div>

      {/* Size section — manual width/height entry for pixel-exact sizing */}
      <SizeFields
        key={node.id}
        node={node}
        onCommit={(size) => { snapshotHistory(); setNodeSize(node.id, size) }}
      />

      {/* Properties section */}
      <div className="px-4 py-3 border-t border-border">
        <div className="flex items-center justify-between mb-2">
          <span className="text-xs text-muted-foreground">Properties{properties.length > 0 ? ` (${properties.length})` : ''}</span>
          <button
            onClick={() => { setAddingProp((v) => !v); setEditingPropIndex(null) }}
            className="flex items-center gap-1 text-[10px] text-[#00d4ff] hover:text-[#00d4ff]/80 transition-colors cursor-pointer"
          >
            <Plus size={10} /> Add
          </button>
        </div>
        {addingProp && (
          <PropertyForm
            form={newProp}
            onChange={setNewProp}
            onConfirm={handleAddProp}
            onCancel={() => { setAddingProp(false); setNewProp(EMPTY_PROP) }}
            confirmLabel="Add"
          />
        )}
        {properties.length > 0 && (
          <div className="flex flex-col gap-1.5">
            {properties.map((prop, i) =>
              editingPropIndex === i ? (
                <PropertyForm
                  key={`edit-${i}`}
                  form={editProp}
                  onChange={setEditProp}
                  onConfirm={handleSaveEditProp}
                  onCancel={() => setEditingPropIndex(null)}
                  confirmLabel="Save"
                />
              ) : (
                <PropertyBadge
                  key={`${prop.key}-${i}`}
                  prop={prop}
                  onToggleVisible={() => handleTogglePropVisible(i)}
                  onEdit={() => handleStartEditProp(i)}
                  onRemove={() => handleRemoveProp(i)}
                />
              )
            )}
          </div>
        )}
        {properties.length === 0 && !addingProp && (
          <p className="text-[10px] text-muted-foreground/50">No properties — click Add to define one.</p>
        )}
      </div>

      <div className="px-4 py-3 border-t border-border">
        <div className="flex items-center justify-between mb-2">
          <span className="text-xs text-muted-foreground">Services{services.length > 0 ? ` (${services.length})` : ''}</span>
          <button onClick={() => { setAddingForNode((v) => v === node.id ? null : node.id); setEditingFor(null) }} className="flex items-center gap-1 text-[10px] text-[#00d4ff] hover:text-[#00d4ff]/80 transition-colors cursor-pointer">
            <Plus size={10} /> Add
          </button>
        </div>
        {addingService && <ServiceForm form={newSvc} onChange={setNewSvc} onConfirm={handleAddService} onCancel={() => setAddingForNode(null)} confirmLabel="Add" autoFocus />}
        {services.length > 0 && (
          <div className="flex flex-col gap-1.5">
            {services.map((svc, i) =>
              editingIndex === i ? (
                <ServiceForm key={`edit-${i}`} form={editSvc} onChange={setEditSvc} onConfirm={handleSaveEdit} onCancel={() => setEditingFor(null)} confirmLabel="Save" autoFocus />
              ) : (
                <ServiceBadge key={`${svc.port ?? 'host'}-${svc.protocol}-${svc.path ?? ''}-${i}`} svc={svc} host={host} status={serviceStatuses[serviceStatusKey(node.id, svc.port, svc.protocol)]} onEdit={() => handleStartEdit(i)} onRemove={() => handleRemoveService(i)} />
              )
            )}
          </div>
        )}
        {services.length === 0 && !addingService && <p className="text-[10px] text-muted-foreground/50">No services — click Add to register one.</p>}
      </div>

      {data.notes && (
        <div className="px-4 py-3 border-t border-border">
          <div className="text-xs text-muted-foreground mb-1">Notes</div>
          <p className="text-xs text-foreground/80 whitespace-pre-wrap">{data.notes}</p>
        </div>
      )}

      <div className="mt-auto flex gap-2 px-4 py-3 border-t border-border">
        <Button size="sm" variant="secondary" className="flex-1 gap-1.5 cursor-pointer" onClick={() => onEdit(node.id)}>
          <Edit size={14} /> Edit
        </Button>
        <Button size="sm" variant="destructive" className="gap-1.5 cursor-pointer" aria-label="Delete node" onClick={handleDelete}>
          <Trash2 size={14} />
        </Button>
      </div>
    </aside>
  )
}

// --- Size fields (manual width/height entry) ---

const round = (v: number | undefined): string => (v != null ? String(Math.round(v)) : '')

function SizeFields({ node, onCommit }: { node: Node<NodeData>; onCommit: (size: { width?: number; height?: number }) => void }) {
  // Live size from the explicit dimension, falling back to the DOM-measured
  // size so the field shows the node's current footprint even before resize.
  const liveWidth = round(node.width ?? node.measured?.width)
  const liveHeight = round(node.height ?? node.measured?.height)
  const [width, setWidth] = useState(liveWidth)
  const [height, setHeight] = useState(liveHeight)

  // Resync the fields when the node is resized by dragging its corner handle
  // (which mutates node.width/height in the store). Adjusting state during
  // render — the React-blessed alternative to an effect — keeps it in step
  // without cascading renders. The field the user is actively editing is left
  // alone so we don't clobber their keystrokes mid-type.
  const [editing, setEditing] = useState<'width' | 'height' | null>(null)
  const [prev, setPrev] = useState({ w: liveWidth, h: liveHeight })
  if (prev.w !== liveWidth || prev.h !== liveHeight) {
    setPrev({ w: liveWidth, h: liveHeight })
    if (editing !== 'width') setWidth(liveWidth)
    if (editing !== 'height') setHeight(liveHeight)
  }

  const commit = (raw: string, axis: 'width' | 'height') => {
    const n = Number(raw)
    if (!raw.trim() || Number.isNaN(n) || n <= 0) {
      // Reset the field to the live value on invalid input — no mutation.
      if (axis === 'width') setWidth(round(node.width ?? node.measured?.width))
      else setHeight(round(node.height ?? node.measured?.height))
      return
    }
    onCommit({ [axis]: n })
  }

  return (
    <div className="px-4 py-3 border-t border-border">
      <span className="text-xs text-muted-foreground">Size</span>
      <div className="mt-2 flex items-center gap-2">
        <label className="flex flex-1 items-center gap-1.5">
          <span className="text-[10px] text-muted-foreground/70 w-3">W</span>
          <Input
            type="number"
            min={140}
            aria-label="Width"
            value={width}
            onFocus={() => setEditing('width')}
            onChange={(e) => setWidth(e.target.value)}
            onBlur={() => { setEditing(null); commit(width, 'width') }}
            onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur() }}
            className="h-7 text-xs font-mono"
          />
        </label>
        <label className="flex flex-1 items-center gap-1.5">
          <span className="text-[10px] text-muted-foreground/70 w-3">H</span>
          <Input
            type="number"
            min={50}
            aria-label="Height"
            value={height}
            onFocus={() => setEditing('height')}
            onChange={(e) => setHeight(e.target.value)}
            onBlur={() => { setEditing(null); commit(height, 'height') }}
            onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur() }}
            className="h-7 text-xs font-mono"
          />
        </label>
      </div>
    </div>
  )
}

// --- Multi-select panel ---

interface MultiSelectPanelProps {
  nodeIds: string[]
  nodes: Node<NodeData>[]
  groupName: string
  setGroupName: (v: string) => void
  creatingGroup: boolean
  setCreatingGroup: (v: boolean) => void
  onCreateGroup: (name: string) => void
  onClose: () => void
}

function MultiSelectPanel({ nodeIds, nodes, groupName, setGroupName, creatingGroup, setCreatingGroup, onCreateGroup, onClose }: MultiSelectPanelProps) {
  const selectedNodes = nodeIds.map((id) => nodes.find((n) => n.id === id)).filter(Boolean) as Node<NodeData>[]

  const handleCreate = () => {
    const name = groupName.trim() || 'Group'
    onCreateGroup(name)
  }

  return (
    <aside className="w-72 shrink-0 flex flex-col border-l border-border bg-[#161b22] overflow-y-auto">
      <div className="flex items-center justify-between px-4 py-3 border-b border-border">
        <div className="flex items-center gap-2">
          <Layers size={14} className="text-[#00d4ff]" />
          <span className="font-semibold text-sm text-foreground">{nodeIds.length} nodes selected</span>
        </div>
        <button aria-label="Close panel" onClick={onClose} className="text-muted-foreground hover:text-foreground transition-colors">
          <X size={16} />
        </button>
      </div>

      <div className="flex-1 px-4 py-3 space-y-1.5 overflow-y-auto">
        {selectedNodes.map((n) => (
          <div key={n.id} className="flex items-center gap-2 px-2 py-1.5 rounded-md bg-[#21262d] text-xs">
            <span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ backgroundColor: STATUS_COLORS[n.data.status] }} />
            <span className="truncate text-foreground font-medium">{n.data.label}</span>
            <span className="ml-auto text-muted-foreground shrink-0">{NODE_TYPE_LABELS[n.data.type] ?? n.data.type}</span>
          </div>
        ))}
      </div>

      <div className="px-4 py-3 border-t border-border space-y-2">
        {creatingGroup ? (
          <>
            <Input
              autoFocus
              placeholder="Group name…"
              value={groupName}
              onChange={(e) => setGroupName(e.target.value)}
              onKeyDown={(e) => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') setCreatingGroup(false) }}
              className="bg-[#21262d] border-[#30363d] text-xs h-7"
            />
            <div className="flex gap-2">
              <Button size="sm" className="flex-1 h-7 text-[10px] bg-[#00d4ff] text-[#0d1117] hover:bg-[#00d4ff]/90" onClick={handleCreate}>
                Create Group
              </Button>
              <Button size="sm" variant="ghost" className="h-7 text-[10px]" onClick={() => setCreatingGroup(false)}>
                Cancel
              </Button>
            </div>
          </>
        ) : (
          <Button
            size="sm"
            className="w-full gap-2 bg-[#00d4ff]/10 text-[#00d4ff] border border-[#00d4ff]/30 hover:bg-[#00d4ff]/20"
            variant="ghost"
            onClick={() => setCreatingGroup(true)}
          >
            <Layers size={13} /> Create Group
          </Button>
        )}
      </div>
    </aside>
  )
}

// --- Group detail panel ---

interface GroupDetailPanelProps {
  node: Node<NodeData>
  nodes: Node<NodeData>[]
  onUngroup: () => void
  onRemoveChild: (id: string) => void
  onChangeDescription: (value: string) => void
  onSnapshotBeforeEdit: () => void
  onToggleBorder: () => void
  onClose: () => void
  onSelectChild: (id: string) => void
}

function GroupDetailPanel({ node, nodes, onUngroup, onRemoveChild, onChangeDescription, onSnapshotBeforeEdit, onToggleBorder, onClose, onSelectChild }: GroupDetailPanelProps) {
  const children = nodes.filter((n) => n.parentId === node.id)
  const onlineCount = children.filter((n) => n.data.status === 'online').length
  const offlineCount = children.filter((n) => n.data.status === 'offline').length
  const showBorder = node.data.custom_colors?.show_border !== false

  // Description reuses data.notes, which already round-trips to the backend.
  // Controlled + committed on every keystroke so ANY save path (incl. Ctrl+S,
  // which never blurs the field) captures it. History is snapshotted once at the
  // start of an edit session so the whole edit is a single undo step.
  const snappedRef = useRef(false)
  const handleDescriptionChange = (value: string) => {
    if (!snappedRef.current) {
      onSnapshotBeforeEdit()
      snappedRef.current = true
    }
    onChangeDescription(value)
  }

  const handleUngroup = () => {
    if (confirm(`Ungroup "${node.data.label}"? Nodes will be released to the canvas.`)) {
      onUngroup()
    }
  }

  return (
    <aside className="w-72 shrink-0 flex flex-col border-l border-border bg-[#161b22] overflow-y-auto">
      <div className="flex items-center justify-between px-4 py-3 border-b border-border">
        <div className="flex items-center gap-2 min-w-0">
          <Layers size={14} className="text-[#00d4ff] shrink-0" />
          <span className="font-semibold text-sm text-foreground truncate">{node.data.label}</span>
        </div>
        <button aria-label="Close panel" onClick={onClose} className="text-muted-foreground hover:text-foreground transition-colors shrink-0">
          <X size={16} />
        </button>
      </div>

      {/* Status summary */}
      <div className="flex items-center gap-4 px-4 py-3 border-b border-border text-xs">
        <span className="text-muted-foreground">{children.length} node{children.length !== 1 ? 's' : ''}</span>
        {onlineCount > 0 && <span style={{ color: STATUS_COLORS.online }}>● {onlineCount} online</span>}
        {offlineCount > 0 && <span style={{ color: STATUS_COLORS.offline }}>● {offlineCount} offline</span>}
      </div>

      {/* Description */}
      <div className="px-4 py-3 border-b border-border">
        <label htmlFor="group-description" className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/50">
          Description
        </label>
        <textarea
          id="group-description"
          value={node.data.notes ?? ''}
          onFocus={() => { snappedRef.current = false }}
          onChange={(e) => handleDescriptionChange(e.target.value)}
          placeholder="Add a description for this group…"
          rows={3}
          className="mt-1.5 w-full resize-y rounded-md bg-[#21262d] border border-[#30363d] px-2 py-1.5 text-xs text-foreground placeholder:text-muted-foreground/40 focus:outline-none focus:border-[#00d4ff]/50"
        />
      </div>

      {/* Children list */}
      <div className="flex-1 px-4 py-3 space-y-1.5 overflow-y-auto">
        <span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/50">Members</span>
        {children.length === 0 && <p className="text-xs text-muted-foreground/50">No nodes in this group.</p>}
        {children.map((child) => (
          <div
            key={child.id}
            className="group/member w-full flex items-center gap-2 px-2 py-1.5 rounded-md bg-[#21262d] text-xs hover:bg-[#30363d] transition-colors"
          >
            <button
              onClick={() => onSelectChild(child.id)}
              className="flex items-center gap-2 min-w-0 flex-1 text-left cursor-pointer"
            >
              <span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ backgroundColor: STATUS_COLORS[child.data.status] }} />
              <span className="truncate text-foreground font-medium">{child.data.label}</span>
              <span className="ml-auto text-muted-foreground shrink-0">{NODE_TYPE_LABELS[child.data.type] ?? child.data.type}</span>
            </button>
            <button
              onClick={() => onRemoveChild(child.id)}
              aria-label={`Remove ${child.data.label} from group`}
              title="Remove from group"
              className="shrink-0 opacity-0 group-hover/member:opacity-100 transition-opacity text-[#8b949e] hover:text-[#f85149] cursor-pointer"
            >
              <X size={12} />
            </button>
          </div>
        ))}
      </div>

      {/* Actions */}
      <div className="px-4 py-3 border-t border-border space-y-2">
        <button
          onClick={onToggleBorder}
          className="w-full flex items-center gap-2 px-3 py-2 rounded-md text-xs text-muted-foreground hover:text-foreground hover:bg-[#21262d] transition-colors"
        >
          {showBorder ? <Eye size={13} /> : <EyeOff size={13} />}
          {showBorder ? 'Hide border & title' : 'Show border & title'}
        </button>
        <Button
          size="sm"
          variant="destructive"
          className="w-full gap-2"
          onClick={handleUngroup}
        >
          <Ungroup size={13} /> Ungroup
        </Button>
      </div>
    </aside>
  )
}

// --- Helpers ---

function DetailRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
  return (
    <div className="flex justify-between gap-2 items-baseline">
      <span className="text-muted-foreground text-xs shrink-0">{label}</span>
      <span className={`text-xs text-right truncate ${mono ? 'font-mono text-[#00d4ff]' : 'text-foreground'}`} title={value}>
        {value}
      </span>
    </div>
  )
}

function ServiceForm({ form, onChange, onConfirm, onCancel, confirmLabel, autoFocus }: {
  form: { port: string; protocol: 'tcp' | 'udp'; service_name: string; path: string }
  onChange: (f: { port: string; protocol: 'tcp' | 'udp'; service_name: string; path: string }) => void
  onConfirm: () => void
  onCancel: () => void
  confirmLabel: string
  autoFocus?: boolean
}) {
  const setPort = (value: string) => {
    const digitsOnly = value.replace(/\D/g, '').slice(0, 5)
    onChange({ ...form, port: digitsOnly })
  }

  const clampPort = (value: string) => {
    if (!value) return ''
    const parsed = Number.parseInt(value, 10)
    if (!Number.isFinite(parsed)) return ''
    return String(Math.max(1, Math.min(65535, parsed)))
  }

  return (
    <div className="flex flex-col gap-1.5 mb-1 p-2 rounded-md bg-[#0d1117] border border-[#30363d]">
      <Input value={form.service_name} onChange={(e) => onChange({ ...form, service_name: e.target.value })} placeholder="Service name" className="bg-[#21262d] border-[#30363d] text-xs h-7" autoFocus={autoFocus} onKeyDown={(e) => e.key === 'Enter' && onConfirm()} />
      <div className="flex gap-1.5">
        <Input
          type="text"
          inputMode="numeric"
          pattern="[0-9]*"
          value={form.port}
          onChange={(e) => setPort(e.target.value)}
          onBlur={() => onChange({ ...form, port: clampPort(form.port) })}
          placeholder="Port"
          className="bg-[#21262d] border-[#30363d] font-mono text-xs h-7 w-28 shrink-0"
          onKeyDown={(e) => e.key === 'Enter' && onConfirm()}
        />
        <select value={form.protocol} onChange={(e) => onChange({ ...form, protocol: e.target.value as 'tcp' | 'udp' })} className="flex-1 bg-[#21262d] border border-[#30363d] rounded-md text-xs h-7 px-1.5 text-foreground">
          <option value="tcp">tcp</option>
          <option value="udp">udp</option>
        </select>
      </div>
      <Input value={form.path} onChange={(e) => onChange({ ...form, path: e.target.value })} placeholder="Path (/admin)" className="bg-[#21262d] border-[#30363d] font-mono text-xs h-7" onKeyDown={(e) => e.key === 'Enter' && onConfirm()} />
      <div className="flex gap-1.5">
        <Button size="sm" className="flex-1 h-6 text-[10px] bg-[#00d4ff] text-[#0d1117] hover:bg-[#00d4ff]/90" onClick={onConfirm}>{confirmLabel}</Button>
        <Button size="sm" variant="ghost" className="h-6 text-[10px]" onClick={onCancel}>Cancel</Button>
      </div>
    </div>
  )
}

// --- Property components ---

function PropertyForm({ form, onChange, onConfirm, onCancel, confirmLabel }: {
  form: PropForm
  onChange: (f: PropForm) => void
  onConfirm: () => void
  onCancel: () => void
  confirmLabel: string
}) {
  return (
    <div className="flex flex-col gap-1.5 mb-1 p-2 rounded-md bg-[#0d1117] border border-[#30363d]">
      <Input
        value={form.key}
        onChange={(e) => onChange({ ...form, key: e.target.value })}
        placeholder="Label (e.g. CPU Model)"
        className="bg-[#21262d] border-[#30363d] text-xs h-7"
        autoFocus
        onKeyDown={(e) => e.key === 'Enter' && onConfirm()}
      />
      <Input
        value={form.value}
        onChange={(e) => onChange({ ...form, value: e.target.value })}
        placeholder="Value (e.g. i7-12700K)"
        className="bg-[#21262d] border-[#30363d] text-xs h-7"
        onKeyDown={(e) => e.key === 'Enter' && onConfirm()}
      />
      {/* Icon picker */}
      <div className="flex flex-wrap gap-1 pt-0.5">
        <button
          onClick={() => onChange({ ...form, icon: null })}
          title="No icon"
          className={`w-6 h-6 rounded flex items-center justify-center text-[10px] border transition-colors ${
            form.icon === null ? 'border-[#00d4ff] bg-[#00d4ff]/10 text-[#00d4ff]' : 'border-[#30363d] text-muted-foreground hover:border-[#8b949e]'
          }`}
        >
          –
        </button>
        {PROPERTY_ICON_NAMES.map((name) => {
          const Icon = PROPERTY_ICONS[name]
          const active = form.icon === name
          return (
            <button
              key={name}
              onClick={() => onChange({ ...form, icon: name })}
              title={name}
              className={`w-6 h-6 rounded flex items-center justify-center border transition-colors ${
                active ? 'border-[#00d4ff] bg-[#00d4ff]/10 text-[#00d4ff]' : 'border-[#30363d] text-muted-foreground hover:border-[#8b949e]'
              }`}
            >
              {createElement(Icon, { size: 11 })}
            </button>
          )
        })}
      </div>
      {/* Visible toggle */}
      <label className="flex items-center gap-2 cursor-pointer pt-0.5">
        <input
          type="checkbox"
          checked={form.visible}
          onChange={(e) => onChange({ ...form, visible: e.target.checked })}
          className="accent-[#00d4ff] w-3 h-3"
        />
        <span className="text-[10px] text-muted-foreground">Show on node</span>
      </label>
      <div className="flex gap-1.5">
        <Button size="sm" className="flex-1 h-6 text-[10px] bg-[#00d4ff] text-[#0d1117] hover:bg-[#00d4ff]/90" onClick={onConfirm}>
          {confirmLabel}
        </Button>
        <Button size="sm" variant="ghost" className="h-6 text-[10px]" onClick={onCancel}>Cancel</Button>
      </div>
    </div>
  )
}

function PropertyBadge({ prop, onToggleVisible, onEdit, onRemove }: {
  prop: NodeProperty
  onToggleVisible: () => void
  onEdit: () => void
  onRemove: () => void
}) {
  const Icon = resolvePropertyIcon(prop.icon)
  return (
    <div className="group flex items-center justify-between gap-2 px-2 py-1.5 rounded-md border text-xs transition-colors" style={{ background: '#21262d', borderColor: '#30363d' }}>
      <div className="flex items-center gap-1.5 min-w-0">
        {Icon && createElement(Icon, { size: 11, className: 'shrink-0 text-muted-foreground' })}
        <span className="font-medium truncate text-foreground" title={prop.key}>{prop.key}</span>
        <span className="text-muted-foreground truncate" title={prop.value}>· {prop.value}</span>
      </div>
      <div className="flex items-center gap-1 shrink-0">
        <button
          onClick={onToggleVisible}
          title={prop.visible ? 'Hide on node' : 'Show on node'}
          className="text-[#8b949e] hover:text-[#00d4ff] transition-colors"
        >
          {prop.visible ? <Eye size={10} /> : <EyeOff size={10} />}
        </button>
        <button onClick={onEdit} className="opacity-0 group-hover:opacity-100 transition-opacity text-[#8b949e] hover:text-[#00d4ff]" title="Edit property">
          <Pencil size={10} />
        </button>
        <button onClick={onRemove} className="opacity-0 group-hover:opacity-100 transition-opacity text-[#8b949e] hover:text-[#f85149]" title="Remove property">
          <X size={10} />
        </button>
      </div>
    </div>
  )
}

const CATEGORY_COLORS: Record<string, string> = {
  web: '#00d4ff', database: '#a855f7', monitoring: '#39d353', storage: '#e3b341', security: '#f85149', remote: '#8b949e',
}

function ServiceBadge({ svc, host, status, onEdit, onRemove }: { svc: ServiceInfo; host?: string; status?: ServiceStatus; onEdit: () => void; onRemove: () => void }) {
  const url = getServiceUrl(svc, host)
  // Manually-added services carry no category, so they fell back to grey even
  // when they're reachable HTTP/HTTPS. Treat any resolvable web URL as `web`.
  const categoryColor = CATEGORY_COLORS[svc.category ?? ''] ?? (url ? CATEGORY_COLORS.web : '#8b949e')
  // A live offline service overrides the category colour with red.
  const color = status === 'offline' ? '#f85149' : categoryColor
  const pathLabel = svc.path?.trim() ? svc.path.trim() : ''

  return (
    <div
      className="group flex items-center justify-between gap-2 px-2 py-1.5 rounded-md border text-xs transition-colors min-w-0"
      style={{ background: '#21262d', borderColor: '#30363d' }}
    >
      <div className="flex items-center gap-1.5 min-w-0 flex-1">
        <span className="shrink-0 w-1.5 h-1.5 rounded-full" style={{ backgroundColor: color }} />

        {url ? (
          <a
            href={url}
            target="_blank"
            rel="noopener noreferrer"
            className="font-medium truncate min-w-0 flex-1"
            style={{ color }}
            title={svc.service_name}
            onClick={e => e.stopPropagation()}
          >
            {svc.service_name}
          </a>
        ) : (
          <span
            className="font-medium truncate min-w-0 flex-1"
            style={{ color }}
            title={svc.service_name}
          >
            {svc.service_name}
          </span>
        )}

        {pathLabel && (
          <span
            className="shrink-0 text-[#8b949e] text-right w-16 truncate"
            title={pathLabel}
          >
            {pathLabel}
          </span>
        )}
      </div>

      <div className="flex items-center gap-1.5 shrink-0">
        {svc.port != null && (
          <span className="font-mono text-[#8b949e]">
            {svc.port}/{svc.protocol}
          </span>
        )}

        {url ? (
          <a
            href={url}
            target="_blank"
            rel="noopener noreferrer"
            className="inline-flex w-2.5 h-2.5 items-center justify-center"
            onClick={e => e.stopPropagation()}
          >
            <ExternalLink size={10} className="text-muted-foreground" />
          </a>
        ) : (
          <span className="w-2.5" />
        )}

        <button
          onClick={(e) => { e.preventDefault(); e.stopPropagation(); onEdit() }}
          className="opacity-0 group-hover:opacity-100 transition-opacity text-[#8b949e] hover:text-[#00d4ff] ml-0.5 cursor-pointer"
          title="Edit service"
        >
          <Pencil size={10} />
        </button>

        <button
          onClick={(e) => { e.preventDefault(); e.stopPropagation(); onRemove() }}
          className="opacity-0 group-hover:opacity-100 transition-opacity text-[#8b949e] hover:text-[#f85149] ml-0.5 cursor-pointer"
          title="Remove service"
        >
          <X size={10} />
        </button>
      </div>
    </div>
  )
}
