/**
 * Registry Validation Utilities
 *
 * Centralized validation logic for ResourceRegistry.
 * All validation runs at API startup - fails fast in development.
 */

import type { z } from 'zod'
import type { ModelConfig } from '../../execution/engine/llm/model-info'
import { validateModelConfig, ModelConfigError } from '../../execution/engine/llm/errors'
import type { DeploymentSpec } from './resource-registry'
import type { ResourceEntry } from '../../organization-model/domains/resources'
import type { SystemEntry } from '../../organization-model/domains/systems'
import type { OrganizationModel } from '../../organization-model/types'
import { listAllSystems } from '../../organization-model/helpers'
import {
  compileOrganizationOntology,
  parseOntologyId,
  type OntologyKind,
  type ResolvedOntologyIndex
} from '../../organization-model/ontology'
import {
  computeInterfaceReadiness,
  type SystemInterfaceReadinessIssueFamily
} from '../../business/acquisition/ontology-validation'
import { SYSTEM_INTERFACE_PROFILES } from '../../organization-model/domains/systems'
import type { OmTopologyNodeRef } from '../../organization-model/domains/topology'
import type {
  TriggerDefinition,
  ResourceRelationships,
  ExternalResourceDefinition,
  HumanCheckpointDefinition,
  ResourceType,
  ApiInterfaceConformanceGap
} from './types'

// ============================================================================
// Validation Error Types
// ============================================================================

export class RegistryValidationError extends Error {
  constructor(
    public readonly orgName: string,
    public readonly resourceId: string | null,
    public readonly field: string | null,
    message: string
  ) {
    super(message)
    this.name = 'RegistryValidationError'
  }
}

export type ResourceValidatorMode = 'strict' | 'warn-only'

export type ResourceGovernanceValidationIssueType =
  | 'missing-code-resource'
  | 'missing-om-resource'
  | 'type-mismatch'
  | 'system-mismatch'
  | 'missing-om-system'
  | 'raw-resource-id'
  | 'descriptor-mismatch'
  | 'missing-ontology-actions'
  | 'ontology-reference-missing'
  | 'ontology-topology-grant-missing'
  | 'primary-action-mismatch'
  | 'topology-reference-missing'

export interface ResourceGovernanceModel {
  systems?: Record<string, SystemEntry>
  resources?: Record<string, ResourceEntry>
  ontology?: OrganizationModel['ontology']
  topology?: OrganizationModel['topology']
  roles?: OrganizationModel['roles']
  policies?: OrganizationModel['policies']
  entities?: OrganizationModel['entities']
  actions?: OrganizationModel['actions']
}

export interface ResourceGovernanceValidationIssue {
  type: ResourceGovernanceValidationIssueType
  orgName: string
  resourceId: string
  message: string
}

export interface ResourceGovernanceValidationResult {
  valid: boolean
  mode: ResourceValidatorMode
  issues: ResourceGovernanceValidationIssue[]
}

export interface ResourceGovernanceValidationOptions {
  mode?: ResourceValidatorMode
  onWarning?: (issue: ResourceGovernanceValidationIssue) => void
}

export interface SystemInterfaceReadinessValidationIssue {
  type: SystemInterfaceReadinessIssueFamily
  orgName: string
  systemPath: string
  interfaceKey: string
  code: string
  path?: string
  ref?: string
  message: string
}

export interface SystemInterfaceReadinessValidationResult {
  valid: boolean
  issues: SystemInterfaceReadinessValidationIssue[]
}

function getResourceValidatorMode(explicitMode?: ResourceValidatorMode): ResourceValidatorMode {
  if (explicitMode) return explicitMode
  const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env
  return env?.ELEVASIS_RESOURCE_VALIDATOR === 'warn-only' ? 'warn-only' : 'strict'
}

function addGovernanceIssue(
  issues: ResourceGovernanceValidationIssue[],
  type: ResourceGovernanceValidationIssueType,
  orgName: string,
  resourceId: string,
  message: string
): void {
  issues.push({
    type,
    orgName,
    resourceId,
    message
  })
}

function emitGovernanceIssues(
  issues: ResourceGovernanceValidationIssue[],
  mode: ResourceValidatorMode,
  onWarning?: (issue: ResourceGovernanceValidationIssue) => void
): void {
  if (issues.length === 0) return

  if (mode === 'strict') {
    const first = issues[0]
    throw new RegistryValidationError(first.orgName, first.resourceId, 'organizationModel.resources', first.message)
  }

  const warn = onWarning ?? ((issue: ResourceGovernanceValidationIssue) => console.warn(issue.message))
  for (const issue of issues) {
    warn(issue)
  }
}

function getRuntimeResources(resources: DeploymentSpec): Array<{
  resourceId: string
  type: ResourceType
  descriptor?: ResourceEntry
}> {
  return [
    ...(resources.workflows ?? []).map((workflow) => ({
      resourceId: workflow.config.resourceId,
      type: workflow.config.type,
      descriptor: workflow.config.resource
    })),
    ...(resources.agents ?? []).map((agent) => ({
      resourceId: agent.config.resourceId,
      type: agent.config.type,
      descriptor: agent.config.resource
    })),
    ...(resources.integrations ?? []).map((integration) => ({
      resourceId: integration.resourceId,
      type: integration.type,
      descriptor: (integration as { resource?: ResourceEntry }).resource
    }))
  ]
}

function isStaticPlatformAgentResource(resource: ResourceEntry): boolean {
  return resource.kind === 'agent' && resource.agentKind === 'platform'
}

function hasOntologySources(organizationModel: ResourceGovernanceModel): boolean {
  return (
    organizationModel.ontology !== undefined ||
    organizationModel.entities !== undefined ||
    organizationModel.actions !== undefined ||
    Object.values(organizationModel.systems ?? {}).some(systemHasOntologySource)
  )
}

function systemHasOntologySource(system: SystemEntry): boolean {
  if (system.ontology !== undefined) return true
  return Object.values(system.systems ?? system.subsystems ?? {}).some(systemHasOntologySource)
}

function ontologyIndexForKind(index: ResolvedOntologyIndex, kind: OntologyKind): Record<string, unknown> {
  switch (kind) {
    case 'object':
      return index.objectTypes
    case 'link':
      return index.linkTypes
    case 'action':
      return index.actionTypes
    case 'catalog':
      return index.catalogTypes
    case 'event':
      return index.eventTypes
    case 'interface':
      return index.interfaceTypes
    case 'value-type':
      return index.valueTypes
    case 'property':
      return index.sharedProperties
    case 'group':
      return index.groups
    case 'endpoint':
      return index.endpoints
  }
}

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

function addOntologyBindingIssues(
  issues: ResourceGovernanceValidationIssue[],
  orgName: string,
  resource: ResourceEntry,
  ontologyIndex: ResolvedOntologyIndex | undefined,
  organizationModel: ResourceGovernanceModel
): void {
  const binding = resource.ontology
  if (binding === undefined) return

  const hasOperationalOntologyBinding =
    binding.primaryAction !== undefined ||
    (binding.reads?.length ?? 0) > 0 ||
    (binding.writes?.length ?? 0) > 0 ||
    (binding.usesCatalogs?.length ?? 0) > 0 ||
    (binding.emits?.length ?? 0) > 0

  if (
    (resource.kind === 'workflow' || resource.kind === 'agent') &&
    hasOperationalOntologyBinding &&
    (binding.actions?.length ?? 0) === 0
  ) {
    addGovernanceIssue(
      issues,
      'missing-ontology-actions',
      orgName,
      resource.id,
      `[${orgName}] Resource '${resource.id}' declares ontology bindings but no ontology actions.`
    )
  }

  if (binding.primaryAction !== undefined && !binding.actions?.includes(binding.primaryAction)) {
    addGovernanceIssue(
      issues,
      'primary-action-mismatch',
      orgName,
      resource.id,
      `[${orgName}] Resource '${resource.id}' primaryAction '${binding.primaryAction}' must be included in ontology.actions.`
    )
  }

  if (ontologyIndex === undefined) return

  const validateRefs = (
    bindingKey: 'actions' | 'primaryAction' | 'reads' | 'writes' | 'usesCatalogs' | 'emits',
    expectedKind: OntologyKind,
    refs: string[] | string | undefined
  ) => {
    const values = refs === undefined ? [] : Array.isArray(refs) ? refs : [refs]
    const index = ontologyIndexForKind(ontologyIndex, expectedKind)

    for (const ref of values) {
      if (index[ref] !== undefined) continue
      addGovernanceIssue(
        issues,
        'ontology-reference-missing',
        orgName,
        resource.id,
        `[${orgName}] Resource '${resource.id}' ontology.${bindingKey} references missing ${expectedKind} ontology record '${ref}'.`
      )
    }
  }

  validateRefs('actions', 'action', binding.actions)
  validateRefs('primaryAction', 'action', binding.primaryAction)
  validateRefs('reads', 'object', binding.reads)
  validateRefs('writes', 'object', binding.writes)
  validateRefs('usesCatalogs', 'catalog', binding.usesCatalogs)
  validateRefs('emits', 'event', binding.emits)
  addOntologyTopologyGrantIssues(issues, orgName, resource, organizationModel)
}

function addOntologyTopologyGrantIssues(
  issues: ResourceGovernanceValidationIssue[],
  orgName: string,
  resource: ResourceEntry,
  organizationModel: ResourceGovernanceModel
): void {
  const binding = resource.ontology
  if (binding === undefined) return

  const refs = [
    ...(binding.actions ?? []),
    ...(binding.primaryAction !== undefined ? [binding.primaryAction] : []),
    ...(binding.reads ?? []),
    ...(binding.writes ?? []),
    ...(binding.usesCatalogs ?? []),
    ...(binding.emits ?? [])
  ]

  for (const ontologyId of new Set(refs)) {
    const parsed = parseOntologyId(ontologyId)
    if (parsed.isGlobal || parsed.scope === resource.systemPath) continue
    if (hasScopedTopologyGrant(organizationModel, resource, parsed.scope, ontologyId)) continue

    const missingGrant = `systemInterfaceGrant(${resource.systemPath} -> ${parsed.scope}, resourceIds: ["${resource.id}"], ontologyIds: ["${ontologyId}"])`
    addGovernanceIssue(
      issues,
      'ontology-topology-grant-missing',
      orgName,
      resource.id,
      `[${orgName}] Resource '${resource.id}' in system '${resource.systemPath}' references ontology '${ontologyId}' from system '${parsed.scope}' without scoped topology grant '${missingGrant}'.`
    )
  }
}

function hasScopedTopologyGrant(
  organizationModel: ResourceGovernanceModel,
  resource: ResourceEntry,
  targetSystemPath: string,
  ontologyId: string
): boolean {
  return Object.values(organizationModel.topology?.relationships ?? {}).some((relationship) => {
    if (relationship.kind !== 'uses') return false

    const grant = relationship.metadata?.['systemInterfaceGrant']
    if (!isSystemInterfaceGrantRecord(grant)) return false

    return (
      grant.consumer.systemPath === resource.systemPath &&
      grant.provider.systemPath === targetSystemPath &&
      grant.resourceIds.includes(resource.id) &&
      grant.ontologyIds.includes(ontologyId)
    )
  })
}

function isSystemInterfaceGrantRecord(value: unknown): value is {
  consumer: { systemPath: string; interfaceKey: string }
  provider: { systemPath: string; interfaceKey: string }
  resourceIds: string[]
  ontologyIds: string[]
} {
  if (typeof value !== 'object' || value === null) return false
  const grant = value as {
    consumer?: { systemPath?: unknown; interfaceKey?: unknown }
    provider?: { systemPath?: unknown; interfaceKey?: unknown }
    resourceIds?: unknown
    ontologyIds?: unknown
  }

  return (
    typeof grant.consumer?.systemPath === 'string' &&
    typeof grant.consumer.interfaceKey === 'string' &&
    typeof grant.provider?.systemPath === 'string' &&
    typeof grant.provider.interfaceKey === 'string' &&
    Array.isArray(grant.resourceIds) &&
    grant.resourceIds.every((id) => typeof id === 'string') &&
    Array.isArray(grant.ontologyIds) &&
    grant.ontologyIds.every((id) => typeof id === 'string')
  )
}

function addTopologyIssues(
  issues: ResourceGovernanceValidationIssue[],
  orgName: string,
  deployment: DeploymentSpec,
  organizationModel: ResourceGovernanceModel,
  systemsById: Map<string, SystemEntry>,
  omResourcesById: Map<string, ResourceEntry>,
  ontologyIndex: ResolvedOntologyIndex | undefined
): void {
  const relationships = organizationModel.topology?.relationships
  if (relationships === undefined) return

  const rolesById = new Set(Object.keys(organizationModel.roles ?? {}))
  const policiesById = new Set(Object.keys(organizationModel.policies ?? {}))
  const triggerIds = new Set(deployment.triggers?.map((trigger) => trigger.resourceId) ?? [])
  const humanCheckpointIds = new Set(deployment.humanCheckpoints?.map((checkpoint) => checkpoint.resourceId) ?? [])
  const externalResourceIds = new Set(deployment.externalResources?.map((external) => external.resourceId) ?? [])

  const topologyRefExists = (ref: OmTopologyNodeRef): boolean => {
    if (ref.kind === 'system') return systemsById.has(ref.id)
    if (ref.kind === 'resource') return omResourcesById.has(ref.id)
    if (ref.kind === 'policy') return policiesById.has(ref.id)
    if (ref.kind === 'role') return rolesById.has(ref.id)
    if (ref.kind === 'trigger') return triggerIds.has(ref.id)
    if (ref.kind === 'humanCheckpoint') return humanCheckpointIds.has(ref.id)
    if (ref.kind === 'externalResource') return externalResourceIds.has(ref.id)
    if (ref.kind === 'ontology') {
      if (ontologyIndex === undefined) return true
      return Object.values(ontologyIndex).some((records) => records[ref.id] !== undefined)
    }
    return false
  }

  for (const [relationshipId, relationship] of Object.entries(relationships)) {
    ;(['from', 'to'] as const).forEach((side) => {
      const ref = relationship[side]
      if (topologyRefExists(ref)) return

      addGovernanceIssue(
        issues,
        'topology-reference-missing',
        orgName,
        ref.id,
        `[${orgName}] Topology relationship '${relationshipId}' ${side} references missing ${ref.kind} '${ref.id}'.`
      )
    })
  }
}

/**
 * Validates runtime resource definitions against OM Resources and Systems.
 *
 * This is an overlay validator for code/resource bindings, ontology bindings,
 * and topology refs that need runtime manifest context. It does not validate
 * Organization Model shape; deploy/runtime structural gates use
 * OrganizationModelSchema.
 *
 * This is the shared core entry point for SDK, CI, deploy, and ResourceRegistry.
 * Default mode is strict. ELEVASIS_RESOURCE_VALIDATOR=warn-only remains a
 * permanent emergency escape hatch unless an explicit mode is passed.
 */
export function validateResourceGovernance(
  orgName: string,
  deployment: DeploymentSpec,
  organizationModel: ResourceGovernanceModel | undefined = deployment.organizationModel,
  options: ResourceGovernanceValidationOptions = {}
): ResourceGovernanceValidationResult {
  const mode = getResourceValidatorMode(options.mode)
  const omResourcesMap = organizationModel?.resources
  const omSystemsMap = organizationModel?.systems
  const issues: ResourceGovernanceValidationIssue[] = []

  if (!omResourcesMap || !omSystemsMap) {
    return { valid: true, mode, issues }
  }

  // Use listAllSystems (DFS) so nested systems under .subsystems are included.
  // The lookup key is the dot-joined path, which equals resource.systemPath.
  const systemsById = new Map(
    listAllSystems({ systems: omSystemsMap } as OrganizationModel).map(({ path, system }) => [path, system])
  )
  const activeOmResources = Object.values(omResourcesMap).filter((resource) => resource.status === 'active')
  const omResourcesById = new Map(activeOmResources.map((resource) => [resource.id, resource]))
  const runtimeResources = getRuntimeResources(deployment)
  const runtimeResourcesById = new Map(runtimeResources.map((resource) => [resource.resourceId, resource]))
  const ontologyCompilation = hasOntologySources(organizationModel)
    ? compileOrganizationOntology(organizationModel)
    : undefined
  const ontologyIndex = ontologyCompilation?.ontology

  for (const diagnostic of ontologyCompilation?.diagnostics ?? []) {
    addGovernanceIssue(
      issues,
      'ontology-reference-missing',
      orgName,
      diagnostic.id,
      `[${orgName}] ${diagnostic.message}`
    )
  }

  for (const resource of activeOmResources) {
    if (!systemsById.has(resource.systemPath)) {
      addGovernanceIssue(
        issues,
        'missing-om-system',
        orgName,
        resource.id,
        `[${orgName}] OM resource '${resource.id}' references missing system path '${resource.systemPath}'.`
      )
    }

    const runtimeResource = runtimeResourcesById.get(resource.id)
    if (!runtimeResource) {
      if (isStaticPlatformAgentResource(resource)) {
        continue
      }

      addGovernanceIssue(
        issues,
        'missing-code-resource',
        orgName,
        resource.id,
        `[${orgName}] OM resource '${resource.id}' has no matching code-side resource.`
      )
      continue
    }

    if (runtimeResource.type !== resource.kind) {
      addGovernanceIssue(
        issues,
        'type-mismatch',
        orgName,
        resource.id,
        `[${orgName}] Resource '${resource.id}' type mismatch: code has '${runtimeResource.type}', OM has '${resource.kind}'.`
      )
    }

    if (runtimeResource.descriptor && runtimeResource.descriptor.systemPath !== resource.systemPath) {
      addGovernanceIssue(
        issues,
        'system-mismatch',
        orgName,
        resource.id,
        `[${orgName}] Resource '${resource.id}' system mismatch: code descriptor has '${runtimeResource.descriptor.systemPath}', OM has '${resource.systemPath}'.`
      )
    }

    if (runtimeResource.descriptor && !sameJson(runtimeResource.descriptor.ontology, resource.ontology)) {
      addGovernanceIssue(
        issues,
        'descriptor-mismatch',
        orgName,
        resource.id,
        `[${orgName}] Resource '${resource.id}' ontology descriptor mismatch between code and OM.`
      )
    }

    addOntologyBindingIssues(issues, orgName, resource, ontologyIndex, organizationModel)
  }

  for (const runtimeResource of runtimeResources) {
    const omResource = omResourcesById.get(runtimeResource.resourceId)
    if (!omResource) {
      addGovernanceIssue(
        issues,
        'missing-om-resource',
        orgName,
        runtimeResource.resourceId,
        `[${orgName}] Code-side resource '${runtimeResource.resourceId}' has no active OM Resource descriptor.`
      )
    }

    if (!runtimeResource.descriptor) {
      addGovernanceIssue(
        issues,
        'raw-resource-id',
        orgName,
        runtimeResource.resourceId,
        `[${orgName}] Code-side resource '${runtimeResource.resourceId}' authors raw resourceId/type values. Use an OM Resource descriptor and bindResourceDescriptor().`
      )
      continue
    }

    if (runtimeResource.descriptor.id !== runtimeResource.resourceId) {
      addGovernanceIssue(
        issues,
        'raw-resource-id',
        orgName,
        runtimeResource.resourceId,
        `[${orgName}] Code-side resource '${runtimeResource.resourceId}' does not derive identity from its OM descriptor '${runtimeResource.descriptor.id}'.`
      )
    }

    if (runtimeResource.descriptor.kind !== runtimeResource.type) {
      addGovernanceIssue(
        issues,
        'type-mismatch',
        orgName,
        runtimeResource.resourceId,
        `[${orgName}] Code-side resource '${runtimeResource.resourceId}' descriptor kind '${runtimeResource.descriptor.kind}' does not match runtime type '${runtimeResource.type}'.`
      )
    }
  }

  addTopologyIssues(issues, orgName, deployment, organizationModel, systemsById, omResourcesById, ontologyIndex)

  emitGovernanceIssues(issues, mode, options.onWarning)

  return {
    valid: issues.length === 0,
    mode,
    issues
  }
}

function addSystemInterfaceIssue(
  issues: SystemInterfaceReadinessValidationIssue[],
  orgName: string,
  systemPath: string,
  interfaceKey: string,
  issue: {
    family: SystemInterfaceReadinessIssueFamily
    code: string
    path?: string
    ref?: string
    message: string
  }
): void {
  issues.push({
    type: issue.family,
    orgName,
    systemPath,
    interfaceKey,
    code: issue.code,
    path: issue.path,
    ref: issue.ref,
    message: `[${orgName}] ${issue.family}:${issue.code}: ${issue.message}`
  })
}

/**
 * Detects systems that are API-backed (have resources with non-empty ontology
 * bindings), CAN adopt a platform-cataloged `apiInterface` readinessProfile
 * (system path appears in `SYSTEM_INTERFACE_PROFILES`), but have not declared
 * `apiInterface` in the Organization Model.
 *
 * Originally broad-by-design (flagged any system with ontology actions/writes
 * lacking apiInterface). Tightened in @elevasis/sdk@1.30.1 after the template
 * surfaced the unactionable-paradox case: custom systems (e.g. `sys.operations`,
 * `sys.notifications`) have valid `ontology.actions` bindings but cannot adopt
 * one of the closed-catalog readiness profiles, AND the documented guidance
 * ("Custom Systems should not declare apiInterface") is incompatible with the
 * broad heuristic. The gate now only flags systems where adoption is even
 * possible — i.e. the system path matches a cataloged profile's `systemPath`.
 *
 * A system is flagged when ALL of:
 *   (a) `system.apiInterface === undefined`, AND
 *   (b) at least one resource in `organizationModel.resources` has
 *       `resource.systemPath` matching the system path AND
 *       `resource.ontology` non-empty (has `actions` and/or `writes` defined), AND
 *   (c) the system path appears as a cataloged `systemPath` in
 *       `SYSTEM_INTERFACE_PROFILES` (so adopting a profile is actually possible).
 *
 * Returns `[]` when `organizationModel` is undefined or no gaps are found.
 */
export function detectMissingApiInterfaceDeclarations(
  orgName: string,
  organizationModel: OrganizationModel | undefined
): ApiInterfaceConformanceGap[] {
  if (organizationModel === undefined) return []

  // Build a map of system path → resource IDs for resources with non-empty ontology.
  const apiBoundResourcesBySystemPath = new Map<string, string[]>()
  for (const resource of Object.values(organizationModel.resources ?? {})) {
    const ontology = resource.ontology
    if (ontology === undefined) continue
    const hasOntologyBindings =
      (ontology.actions !== undefined && ontology.actions.length > 0) ||
      (ontology.writes !== undefined && ontology.writes.length > 0)
    if (!hasOntologyBindings) continue
    const existing = apiBoundResourcesBySystemPath.get(resource.systemPath)
    if (existing) {
      existing.push(resource.id)
    } else {
      apiBoundResourcesBySystemPath.set(resource.systemPath, [resource.id])
    }
  }

  if (apiBoundResourcesBySystemPath.size === 0) return []

  // Closed-catalog filter: a system is only a candidate for missing apiInterface
  // if it could actually adopt one of the cataloged platform profiles.
  const catalogedSystemPaths = new Set<string>(SYSTEM_INTERFACE_PROFILES.map((profile) => profile.systemPath))

  const gaps: ApiInterfaceConformanceGap[] = []

  for (const { path, system } of listAllSystems(organizationModel)) {
    if (system.apiInterface !== undefined) continue
    if (!catalogedSystemPaths.has(path)) continue
    const resourceIds = apiBoundResourcesBySystemPath.get(path)
    if (!resourceIds || resourceIds.length === 0) continue
    gaps.push({
      systemPath: path,
      resourceIds,
      message: `[${orgName}] System '${path}' has ${resourceIds.length} API-backed resource(s) (${resourceIds.join(', ')}) but no apiInterface declaration.`
    })
  }

  return gaps
}

/**
 * Validates every explicitly declared API System Interface by deriving
 * readiness from its scoped resources and ontology bindings.
 *
 * Contract-absent models remain valid until they opt in with
 * `systems.*.apiInterface`.
 */
export function validateDeclaredSystemInterfaceReadiness(
  orgName: string,
  organizationModel: ResourceGovernanceModel | undefined
): SystemInterfaceReadinessValidationResult {
  const issues: SystemInterfaceReadinessValidationIssue[] = []
  if (organizationModel === undefined) return { valid: true, issues }

  const model = organizationModel as OrganizationModel

  for (const { path, system } of listAllSystems(model)) {
    if (system.apiInterface === undefined) continue

    const interfaceKey = 'api'
    const resourceIds = system.apiInterface.resourceIds ?? []
    if (resourceIds.length === 0) {
      const readinessProfile = system.apiInterface.readinessProfile
      if (
        readinessProfile !== undefined &&
        !SYSTEM_INTERFACE_PROFILES.some((profile) => profile.readinessProfile === readinessProfile)
      ) {
        addSystemInterfaceIssue(issues, orgName, path, interfaceKey, {
          family: 'SYSTEM_INTERFACE_INVALID',
          code: 'unknown-readiness-profile',
          path: `systems.${path}.apiInterface.readinessProfile`,
          ref: readinessProfile,
          message: `System Interface "${path}/${interfaceKey}" references unknown readiness profile "${readinessProfile}".`
        })
      }
      continue
    }

    const result = computeInterfaceReadiness(model, { systemPath: path, interfaceKey })
    for (const issue of result.issues) {
      addSystemInterfaceIssue(issues, orgName, path, interfaceKey, issue)
    }
  }

  if (issues.length > 0) {
    const first = issues[0]
    throw new RegistryValidationError(
      first.orgName,
      `${first.systemPath}/${first.interfaceKey}`,
      first.path ?? 'organizationModel.systems.apiInterface',
      first.message
    )
  }

  return { valid: true, issues }
}

// ============================================================================
// Resource Validation
// ============================================================================

/**
 * Validates resources for a single organization
 * - Duplicate resourceId check
 * - Model configuration validation
 * - ExecutionInterface-to-inputSchema validation
 * @throws RegistryValidationError if validation fails
 */
export function validateDeploymentSpec(orgName: string, resources: DeploymentSpec): void {
  const seenIds = new Set<string>()

  // Validate workflows
  resources.workflows?.forEach((workflow) => {
    const id = workflow.config.resourceId

    // Check for duplicate IDs
    if (seenIds.has(id)) {
      throw new RegistryValidationError(
        orgName,
        id,
        null,
        `Duplicate resourceId "${id}" in organization "${orgName}". ` +
          `Workflows and agents must have unique IDs within an organization.`
      )
    }
    seenIds.add(id)
    // Validate model config if present (workflows may optionally have modelConfig)
    if ('modelConfig' in workflow && workflow.modelConfig) {
      validateResourceModelConfig(orgName, id, workflow.modelConfig as ModelConfig)
    }

    // Validate ExecutionInterface matches inputSchema
    if (workflow.interface) {
      validateExecutionInterface(orgName, id, workflow.interface, workflow.contract.inputSchema)
    }
  })

  // Validate agents
  resources.agents?.forEach((agent) => {
    const id = agent.config.resourceId

    // Check for duplicate IDs
    if (seenIds.has(id)) {
      throw new RegistryValidationError(
        orgName,
        id,
        null,
        `Duplicate resourceId "${id}" in organization "${orgName}". ` +
          `Workflows and agents must have unique IDs within an organization.`
      )
    }
    seenIds.add(id)

    // Validate model config
    validateResourceModelConfig(orgName, id, agent.modelConfig)

    // Validate ExecutionInterface matches inputSchema
    if (agent.interface) {
      validateExecutionInterface(orgName, id, agent.interface, agent.contract.inputSchema)
    }
  })

  validateResourceGovernance(orgName, resources)
  validateDeclaredSystemInterfaceReadiness(orgName, resources.organizationModel)
}

/**
 * Validates model configuration for a resource
 */
function validateResourceModelConfig(orgName: string, resourceId: string, modelConfig: ModelConfig): void {
  try {
    validateModelConfig(modelConfig)
  } catch (error) {
    if (error instanceof ModelConfigError) {
      throw new RegistryValidationError(
        orgName,
        resourceId,
        error.field,
        `Invalid model config in ${orgName}/${resourceId}: ${error.message} (field: ${error.field})`
      )
    }
    throw error
  }
}

// ============================================================================
// ExecutionInterface Validation
// ============================================================================

/**
 * Validates that ExecutionInterface form fields match the inputSchema
 *
 * Checks:
 * 1. All required fields in inputSchema have corresponding form fields
 * 2. Form field names match schema field names (or have valid fieldMappings)
 * 3. Required/optional alignment between form and schema
 *
 * @throws RegistryValidationError if interface doesn't match schema
 */
export function validateExecutionInterface(
  orgName: string,
  resourceId: string,
  executionInterface: {
    form: { fields: Array<{ name: string; required?: boolean }>; fieldMappings?: Record<string, string> }
  },
  inputSchema: z.ZodType
): void {
  const form = executionInterface.form
  const fieldMappings = form.fieldMappings ?? {}

  // Extract schema shape (only works for ZodObject)
  const schemaShape = extractZodShape(inputSchema)
  if (!schemaShape) {
    // Can't validate non-object schemas - skip validation
    return
  }

  const schemaFieldNames = Object.keys(schemaShape)

  // Build effective mapping: form field name -> schema field name
  const formToSchemaMap = new Map<string, string>()
  for (const field of form.fields) {
    const schemaKey = fieldMappings[field.name] ?? field.name
    formToSchemaMap.set(field.name, schemaKey)
  }

  // Check 1: All required schema fields have form fields
  for (const schemaFieldName of schemaFieldNames) {
    const schemaField = schemaShape[schemaFieldName]
    const isRequired = !isZodOptional(schemaField)

    // Find form field that maps to this schema field
    let hasFormField = false
    for (const [formFieldName, mappedSchemaName] of Array.from(formToSchemaMap.entries())) {
      if (mappedSchemaName === schemaFieldName) {
        hasFormField = true

        // Check required alignment
        const formField = form.fields.find((f) => f.name === formFieldName)
        if (isRequired && !formField?.required) {
          throw new RegistryValidationError(
            orgName,
            resourceId,
            `interface.form.fields.${formFieldName}`,
            `ExecutionInterface field "${formFieldName}" should be required to match inputSchema field "${schemaFieldName}" in ${orgName}/${resourceId}`
          )
        }
        break
      }
    }

    if (isRequired && !hasFormField) {
      throw new RegistryValidationError(
        orgName,
        resourceId,
        'interface.form.fields',
        `ExecutionInterface missing required field "${schemaFieldName}" from inputSchema in ${orgName}/${resourceId}`
      )
    }
  }

  // Check 2: All form fields map to valid schema fields
  for (const [formFieldName, schemaFieldName] of Array.from(formToSchemaMap.entries())) {
    // Detect nested field notation (e.g., "criteria.targetTitles") and suggest flattening
    if (schemaFieldName.includes('.')) {
      const topLevelField = schemaFieldName.split('.')[0]
      throw new RegistryValidationError(
        orgName,
        resourceId,
        `interface.form.fields.${formFieldName}`,
        `ExecutionInterface field "${formFieldName}" uses nested notation. ` +
          `Flatten the inputSchema by moving nested fields to top-level ` +
          `(e.g., "${topLevelField}.x" → "x") in ${orgName}/${resourceId}`
      )
    }

    if (!schemaFieldNames.includes(schemaFieldName)) {
      throw new RegistryValidationError(
        orgName,
        resourceId,
        `interface.form.fields.${formFieldName}`,
        `ExecutionInterface field "${formFieldName}" maps to non-existent schema field "${schemaFieldName}" in ${orgName}/${resourceId}`
      )
    }
  }
}

/**
 * Extract shape from a Zod schema (handles ZodObject and wrapped types)
 */
function extractZodShape(schema: z.ZodType): Record<string, z.ZodType> | null {
  // Handle ZodObject directly
  if ('shape' in schema && typeof schema.shape === 'object') {
    return schema.shape as Record<string, z.ZodType>
  }

  // Handle wrapped types (ZodEffects, ZodDefault, etc.)
  if ('_def' in schema) {
    const def = schema._def as { innerType?: z.ZodType; schema?: z.ZodType }
    if (def.innerType) {
      return extractZodShape(def.innerType)
    }
    if (def.schema) {
      return extractZodShape(def.schema)
    }
  }

  return null
}

/**
 * Check if a Zod type is optional (or has a default)
 * Uses isOptional() method which handles ZodOptional, ZodDefault, ZodNullable
 */
function isZodOptional(schema: z.ZodType): boolean {
  // Zod provides isOptional() which returns true for optional, default, and nullable types
  return schema.isOptional()
}

// ============================================================================
// Relationship Validation
// ============================================================================

/**
 * Validates relationship declarations reference valid resources
 * @throws RegistryValidationError if validation fails
 */
export function validateRelationships(orgName: string, resources: DeploymentSpec): void {
  // Skip if no manifest data
  if (!resources.relationships && !resources.triggers && !resources.externalResources && !resources.humanCheckpoints)
    return

  // Build resource ID sets for validation
  const validAgentIds = new Set(resources.agents?.map((a) => a.config.resourceId) ?? [])
  const validWorkflowIds = new Set(resources.workflows?.map((w) => w.config.resourceId) ?? [])
  const validIntegrationIds = new Set(resources.integrations?.map((i) => i.resourceId) ?? [])
  const validTriggerIds = new Set(resources.triggers?.map((t) => t.resourceId) ?? [])

  // Collect all internal resource IDs for uniqueness check
  const allInternalIds = new Set([
    ...Array.from(validAgentIds),
    ...Array.from(validWorkflowIds),
    ...Array.from(validTriggerIds),
    ...Array.from(validIntegrationIds)
  ])

  // Validate triggers
  validateTriggers(orgName, resources.triggers ?? [], validAgentIds, validWorkflowIds)

  // Validate resource relationships
  validateResourceRelationships(
    orgName,
    resources.relationships ?? {},
    validAgentIds,
    validWorkflowIds,
    validIntegrationIds,
    validTriggerIds
  )

  // Validate external resources
  validateExternalResources(
    orgName,
    resources.externalResources ?? [],
    allInternalIds,
    validAgentIds,
    validWorkflowIds,
    validIntegrationIds
  )

  // Validate human checkpoints
  validateHumanCheckpoints(orgName, resources.humanCheckpoints ?? [], allInternalIds, validAgentIds, validWorkflowIds)
}

/**
 * Validates trigger invocations
 * NOTE: Trigger relationships are declared in ResourceRelationships, not on TriggerDefinition
 * This validation is now handled by validateResourceRelationships()
 */
function validateTriggers(
  _orgName: string,
  _triggers: TriggerDefinition[],
  _validAgentIds: Set<string>,
  _validWorkflowIds: Set<string>
): void {
  // No validation needed here - triggers declare their relationships in ResourceRelationships
  // This prevents duplication and keeps all relationship declarations in one place
}

/**
 * Validates resource relationship declarations
 */
function validateResourceRelationships(
  orgName: string,
  relationships: ResourceRelationships,
  validAgentIds: Set<string>,
  validWorkflowIds: Set<string>,
  validIntegrationIds: Set<string>,
  validTriggerIds: Set<string>
): void {
  for (const [resourceId, declaration] of Object.entries(relationships)) {
    // Validate declaring resource exists (agents, workflows, or triggers can declare relationships)
    const resourceExists =
      validAgentIds.has(resourceId) || validWorkflowIds.has(resourceId) || validTriggerIds.has(resourceId)
    if (!resourceExists) {
      throw new RegistryValidationError(
        orgName,
        resourceId,
        null,
        `[${orgName}] Relationship declared for non-existent resource: ${resourceId}`
      )
    }

    // Validate triggers.agents
    declaration.triggers?.agents?.forEach((agentId) => {
      if (!validAgentIds.has(agentId)) {
        throw new RegistryValidationError(
          orgName,
          resourceId,
          'triggers.agents',
          `[${orgName}] Resource '${resourceId}' triggers non-existent agent: ${agentId}`
        )
      }
    })

    // Validate triggers.workflows
    declaration.triggers?.workflows?.forEach((workflowId) => {
      if (!validWorkflowIds.has(workflowId)) {
        throw new RegistryValidationError(
          orgName,
          resourceId,
          'triggers.workflows',
          `[${orgName}] Resource '${resourceId}' triggers non-existent workflow: ${workflowId}`
        )
      }
    })

    // Validate uses.integrations
    declaration.uses?.integrations?.forEach((integrationId) => {
      if (!validIntegrationIds.has(integrationId)) {
        throw new RegistryValidationError(
          orgName,
          resourceId,
          'uses.integrations',
          `[${orgName}] Resource '${resourceId}' uses non-existent integration: ${integrationId}`
        )
      }
    })
  }
}

/**
 * Validates external resource definitions
 */
function validateExternalResources(
  orgName: string,
  externalResources: ExternalResourceDefinition[],
  allInternalIds: Set<string>,
  validAgentIds: Set<string>,
  validWorkflowIds: Set<string>,
  validIntegrationIds: Set<string>
): void {
  externalResources.forEach((external) => {
    // Validate unique ID (no conflict with internal resources)
    if (allInternalIds.has(external.resourceId)) {
      throw new RegistryValidationError(
        orgName,
        external.resourceId,
        null,
        `[${orgName}] External resource ID '${external.resourceId}' conflicts with internal resource ID`
      )
    }

    // Validate triggers (external -> internal)
    external.triggers?.agents?.forEach((agentId) => {
      if (!validAgentIds.has(agentId)) {
        throw new RegistryValidationError(
          orgName,
          external.resourceId,
          'triggers.agents',
          `[${orgName}] External resource '${external.resourceId}' triggers non-existent agent: ${agentId}`
        )
      }
    })

    external.triggers?.workflows?.forEach((workflowId) => {
      if (!validWorkflowIds.has(workflowId)) {
        throw new RegistryValidationError(
          orgName,
          external.resourceId,
          'triggers.workflows',
          `[${orgName}] External resource '${external.resourceId}' triggers non-existent workflow: ${workflowId}`
        )
      }
    })

    // Validate uses integrations
    external.uses?.integrations?.forEach((integrationId) => {
      if (!validIntegrationIds.has(integrationId)) {
        throw new RegistryValidationError(
          orgName,
          external.resourceId,
          'uses.integrations',
          `[${orgName}] External resource '${external.resourceId}' uses non-existent integration: ${integrationId}`
        )
      }
    })
  })
}

/**
 * Validates human checkpoint definitions
 */
function validateHumanCheckpoints(
  orgName: string,
  humanCheckpoints: HumanCheckpointDefinition[],
  allInternalIds: Set<string>,
  validAgentIds: Set<string>,
  validWorkflowIds: Set<string>
): void {
  humanCheckpoints.forEach((humanCheckpoint) => {
    // Check for ID conflicts with internal resources
    if (allInternalIds.has(humanCheckpoint.resourceId)) {
      throw new RegistryValidationError(
        orgName,
        humanCheckpoint.resourceId,
        null,
        `[${orgName}] Human checkpoint ID '${humanCheckpoint.resourceId}' conflicts with internal resource ID`
      )
    }

    // Validate requestedBy.agents exist
    humanCheckpoint.requestedBy?.agents?.forEach((agentId) => {
      if (!validAgentIds.has(agentId)) {
        throw new RegistryValidationError(
          orgName,
          humanCheckpoint.resourceId,
          'requestedBy.agents',
          `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' requestedBy non-existent agent: ${agentId}`
        )
      }
    })

    // Validate requestedBy.workflows exist
    humanCheckpoint.requestedBy?.workflows?.forEach((workflowId) => {
      if (!validWorkflowIds.has(workflowId)) {
        throw new RegistryValidationError(
          orgName,
          humanCheckpoint.resourceId,
          'requestedBy.workflows',
          `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' requestedBy non-existent workflow: ${workflowId}`
        )
      }
    })

    // Validate routesTo.agents exist
    humanCheckpoint.routesTo?.agents?.forEach((agentId) => {
      if (!validAgentIds.has(agentId)) {
        throw new RegistryValidationError(
          orgName,
          humanCheckpoint.resourceId,
          'routesTo.agents',
          `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' routesTo non-existent agent: ${agentId}`
        )
      }
    })

    // Validate routesTo.workflows exist
    humanCheckpoint.routesTo?.workflows?.forEach((workflowId) => {
      if (!validWorkflowIds.has(workflowId)) {
        throw new RegistryValidationError(
          orgName,
          humanCheckpoint.resourceId,
          'routesTo.workflows',
          `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' routesTo non-existent workflow: ${workflowId}`
        )
      }
    })
  })
}
