import { BuildOrganizationGraphInputSchema, OrganizationGraphSchema } from './schema'
import type { CommandViewData } from '../../platform/registry/command-view'
import type {
  BuildOrganizationGraphInput,
  OrganizationGraph,
  OrganizationGraphEdge,
  OrganizationGraphEdgeKind,
  OrganizationGraphNode,
  OrganizationGraphNodeKind
} from './types'
import type { ActionInvocation } from '../domains/actions'
import type { Entity } from '../domains/entities'
import type { ResourceEntry } from '../domains/resources'
import type { OmTopologyNodeRef } from '../domains/topology'
import type { EventDescriptor, OrganizationModelSidebarNode } from '../types'
import {
  getAllPipelines,
  getAllBuildTemplates,
  getLeadGenStageCatalog,
  getAllProjectStatuses,
  getAllProspectingStages
} from '../migration-helpers'
import { listAllSystems } from '../helpers'
import {
  compileOrganizationOntology,
  listResolvedOntologyRecords,
  ontologyGraphNodeId,
  parseOntologyId,
  type OntologyActionType,
  type OntologyCatalogType,
  type OntologyGroup,
  type OntologyId,
  type OntologyLinkType
} from '../ontology'

type EventEmissionDescriptor = NonNullable<Extract<ResourceEntry, { kind: 'workflow' }>['emits']>[number]

function nodeId(kind: OrganizationGraphNodeKind, sourceId?: string): string {
  return kind === 'organization' ? 'organization-model' : `${kind}:${sourceId ?? ''}`
}

function edgeId(kind: string, sourceId: string, targetId: string, variant?: string): string {
  return variant ? `edge:${kind}:${variant}:${sourceId}:${targetId}` : `edge:${kind}:${sourceId}:${targetId}`
}

function pushUniqueNode(nodes: OrganizationGraphNode[], seen: Set<string>, node: OrganizationGraphNode): void {
  if (seen.has(node.id)) return
  seen.add(node.id)
  nodes.push(node)
}

function pushUniqueEdge(edges: OrganizationGraphEdge[], seen: Set<string>, edge: OrganizationGraphEdge): void {
  if (seen.has(edge.id)) return
  seen.add(edge.id)
  edges.push(edge)
}

function collectSidebarGraphNodes(
  nodes: Record<string, OrganizationModelSidebarNode>,
  groups: Array<{
    id: string
    node: Extract<OrganizationModelSidebarNode, { type: 'group' }>
    parentGroupId?: string
  }>,
  surfaces: Array<{
    id: string
    node: Extract<OrganizationModelSidebarNode, { type: 'surface' }>
    parentGroupId?: string
  }>,
  parentGroupId?: string
): void {
  Object.entries(nodes)
    .sort(([leftId, left], [rightId, right]) => {
      const orderDelta = (left.order ?? Number.MAX_SAFE_INTEGER) - (right.order ?? Number.MAX_SAFE_INTEGER)
      return orderDelta === 0 ? leftId.localeCompare(rightId) : orderDelta
    })
    .forEach(([id, node]) => {
      if (node.type === 'group') {
        groups.push({ id, node, ...(parentGroupId !== undefined ? { parentGroupId } : {}) })
        collectSidebarGraphNodes(node.children, groups, surfaces, id)
        return
      }

      surfaces.push({ id, node, ...(parentGroupId !== undefined ? { parentGroupId } : {}) })
    })
}

function upsertResourceNode(
  nodes: OrganizationGraphNode[],
  seen: Set<string>,
  resourceNodesById: Map<string, OrganizationGraphNode>,
  node: OrganizationGraphNode
): OrganizationGraphNode {
  const existing = resourceNodesById.get(node.id)
  if (existing) {
    if (!existing.label || existing.label === existing.sourceId) {
      existing.label = node.label
    }
    if (!existing.description && node.description) {
      existing.description = node.description
    }
    if (!existing.sourceId && node.sourceId) {
      existing.sourceId = node.sourceId
    }
    if (!existing.resourceType && node.resourceType) {
      existing.resourceType = node.resourceType
    }
    return existing
  }

  resourceNodesById.set(node.id, node)
  pushUniqueNode(nodes, seen, node)
  return node
}

function ensureResourceNode(
  nodes: OrganizationGraphNode[],
  seen: Set<string>,
  resourceNodesById: Map<string, OrganizationGraphNode>,
  resourceId: string
): OrganizationGraphNode {
  const existing = resourceNodesById.get(nodeId('resource', resourceId))
  if (existing) return existing

  return upsertResourceNode(nodes, seen, resourceNodesById, {
    id: nodeId('resource', resourceId),
    kind: 'resource',
    label: resourceId,
    sourceId: resourceId
  })
}

type CommandViewResource =
  | CommandViewData['workflows'][number]
  | CommandViewData['agents'][number]
  | CommandViewData['triggers'][number]
  | CommandViewData['integrations'][number]
  | CommandViewData['externalResources'][number]
  | CommandViewData['humanCheckpoints'][number]

function normalizeCommandViewResourceType(
  resourceType: CommandViewResource['type']
): OrganizationGraphNode['resourceType'] {
  return resourceType === 'human' ? 'human_checkpoint' : resourceType
}

function normalizeOrganizationModelResourceType(
  resourceType: ResourceEntry['kind']
): OrganizationGraphNode['resourceType'] {
  return resourceType
}

function collectCommandViewResources(commandViewData: CommandViewData): CommandViewResource[] {
  return [
    ...commandViewData.workflows,
    ...commandViewData.agents,
    ...commandViewData.triggers,
    ...commandViewData.integrations,
    ...commandViewData.externalResources,
    ...commandViewData.humanCheckpoints
  ]
}

function invocationSignature(invocation: ActionInvocation): string {
  switch (invocation.kind) {
    case 'slash-command':
      return `${invocation.kind}:${invocation.command}`
    case 'mcp-tool':
      return `${invocation.kind}:${invocation.server}:${invocation.name}`
    case 'api-endpoint':
      return `${invocation.kind}:${invocation.method}:${invocation.path}`
    case 'script-execution':
      return `${invocation.kind}:${invocation.resourceId}`
  }
}

function invocationLabel(invocation: ActionInvocation): string {
  switch (invocation.kind) {
    case 'slash-command':
      return invocation.command
    case 'mcp-tool':
      return `${invocation.server}.${invocation.name}`
    case 'api-endpoint':
      return `${invocation.method} ${invocation.path}`
    case 'script-execution':
      return `script ${invocation.resourceId}`
  }
}

function eventNodeId(eventId: string): string {
  return nodeId('event', eventId)
}

function ontologyLabel(id: OntologyId, label: string | undefined): string {
  return label ?? parseOntologyId(id).localId
}

function pushOntologyBindingEdges(
  edges: OrganizationGraphEdge[],
  edgeIds: Set<string>,
  resourceNodeId: string,
  kind: OrganizationGraphEdgeKind,
  ids: OntologyId[] | undefined
): void {
  ids?.forEach((ontologyId) => {
    const targetId = ontologyGraphNodeId(ontologyId)
    pushUniqueEdge(edges, edgeIds, {
      id: edgeId(kind, resourceNodeId, targetId),
      kind,
      sourceId: resourceNodeId,
      targetId
    })
  })
}

function buildResourceEventDescriptor(resourceId: string, emission: EventEmissionDescriptor): EventDescriptor {
  return {
    ...emission,
    id: `${resourceId}:${emission.eventKey}`,
    ownerId: resourceId,
    ownerKind: 'resource'
  }
}

function buildEntityEventDescriptor(entity: Entity, eventKey: string, label: string): EventDescriptor {
  return {
    id: `${entity.id}:${eventKey}`,
    ownerId: entity.id,
    ownerKind: 'entity',
    eventKey,
    label
  }
}

function pushEventProjection(
  nodes: OrganizationGraphNode[],
  nodeIds: Set<string>,
  edges: OrganizationGraphEdge[],
  edgeIds: Set<string>,
  eventNodeIdsByEventId: Map<string, string>,
  event: EventDescriptor,
  sourceNodeId: string,
  edgeKind: 'emits' | 'originates_from'
): void {
  const id = eventNodeId(event.id)
  eventNodeIdsByEventId.set(event.id, id)
  pushUniqueNode(nodes, nodeIds, {
    id,
    kind: 'event',
    label: event.label,
    sourceId: event.id
  })
  const sourceId = edgeKind === 'originates_from' ? id : sourceNodeId
  const targetId = edgeKind === 'originates_from' ? sourceNodeId : id
  pushUniqueEdge(edges, edgeIds, {
    id: edgeId(edgeKind, sourceId, targetId),
    kind: edgeKind,
    sourceId,
    targetId
  })
}

export function buildOrganizationGraph(input: BuildOrganizationGraphInput): OrganizationGraph {
  const parsed = BuildOrganizationGraphInputSchema.parse(input)
  const organizationModel = parsed.organizationModel
  const commandViewData = parsed.commandViewData as CommandViewData | undefined

  const nodes: OrganizationGraphNode[] = []
  const edges: OrganizationGraphEdge[] = []
  const nodeIds = new Set<string>()
  const edgeIds = new Set<string>()
  const resourceNodesById = new Map<string, OrganizationGraphNode>()
  const organizationModelResourceIds = new Set(Object.keys(organizationModel.resources))
  const actionIdsByInvocation = new Map<string, string[]>()
  const projectedEventNodeIdsByEventId = new Map<string, string>()
  const ontologyCompilation = compileOrganizationOntology(organizationModel)

  const organizationNode: OrganizationGraphNode = {
    id: nodeId('organization'),
    kind: 'organization',
    label: 'Organization Model'
  }
  pushUniqueNode(nodes, nodeIds, organizationNode)
  const systemsWithPaths = listAllSystems(organizationModel)
  const systemPathByRef = new Map<string, string>()
  for (const { path, system } of systemsWithPaths) {
    systemPathByRef.set(path, path)
    systemPathByRef.set(system.id, path)
  }
  const validSystemRefs = new Set(systemPathByRef.keys())
  const systemNodeId = (systemRef: string) => nodeId('system', systemPathByRef.get(systemRef) ?? systemRef)

  for (const client of Object.values(organizationModel.clients).sort((a, b) => a.slug.localeCompare(b.slug))) {
    const id = nodeId('client', client.id)
    pushUniqueNode(nodes, nodeIds, {
      id,
      kind: 'client',
      label: client.name,
      sourceId: client.id,
      description: client.identity.clientBrief || undefined
    })
    pushUniqueEdge(edges, edgeIds, {
      id: edgeId('contains', organizationNode.id, id),
      kind: 'contains',
      sourceId: organizationNode.id,
      targetId: id
    })
  }

  function topologyNodeId(ref: OmTopologyNodeRef): string {
    if (ref.kind === 'system') return systemNodeId(ref.id)
    if (ref.kind === 'resource') return nodeId('resource', ref.id)
    if (ref.kind === 'ontology') return ontologyGraphNodeId(ref.id)
    if (ref.kind === 'policy') return nodeId('policy', ref.id)
    if (ref.kind === 'role') return nodeId('role', ref.id)
    return nodeId('resource', ref.id)
  }

  function ensureTopologyNode(ref: OmTopologyNodeRef): string {
    const id = topologyNodeId(ref)
    if (nodeIds.has(id)) return id

    if (ref.kind === 'resource') {
      ensureResourceNode(nodes, nodeIds, resourceNodesById, ref.id)
      return id
    }

    if (ref.kind === 'trigger' || ref.kind === 'humanCheckpoint' || ref.kind === 'externalResource') {
      pushUniqueNode(nodes, nodeIds, {
        id,
        kind: 'resource',
        label: ref.id,
        sourceId: ref.id,
        resourceType:
          ref.kind === 'trigger' ? 'trigger' : ref.kind === 'humanCheckpoint' ? 'human_checkpoint' : 'external'
      })
    }

    return id
  }

  for (const { path, system } of systemsWithPaths.sort((a, b) => a.path.localeCompare(b.path))) {
    const id = nodeId('system', path)
    pushUniqueNode(nodes, nodeIds, {
      id,
      kind: 'system',
      label: system.label ?? system.title ?? system.id,
      sourceId: path,
      description: system.description,
      icon: system.ui?.icon ?? system.icon,
      enabled: system.enabled
    })
    pushUniqueEdge(edges, edgeIds, {
      id: edgeId('contains', organizationNode.id, id),
      kind: 'contains',
      sourceId: organizationNode.id,
      targetId: id
    })
    const parentSystemId = path.includes('.') ? path.slice(0, path.lastIndexOf('.')) : undefined
    if (parentSystemId !== undefined) {
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('contains', nodeId('system', parentSystemId), id),
        kind: 'contains',
        sourceId: nodeId('system', parentSystemId),
        targetId: id
      })
    }
  }

  for (const { id: ontologyId, kind, record } of listResolvedOntologyRecords(ontologyCompilation.ontology).sort((a, b) =>
    a.id.localeCompare(b.id)
  )) {
    const id = ontologyGraphNodeId(ontologyId)
    const parsedId = parseOntologyId(ontologyId)
    pushUniqueNode(nodes, nodeIds, {
      id,
      kind: 'ontology',
      label: ontologyLabel(ontologyId, record.label),
      sourceId: ontologyId,
      description: record.description,
      ontologyKind: kind
    })

    const ownerSystemId = record.ownerSystemId ?? (parsedId.isGlobal ? undefined : parsedId.scope)
    if (ownerSystemId !== undefined && validSystemRefs.has(ownerSystemId)) {
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('contains', systemNodeId(ownerSystemId), id, `ontology-${kind}`),
        kind: 'contains',
        sourceId: systemNodeId(ownerSystemId),
        targetId: id
      })
    } else {
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('contains', organizationNode.id, id, `ontology-${kind}`),
        kind: 'contains',
        sourceId: organizationNode.id,
        targetId: id
      })
    }

    if (kind === 'link') {
      const link = record as OntologyLinkType
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('links', ontologyGraphNodeId(link.from), ontologyGraphNodeId(link.to), ontologyId),
        kind: 'links',
        sourceId: ontologyGraphNodeId(link.from),
        targetId: ontologyGraphNodeId(link.to),
        label: link.label ?? parsedId.localId
      })
    }

    if (kind === 'action') {
      const action = record as OntologyActionType
      for (const targetOntologyId of action.actsOn ?? []) {
        pushUniqueEdge(edges, edgeIds, {
          id: edgeId('affects', id, ontologyGraphNodeId(targetOntologyId), ontologyId),
          kind: 'affects',
          sourceId: id,
          targetId: ontologyGraphNodeId(targetOntologyId)
        })
      }
    }

    if (kind === 'catalog') {
      const catalog = record as OntologyCatalogType
      if (catalog.appliesTo === undefined) continue
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('applies_to', id, ontologyGraphNodeId(catalog.appliesTo), ontologyId),
        kind: 'applies_to',
        sourceId: id,
        targetId: ontologyGraphNodeId(catalog.appliesTo)
      })
    }

    if (kind === 'group') {
      const group = record as OntologyGroup
      for (const memberOntologyId of group.members ?? []) {
        pushUniqueEdge(edges, edgeIds, {
          id: edgeId('contains', id, ontologyGraphNodeId(memberOntologyId), ontologyId),
          kind: 'contains',
          sourceId: id,
          targetId: ontologyGraphNodeId(memberOntologyId)
        })
      }
    }
  }

  for (const role of Object.values(organizationModel.roles).sort((a, b) => a.id.localeCompare(b.id))) {
    const id = nodeId('role', role.id)
    pushUniqueNode(nodes, nodeIds, {
      id,
      kind: 'role',
      label: role.title,
      sourceId: role.id,
      description: role.responsibilities.length > 0 ? role.responsibilities.join('; ') : undefined
    })
    pushUniqueEdge(edges, edgeIds, {
      id: edgeId('contains', organizationNode.id, id),
      kind: 'contains',
      sourceId: organizationNode.id,
      targetId: id
    })
    if (role.reportsToId !== undefined) {
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('references', id, nodeId('role', role.reportsToId), 'reports-to'),
        kind: 'references',
        sourceId: id,
        targetId: nodeId('role', role.reportsToId),
        label: 'reports to'
      })
    }
    for (const systemId of role.responsibleFor ?? []) {
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('governs', id, systemNodeId(systemId), 'responsible-for'),
        kind: 'governs',
        sourceId: id,
        targetId: systemNodeId(systemId),
        label: 'responsible for'
      })
    }
  }

  // Phase 4: knowledge is now Record<id, OrgKnowledgeNode> (D3); iterate Object.values.
  for (const node of Object.values(organizationModel.knowledge).sort((a, b) => a.id.localeCompare(b.id))) {
    const id = nodeId('knowledge', node.id)
    pushUniqueNode(nodes, nodeIds, {
      id,
      kind: 'knowledge',
      label: node.title,
      sourceId: node.id,
      description: node.summary,
      icon: node.icon
    })
    pushUniqueEdge(edges, edgeIds, {
      id: edgeId('contains', organizationNode.id, id),
      kind: 'contains',
      sourceId: organizationNode.id,
      targetId: id
    })
    for (const link of node.links) {
      const targetId =
        link.target.kind === 'system'
          ? systemNodeId(link.target.id)
          : link.target.kind === 'ontology'
            ? ontologyGraphNodeId(link.target.id)
            : link.nodeId
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('governs', id, targetId),
        kind: 'governs',
        sourceId: id,
        targetId
      })
    }
  }

  // Phase 4: prospecting domain removed; read stages via migration helper.
  const allStages = [
    ...getAllProspectingStages(organizationModel, 'company'),
    ...getAllProspectingStages(organizationModel, 'contact')
  ].sort((a, b) => a.order - b.order || a.id.localeCompare(b.id))

  for (const stage of allStages) {
    const id = nodeId('stage', stage.id)
    pushUniqueNode(nodes, nodeIds, {
      id,
      kind: 'stage',
      label: stage.label,
      sourceId: stage.id,
      ...(stage.description ? { description: stage.description } : {}),
      ...(stage.icon ? { icon: stage.icon } : {})
    })
    pushUniqueEdge(edges, edgeIds, {
      id: edgeId('contains', organizationNode.id, id),
      kind: 'contains',
      sourceId: organizationNode.id,
      targetId: id
    })
  }

  for (const action of Object.values(organizationModel.actions).sort((a, b) => a.id.localeCompare(b.id))) {
    const id = nodeId('action', action.id)
    pushUniqueNode(nodes, nodeIds, {
      id,
      kind: 'action',
      label: action.label,
      sourceId: action.id,
      description: action.description
    })
    pushUniqueEdge(edges, edgeIds, {
      id: edgeId('contains', organizationNode.id, id),
      kind: 'contains',
      sourceId: organizationNode.id,
      targetId: id
    })
    if (action.resourceId !== undefined) {
      const resourceNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, action.resourceId)
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('maps_to', id, resourceNode.id),
        kind: 'maps_to',
        sourceId: id,
        targetId: resourceNode.id
      })
    }

    for (const entityId of action.affects ?? []) {
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('affects', id, nodeId('entity', entityId)),
        kind: 'affects',
        sourceId: id,
        targetId: nodeId('entity', entityId)
      })
    }

    for (const invocation of action.invocations) {
      const key = invocationSignature(invocation)
      const existing = actionIdsByInvocation.get(key)
      if (existing) {
        existing.push(id)
      } else {
        actionIdsByInvocation.set(key, [id])
      }
    }
  }

  for (const entity of Object.values(organizationModel.entities).sort(
    (a, b) => a.order - b.order || a.id.localeCompare(b.id)
  )) {
    const id = nodeId('entity', entity.id)
    pushUniqueNode(nodes, nodeIds, {
      id,
      kind: 'entity',
      label: entity.label,
      sourceId: entity.id,
      description: entity.description
    })
    pushUniqueEdge(edges, edgeIds, {
      id: edgeId('contains', systemNodeId(entity.ownedBySystemId), id, 'system-entity'),
      kind: 'contains',
      sourceId: systemNodeId(entity.ownedBySystemId),
      targetId: id
    })

    for (const [linkIndex, link] of (entity.links ?? []).entries()) {
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('links', id, nodeId('entity', link.toEntity), `${link.kind}-${linkIndex}`),
        kind: 'links',
        sourceId: id,
        targetId: nodeId('entity', link.toEntity),
        label: link.label ?? link.kind
      })
    }

    if (entity.stateCatalogId !== undefined) {
      const stateEvents: EventDescriptor[] = []

      // Phase 4: model.statuses removed (D1). Status-flow authoring belongs
      // in System.ontology catalogTypes.

      if (entity.stateCatalogId === 'crm.pipeline') {
        // Phase 4: model.sales removed (D8). Read pipelines via migration helper.
        for (const { pipeline } of getAllPipelines(organizationModel)
          .filter(({ pipeline: p }) => p.entityId === entity.id)
          .sort((a, b) => a.pipeline.id.localeCompare(b.pipeline.id))) {
          for (const stage of [...pipeline.stages].sort((a, b) => a.order - b.order || a.id.localeCompare(b.id))) {
            stateEvents.push(buildEntityEventDescriptor(entity, stage.id, stage.label))
          }
        }
      }

      if (entity.stateCatalogId === 'delivery.task') {
        // Phase 4: model.projects removed (D8). Read statuses via migration helper.
        for (const status of getAllProjectStatuses(organizationModel, 'task').sort(
          (a, b) => a.order - b.order || a.id.localeCompare(b.id)
        )) {
          stateEvents.push(buildEntityEventDescriptor(entity, status.id, status.label))
        }
      }

      if (entity.stateCatalogId === 'lead-gen.company' || entity.stateCatalogId === 'lead-gen.contact') {
        const leadGenEntity = entity.stateCatalogId === 'lead-gen.company' ? 'company' : 'contact'
        for (const stage of Object.values(getLeadGenStageCatalog(organizationModel)).sort(
          (a, b) => a.order - b.order || a.key.localeCompare(b.key)
        )) {
          if (stage.entity !== leadGenEntity && !(stage.additionalEntities ?? []).includes(leadGenEntity)) continue
          stateEvents.push(buildEntityEventDescriptor(entity, stage.key, stage.label))
        }
      }

      for (const event of stateEvents) {
        pushEventProjection(
          nodes,
          nodeIds,
          edges,
          edgeIds,
          projectedEventNodeIdsByEventId,
          event,
          id,
          'originates_from'
        )
      }
    }
  }

  for (const { path, system } of systemsWithPaths.sort((a, b) => a.path.localeCompare(b.path))) {
    for (const actionRef of system.actions ?? []) {
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('uses', nodeId('system', path), nodeId('action', actionRef.actionId), actionRef.intent),
        kind: 'uses',
        sourceId: nodeId('system', path),
        targetId: nodeId('action', actionRef.actionId),
        label: actionRef.intent
      })
    }
  }

  // Pre-compute the set of valid system paths so the resource loop below can
  // skip dangling-path edge emission in O(1) per resource.  Belt-and-suspenders
  // against the Wave 2 superRefine; reachable only for partial-OM fixture cases.
  const validSystemPaths = new Set([...validSystemRefs])

  for (const resource of Object.values(organizationModel.resources).sort((a, b) => a.id.localeCompare(b.id))) {
    const resourceNode = upsertResourceNode(nodes, nodeIds, resourceNodesById, {
      id: nodeId('resource', resource.id),
      kind: 'resource',
      label: resource.id,
      sourceId: resource.id,
      resourceType: normalizeOrganizationModelResourceType(resource.kind)
    })

    // Skip the contains edge if the systemPath doesn't resolve to a real system.
    // In a fully-validated model this never fires; it guards partial-OM fixtures.
    if (validSystemPaths.has(resource.systemPath)) {
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('contains', systemNodeId(resource.systemPath), resourceNode.id, 'system-resource'),
        kind: 'contains',
        sourceId: systemNodeId(resource.systemPath),
        targetId: resourceNode.id
      })
    }

    pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'actions', resource.ontology?.actions)
    pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'reads', resource.ontology?.reads)
    pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'writes', resource.ontology?.writes)
    pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'uses_catalog', resource.ontology?.usesCatalogs)
    pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'emits', resource.ontology?.emits)

    if (resource.kind === 'workflow' || resource.kind === 'agent') {
      for (const emission of resource.emits ?? []) {
        pushEventProjection(
          nodes,
          nodeIds,
          edges,
          edgeIds,
          projectedEventNodeIdsByEventId,
          buildResourceEventDescriptor(resource.id, emission),
          resourceNode.id,
          'emits'
        )
      }
    }

    if (resource.kind === 'agent') {
      for (const [index, invocation] of resource.invocations.entries()) {
        const label = invocationLabel(invocation)
        if (invocation.kind === 'script-execution') {
          const targetNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, invocation.resourceId)
          pushUniqueEdge(edges, edgeIds, {
            id: edgeId('uses', resourceNode.id, targetNode.id, `agent-invocation-${index}`),
            kind: 'uses',
            sourceId: resourceNode.id,
            targetId: targetNode.id,
            label
          })
          continue
        }

        for (const actionNodeId of actionIdsByInvocation.get(invocationSignature(invocation)) ?? []) {
          pushUniqueEdge(edges, edgeIds, {
            id: edgeId('references', resourceNode.id, actionNodeId, `agent-invocation-${index}`),
            kind: 'references',
            sourceId: resourceNode.id,
            targetId: actionNodeId,
            label
          })
        }
      }
    }
  }

  for (const policy of Object.values(organizationModel.policies).sort(
    (a, b) => a.order - b.order || a.id.localeCompare(b.id)
  )) {
    const id = nodeId('policy', policy.id)
    pushUniqueNode(nodes, nodeIds, {
      id,
      kind: 'policy',
      label: policy.label,
      sourceId: policy.id,
      description: policy.description
    })
    pushUniqueEdge(edges, edgeIds, {
      id: edgeId('contains', organizationNode.id, id),
      kind: 'contains',
      sourceId: organizationNode.id,
      targetId: id
    })

    for (const systemId of policy.appliesTo.systemIds) {
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('applies_to', id, systemNodeId(systemId), 'system'),
        kind: 'applies_to',
        sourceId: id,
        targetId: systemNodeId(systemId)
      })
    }
    for (const actionId of policy.appliesTo.actionIds) {
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('applies_to', id, nodeId('action', actionId), 'action'),
        kind: 'applies_to',
        sourceId: id,
        targetId: nodeId('action', actionId)
      })
    }
    for (const resourceId of policy.appliesTo.resourceIds) {
      const resourceNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, resourceId)
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('applies_to', id, resourceNode.id, 'resource'),
        kind: 'applies_to',
        sourceId: id,
        targetId: resourceNode.id
      })
    }
    for (const roleId of policy.appliesTo.roleIds) {
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('applies_to', id, nodeId('role', roleId), 'role'),
        kind: 'applies_to',
        sourceId: id,
        targetId: nodeId('role', roleId)
      })
    }

    if (policy.trigger.kind === 'event') {
      const eventNode = projectedEventNodeIdsByEventId.get(policy.trigger.eventId)
      if (eventNode !== undefined) {
        pushUniqueEdge(edges, edgeIds, {
          id: edgeId('triggers', eventNode, id),
          kind: 'triggers',
          sourceId: eventNode,
          targetId: id
        })
      }
    } else if (policy.trigger.kind === 'action-invocation') {
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('triggers', nodeId('action', policy.trigger.actionId), id),
        kind: 'triggers',
        sourceId: nodeId('action', policy.trigger.actionId),
        targetId: id
      })
    }

    for (const [effectIndex, effect] of policy.actions.entries()) {
      if (effect.kind === 'invoke-action') {
        pushUniqueEdge(edges, edgeIds, {
          id: edgeId('effects', id, nodeId('action', effect.actionId), `invoke-action-${effectIndex}`),
          kind: 'effects',
          sourceId: id,
          targetId: nodeId('action', effect.actionId),
          label: 'invoke action'
        })
      }
      if ((effect.kind === 'notify-role' || effect.kind === 'require-approval') && effect.roleId !== undefined) {
        pushUniqueEdge(edges, edgeIds, {
          id: edgeId('effects', id, nodeId('role', effect.roleId), `${effect.kind}-${effectIndex}`),
          kind: 'effects',
          sourceId: id,
          targetId: nodeId('role', effect.roleId),
          label: effect.kind
        })
      }
    }
  }

  for (const [relationshipId, relationship] of Object.entries(organizationModel.topology.relationships).sort(([a], [b]) =>
    a.localeCompare(b)
  )) {
    const sourceId = ensureTopologyNode(relationship.from)
    const targetId = ensureTopologyNode(relationship.to)

    pushUniqueEdge(edges, edgeIds, {
      id: edgeId(relationship.kind, sourceId, targetId, `topology-${relationshipId}`),
      kind: relationship.kind,
      sourceId,
      targetId,
      relationshipType: relationship.kind
    })
  }

  for (const segment of Object.values(organizationModel.customers).sort(
    (a, b) => a.order - b.order || a.id.localeCompare(b.id)
  )) {
    const id = nodeId('customer-segment', segment.id)
    pushUniqueNode(nodes, nodeIds, {
      id,
      kind: 'customer-segment',
      label: segment.name,
      sourceId: segment.id,
      description: segment.description || undefined
    })
    pushUniqueEdge(edges, edgeIds, {
      id: edgeId('contains', organizationNode.id, id),
      kind: 'contains',
      sourceId: organizationNode.id,
      targetId: id
    })
  }

  for (const product of Object.values(organizationModel.offerings).sort(
    (a, b) => a.order - b.order || a.id.localeCompare(b.id)
  )) {
    const id = nodeId('offering', product.id)
    pushUniqueNode(nodes, nodeIds, {
      id,
      kind: 'offering',
      label: product.name,
      sourceId: product.id,
      description: product.description || undefined
    })
    pushUniqueEdge(edges, edgeIds, {
      id: edgeId('contains', organizationNode.id, id),
      kind: 'contains',
      sourceId: organizationNode.id,
      targetId: id
    })
    for (const segmentId of product.targetSegmentIds) {
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('applies_to', id, nodeId('customer-segment', segmentId), 'targets-segment'),
        kind: 'applies_to',
        sourceId: id,
        targetId: nodeId('customer-segment', segmentId),
        label: 'targets'
      })
    }
  }

  for (const objective of Object.values(organizationModel.goals).sort(
    (a, b) => a.order - b.order || a.id.localeCompare(b.id)
  )) {
    const id = nodeId('goal', objective.id)
    pushUniqueNode(nodes, nodeIds, {
      id,
      kind: 'goal',
      label: objective.description,
      sourceId: objective.id
    })
    pushUniqueEdge(edges, edgeIds, {
      id: edgeId('contains', organizationNode.id, id),
      kind: 'contains',
      sourceId: organizationNode.id,
      targetId: id
    })
  }

  const sidebarGroups: Array<{
    id: string
    node: Extract<OrganizationModelSidebarNode, { type: 'group' }>
    parentGroupId?: string
  }> = []
  const sidebarSurfaces: Array<{
    id: string
    node: Extract<OrganizationModelSidebarNode, { type: 'surface' }>
    parentGroupId?: string
  }> = []
  collectSidebarGraphNodes(organizationModel.navigation.sidebar.primary, sidebarGroups, sidebarSurfaces)
  collectSidebarGraphNodes(organizationModel.navigation.sidebar.bottom, sidebarGroups, sidebarSurfaces)

  for (const { id: surfaceSourceId, node: surface, parentGroupId } of sidebarSurfaces) {
    const id = nodeId('surface', surfaceSourceId)
    pushUniqueNode(nodes, nodeIds, {
      id,
      kind: 'surface',
      label: surface.label,
      sourceId: surfaceSourceId,
      description: surface.description,
      icon: surface.icon,
      enabled: true
    })
    pushUniqueEdge(edges, edgeIds, {
      id: edgeId('contains', parentGroupId ? nodeId('navigation-group', parentGroupId) : organizationNode.id, id),
      kind: 'contains',
      sourceId: parentGroupId ? nodeId('navigation-group', parentGroupId) : organizationNode.id,
      targetId: id
    })
    for (const systemId of surface.targets?.systems ?? []) {
      if (!validSystemRefs.has(systemId)) continue

      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('applies_to', id, systemNodeId(systemId), 'surface-system'),
        kind: 'applies_to',
        sourceId: id,
        targetId: systemNodeId(systemId)
      })
    }
  }

  for (const { id: groupSourceId, node: group, parentGroupId } of sidebarGroups) {
    const id = nodeId('navigation-group', groupSourceId)
    pushUniqueNode(nodes, nodeIds, {
      id,
      kind: 'navigation-group',
      label: group.label,
      sourceId: groupSourceId
    })
    pushUniqueEdge(edges, edgeIds, {
      id: edgeId('contains', parentGroupId ? nodeId('navigation-group', parentGroupId) : organizationNode.id, id),
      kind: 'contains',
      sourceId: parentGroupId ? nodeId('navigation-group', parentGroupId) : organizationNode.id,
      targetId: id
    })
  }
  // System.content bridge projection removed; ontology catalog nodes now carry
  // operational catalog identity in the graph.

  // Phase 4: prospecting domain removed; read templates via migration helper.
  // Steps are typed as BuildTemplate['steps'] which carries ProspectingBuildTemplateStepSchema fields.
  type TemplateStep = { id: string; stageKey: string; actionKey: string; dependsOn?: string[] }
  for (const template of getAllBuildTemplates(organizationModel).sort((a, b) => a.id.localeCompare(b.id))) {
    const steps = template.steps as unknown as TemplateStep[]
    const stepById = new Map(steps.map((s) => [s.id, s]))
    for (const step of [...steps].sort((a, b) => a.id.localeCompare(b.id))) {
      const stageNodeId = nodeId('stage', step.stageKey)
      const actionNodeId = nodeId('action', step.actionKey)
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('uses', stageNodeId, actionNodeId, step.id),
        kind: 'uses',
        sourceId: stageNodeId,
        targetId: actionNodeId
      })
      for (const depId of step.dependsOn ?? []) {
        const depStep = stepById.get(depId)
        if (depStep) {
          const depStageNodeId = nodeId('stage', depStep.stageKey)
          pushUniqueEdge(edges, edgeIds, {
            id: edgeId('references', stageNodeId, depStageNodeId, step.id),
            kind: 'references',
            sourceId: stageNodeId,
            targetId: depStageNodeId
          })
        }
      }
    }
  }

  if (commandViewData) {
    const commandViewResources = collectCommandViewResources(commandViewData).sort((a, b) =>
      a.resourceId.localeCompare(b.resourceId)
    )

    for (const resource of commandViewResources) {
      const id = nodeId('resource', resource.resourceId)
      const resourceNode = upsertResourceNode(nodes, nodeIds, resourceNodesById, {
        id,
        kind: 'resource',
        label: resource.name,
        sourceId: resource.resourceId,
        description: resource.description,
        resourceType: normalizeCommandViewResourceType(resource.type)
      })

      if (!organizationModelResourceIds.has(resource.resourceId)) {
        pushUniqueEdge(edges, edgeIds, {
          id: edgeId('contains', organizationNode.id, resourceNode.id),
          kind: 'contains',
          sourceId: organizationNode.id,
          targetId: resourceNode.id
        })
      }
    }

    for (const relationship of [...commandViewData.edges].sort((a, b) => a.id.localeCompare(b.id))) {
      const sourceNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, relationship.source)
      const targetNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, relationship.target)

      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('contains', organizationNode.id, sourceNode.id),
        kind: 'contains',
        sourceId: organizationNode.id,
        targetId: sourceNode.id
      })
      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('contains', organizationNode.id, targetNode.id),
        kind: 'contains',
        sourceId: organizationNode.id,
        targetId: targetNode.id
      })

      pushUniqueEdge(edges, edgeIds, {
        id: edgeId('references', sourceNode.id, targetNode.id, relationship.relationship),
        kind: 'references',
        sourceId: sourceNode.id,
        targetId: targetNode.id,
        label: relationship.relationship,
        relationshipType: relationship.relationship
      })
    }
  }

  const graph: OrganizationGraph = {
    version: 1,
    organizationModelVersion: organizationModel.version,
    nodes,
    edges
  }

  return OrganizationGraphSchema.parse(graph)
}
