import fs from 'node:fs/promises'
import path from 'node:path'
import { loadCatalog, serviceRoot } from './catalog.mjs'

const ID_RE = /^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9_]*)+$/
const RULE_ID_RE = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$/
const ROLE_ID_RE = /^[a-z][a-z0-9_]*$/
const FIELD_ID_RE = /^[a-z][a-z0-9_]*$/
const ALLOWED_RELATION_STATUSES = new Set([
  'source-confirmed',
  'source-evidenced',
  'owner-confirmed',
  'product-required',
  'future-concept',
  'tech-debt-noncanonical',
  'pending',
])

function assert(condition, message, errors) {
  if (!condition) errors.push(message)
}

function assertUnique(ids, label, errors) {
  const seen = new Set()
  for (const id of ids) {
    assert(!seen.has(id), `${label} duplicate id: ${id}`, errors)
    seen.add(id)
  }
}

function assertEntityRefs(refs, entityById, label, errors) {
  for (const ref of refs || []) {
    assert(entityById.has(ref), `${label} references unknown entity: ${ref}`, errors)
  }
}

function assertRelationRef(ref, relationById, label, errors) {
  assert(relationById.has(ref), `${label} references unknown relation: ${ref}`, errors)
}

function assertContextRefs(refs, contextById, label, errors) {
  for (const ref of refs || []) {
    assert(contextById.has(ref), `${label} references unknown context: ${ref}`, errors)
  }
}

function assertRefsObject(refs, label, errors) {
  assert(refs && typeof refs === 'object' && !Array.isArray(refs), `${label} refs must be object`, errors)
  for (const [key, value] of Object.entries(refs || {})) {
    assert(/^[a-z][a-z0-9_]*$/i.test(key), `${label} refs has invalid key: ${key}`, errors)
    assert(value !== null && value !== undefined && String(value).trim() !== '', `${label} refs.${key} is empty`, errors)
  }
}

async function pathExists(relativePath) {
  try {
    await fs.access(path.join(serviceRoot, relativePath))
    return true
  } catch {
    return false
  }
}

export async function validateCatalog() {
  const catalog = await loadCatalog()
  const errors = []
  const warnings = []
  const {
    entities,
    relations,
    aliases,
    guardrails,
    evidence,
    resolverRules,
    assistantAccessPolicy,
    assistantActions,
    assistantRiskPolicy,
    contextBindings,
    entityById,
    relationById,
    contextById,
    bindingTypeById,
  } = catalog

  const statusVocabulary = new Set(entities.statusVocabulary || [])

  assertUnique(entities.entities.map((entity) => entity.id), 'entity', errors)
  for (const entity of entities.entities) {
    assert(ID_RE.test(entity.id), `entity id has invalid format: ${entity.id}`, errors)
    assert(entity.name, `entity ${entity.id} missing name`, errors)
    assert(entity.surface, `entity ${entity.id} missing surface`, errors)
    assert(Array.isArray(entity.status) && entity.status.length > 0, `entity ${entity.id} missing status`, errors)
    for (const status of entity.status || []) {
      assert(statusVocabulary.has(status), `entity ${entity.id} uses unknown status: ${status}`, errors)
    }
    validateValueContract(entity.valueContract, entity.id, errors)
  }

  assertUnique(relations.relations.map((relation) => relation.id), 'relation', errors)
  for (const relation of relations.relations) {
    assert(RULE_ID_RE.test(relation.id), `relation id has invalid format: ${relation.id}`, errors)
    assert(ALLOWED_RELATION_STATUSES.has(relation.status), `relation ${relation.id} uses unknown status: ${relation.status}`, errors)
    assertEntityRefs(relation.from, entityById, `relation ${relation.id}.from`, errors)
    assertEntityRefs(relation.to, entityById, `relation ${relation.id}.to`, errors)
  }

  assertUnique(aliases.aliases.map((alias) => alias.alias.toLowerCase()), 'alias', errors)
  for (const alias of aliases.aliases) {
    assert(alias.alias, 'alias missing alias text', errors)
    assert(entityById.has(alias.canonicalId), `alias ${alias.alias} points to unknown entity: ${alias.canonicalId}`, errors)
  }

  assertUnique(guardrails.rules.map((rule) => rule.id), 'guardrail', errors)
  for (const rule of guardrails.rules) {
    assert(RULE_ID_RE.test(rule.id), `guardrail id has invalid format: ${rule.id}`, errors)
    assert(['error', 'warning'].includes(rule.severity), `guardrail ${rule.id} invalid severity`, errors)
    assertEntityRefs(rule.entityIds, entityById, `guardrail ${rule.id}`, errors)
  }
  for (const pair of guardrails.blockedConflations || []) {
    assert(Array.isArray(pair) && pair.length === 2, `blocked conflation must be a pair: ${JSON.stringify(pair)}`, errors)
    assertEntityRefs(pair, entityById, 'blocked conflation', errors)
  }

  for (const ledger of evidence.ledgers || []) {
    assert(await pathExists(ledger.path), `evidence ledger path missing: ${ledger.path}`, errors)
    assertEntityRefs(ledger.entityIds, entityById, `ledger ${ledger.id}`, errors)
  }
  for (const baselineDoc of evidence.baselineDocs || []) {
    assert(await pathExists(baselineDoc), `baseline doc path missing: ${baselineDoc}`, errors)
  }

  assertUnique(resolverRules.rules.map((rule) => rule.id), 'resolver rule', errors)
  for (const rule of resolverRules.rules) {
    assert(RULE_ID_RE.test(rule.id), `resolver rule id has invalid format: ${rule.id}`, errors)
    assert(rule.fromSurface, `resolver rule ${rule.id} missing fromSurface`, errors)
    assert(rule.outputSurface, `resolver rule ${rule.id} missing outputSurface`, errors)
    assertRelationRef(rule.relationId, relationById, `resolver rule ${rule.id}`, errors)
    assertEntityRefs(rule.inputEntityIds, entityById, `resolver rule ${rule.id}.inputEntityIds`, errors)
    assertEntityRefs(rule.outputEntityIds, entityById, `resolver rule ${rule.id}.outputEntityIds`, errors)
    if (rule.status !== 'product-required') {
      warnings.push(`resolver rule ${rule.id} has non-product-required status: ${rule.status}`)
    }
  }

  const assistantRoleIds = new Set((assistantAccessPolicy.roleVocabulary || []).map((role) => role.id))
  const assistantCapabilityIds = new Set((assistantAccessPolicy.capabilities || []).map((capability) => capability.id))
  const assistantOperationIds = new Set((assistantAccessPolicy.operations || []).map((operation) => operation.id))
  const assistantHardDenyIds = new Set((assistantAccessPolicy.hardDenies || []).map((deny) => deny.id))
  const assistantRiskLevelIds = new Set((assistantRiskPolicy.riskLevels || []).map((riskLevel) => riskLevel.id))
  const assistantConfirmationModeIds = new Set((assistantRiskPolicy.confirmationModes || []).map((mode) => mode.id))
  const assistantSelfActionPolicyIds = new Set((assistantRiskPolicy.selfActionPolicies || []).map((policy) => policy.id))
  const assistantActionHardDenyIds = new Set((assistantRiskPolicy.hardDenies || []).map((deny) => deny.id))
  const assistantAppIds = new Set((assistantActions.apps || []).map((app) => app.id))
  const assistantAdapterStatuses = new Set(assistantActions.adapterStatuses || [])
  const assistantActionIds = new Set((assistantActions.actions || []).map((action) => action.id))

  assertUnique([...assistantRoleIds], 'assistant role', errors)
  for (const role of assistantAccessPolicy.roleVocabulary || []) {
    assert(ROLE_ID_RE.test(role.id), `assistant role id has invalid format: ${role.id}`, errors)
    assert(role.name, `assistant role ${role.id} missing name`, errors)
    assert(role.summary, `assistant role ${role.id} missing summary`, errors)
  }

  assertUnique((assistantAccessPolicy.roleAliases || []).map((alias) => alias.alias.toLowerCase()), 'assistant role alias', errors)
  for (const alias of assistantAccessPolicy.roleAliases || []) {
    assert(alias.alias, 'assistant role alias missing alias text', errors)
    assert(assistantRoleIds.has(alias.roleId), `assistant role alias ${alias.alias} references unknown role: ${alias.roleId}`, errors)
  }

  for (const [key, roleId] of Object.entries(assistantAccessPolicy.defaultResolution || {})) {
    if (key.endsWith('Role')) {
      assert(assistantRoleIds.has(roleId), `assistant defaultResolution.${key} references unknown role: ${roleId}`, errors)
    }
  }

  for (const sourceAuthority of assistantAccessPolicy.sourceAuthorities || []) {
    assert(RULE_ID_RE.test(sourceAuthority.id), `assistant source authority id has invalid format: ${sourceAuthority.id}`, errors)
    assertEntityRefs(sourceAuthority.entityIds, entityById, `assistant source authority ${sourceAuthority.id}`, errors)
  }

  assertUnique([...assistantCapabilityIds], 'assistant capability', errors)
  for (const capability of assistantAccessPolicy.capabilities || []) {
    assert(RULE_ID_RE.test(capability.id), `assistant capability id has invalid format: ${capability.id}`, errors)
    assert(entityById.has(capability.entityId), `assistant capability ${capability.id} references unknown entity: ${capability.entityId}`, errors)
    assert(capability.summary, `assistant capability ${capability.id} missing summary`, errors)
  }

  assertUnique((assistantAccessPolicy.roleMatrix || []).map((matrix) => matrix.roleId), 'assistant role matrix', errors)
  for (const matrix of assistantAccessPolicy.roleMatrix || []) {
    assert(assistantRoleIds.has(matrix.roleId), `assistant matrix references unknown role: ${matrix.roleId}`, errors)
    for (const capabilityId of matrix.allowedCapabilities || []) {
      assert(assistantCapabilityIds.has(capabilityId), `assistant matrix ${matrix.roleId} allows unknown capability: ${capabilityId}`, errors)
    }
    for (const capabilityId of matrix.deniedCapabilities || []) {
      if (capabilityId === '*') continue
      assert(assistantCapabilityIds.has(capabilityId), `assistant matrix ${matrix.roleId} denies unknown capability: ${capabilityId}`, errors)
    }
    for (const capabilityId of matrix.deniedWhenNoAdminScope || []) {
      assert(assistantCapabilityIds.has(capabilityId), `assistant matrix ${matrix.roleId} no-scope denies unknown capability: ${capabilityId}`, errors)
    }
    for (const conditional of matrix.conditionalCapabilities || []) {
      assert(assistantCapabilityIds.has(conditional.capabilityId), `assistant matrix ${matrix.roleId} conditional unknown capability: ${conditional.capabilityId}`, errors)
      assert(conditional.requires, `assistant matrix ${matrix.roleId} conditional ${conditional.capabilityId} missing requires`, errors)
    }
  }

  assertUnique([...assistantOperationIds], 'assistant operation', errors)
  for (const operation of assistantAccessPolicy.operations || []) {
    assert(RULE_ID_RE.test(operation.id), `assistant operation id has invalid format: ${operation.id}`, errors)
    assert(assistantCapabilityIds.has(operation.capabilityId), `assistant operation ${operation.id} references unknown capability: ${operation.capabilityId}`, errors)
    for (const denyId of operation.hardDenies || []) {
      assert(assistantHardDenyIds.has(denyId), `assistant operation ${operation.id} references unknown hard deny: ${denyId}`, errors)
    }
    assert(operation.summary, `assistant operation ${operation.id} missing summary`, errors)
  }

  assertUnique((assistantAccessPolicy.hardDenies || []).map((deny) => deny.id), 'assistant hard deny', errors)
  for (const deny of assistantAccessPolicy.hardDenies || []) {
    assert(RULE_ID_RE.test(deny.id), `assistant hard deny id has invalid format: ${deny.id}`, errors)
    assertEntityRefs(deny.entityIds, entityById, `assistant hard deny ${deny.id}`, errors)
  }

  assertUnique([...assistantRiskLevelIds], 'assistant risk level', errors)
  for (const riskLevel of assistantRiskPolicy.riskLevels || []) {
    assert(ROLE_ID_RE.test(riskLevel.id), `assistant risk level id has invalid format: ${riskLevel.id}`, errors)
    assert(riskLevel.summary, `assistant risk level ${riskLevel.id} missing summary`, errors)
  }

  assertUnique([...assistantConfirmationModeIds], 'assistant confirmation mode', errors)
  for (const mode of assistantRiskPolicy.confirmationModes || []) {
    assert(ROLE_ID_RE.test(mode.id), `assistant confirmation mode id has invalid format: ${mode.id}`, errors)
    assert(mode.summary, `assistant confirmation mode ${mode.id} missing summary`, errors)
  }

  assertUnique([...assistantSelfActionPolicyIds], 'assistant self action policy', errors)
  for (const policy of assistantRiskPolicy.selfActionPolicies || []) {
    assert(ROLE_ID_RE.test(policy.id), `assistant self action policy id has invalid format: ${policy.id}`, errors)
    assert(policy.summary, `assistant self action policy ${policy.id} missing summary`, errors)
  }

  assertUnique([...assistantActionHardDenyIds], 'assistant action hard deny', errors)
  for (const deny of assistantRiskPolicy.hardDenies || []) {
    assert(RULE_ID_RE.test(deny.id), `assistant action hard deny id has invalid format: ${deny.id}`, errors)
    for (const actionId of deny.safeAlternatives || []) {
      assert(assistantActionIds.has(actionId), `assistant action hard deny ${deny.id} references unknown safe alternative: ${actionId}`, errors)
    }
  }

  assertUnique((assistantRiskPolicy.globalRules || []).map((rule) => rule.id), 'assistant risk global rule', errors)
  for (const rule of assistantRiskPolicy.globalRules || []) {
    assert(RULE_ID_RE.test(rule.id), `assistant risk global rule id has invalid format: ${rule.id}`, errors)
    assert(['error', 'warning'].includes(rule.severity), `assistant risk global rule ${rule.id} invalid severity`, errors)
    assert(rule.summary, `assistant risk global rule ${rule.id} missing summary`, errors)
  }

  assertUnique([...assistantAppIds], 'assistant app', errors)
  for (const app of assistantActions.apps || []) {
    assert(ROLE_ID_RE.test(app.id), `assistant app id has invalid format: ${app.id}`, errors)
    assert(app.name, `assistant app ${app.id} missing name`, errors)
    assert(app.adapterId, `assistant app ${app.id} missing adapterId`, errors)
    assertEntityRefs(app.authorityEntityIds, entityById, `assistant app ${app.id}.authorityEntityIds`, errors)
  }

  assertUnique([...assistantAdapterStatuses], 'assistant adapter status', errors)
  for (const status of assistantActions.adapterStatuses || []) {
    assert(ROLE_ID_RE.test(status), `assistant adapter status has invalid format: ${status}`, errors)
  }

  assertUnique([...assistantActionIds], 'assistant action', errors)
  for (const action of assistantActions.actions || []) {
    assert(RULE_ID_RE.test(action.id), `assistant action id has invalid format: ${action.id}`, errors)
    assert(assistantAppIds.has(action.app), `assistant action ${action.id} references unknown app: ${action.app}`, errors)
    assert(action.domain, `assistant action ${action.id} missing domain`, errors)
    assert(Array.isArray(action.intentAliases), `assistant action ${action.id} intentAliases must be array`, errors)
    assertEntityRefs(action.entityIds, entityById, `assistant action ${action.id}.entityIds`, errors)
    for (const capabilityId of action.capabilityIds || []) {
      assert(assistantCapabilityIds.has(capabilityId), `assistant action ${action.id} references unknown capability: ${capabilityId}`, errors)
    }
    assert(assistantRiskLevelIds.has(action.riskLevel), `assistant action ${action.id} references unknown riskLevel: ${action.riskLevel}`, errors)
    assert(assistantConfirmationModeIds.has(action.confirmationMode), `assistant action ${action.id} references unknown confirmationMode: ${action.confirmationMode}`, errors)
    assert(assistantSelfActionPolicyIds.has(action.selfActionPolicy), `assistant action ${action.id} references unknown selfActionPolicy: ${action.selfActionPolicy}`, errors)
    assert(action.adapterId, `assistant action ${action.id} missing adapterId`, errors)
    assert(assistantAdapterStatuses.has(action.adapterStatus), `assistant action ${action.id} references unknown adapterStatus: ${action.adapterStatus}`, errors)
    for (const denyId of action.hardDenyIds || []) {
      assert(assistantActionHardDenyIds.has(denyId), `assistant action ${action.id} references unknown hard deny: ${denyId}`, errors)
    }
    for (const actionId of action.safeAlternativeActionIds || []) {
      assert(assistantActionIds.has(actionId), `assistant action ${action.id} references unknown safe alternative action: ${actionId}`, errors)
    }
    if (action.riskLevel === 'destructive') {
      assert(action.confirmationMode === 'forbidden', `assistant action ${action.id} destructive action must use confirmationMode=forbidden`, errors)
      assert(action.adapterStatus === 'forbidden', `assistant action ${action.id} destructive action must use adapterStatus=forbidden`, errors)
    }
    if (action.confirmationMode === 'forbidden') {
      assert(action.refusal, `assistant action ${action.id} forbidden action missing refusal`, errors)
    }
    if (action.riskLevel === 'privileged') {
      assert(Array.isArray(action.requiredScopes) && action.requiredScopes.length > 0, `assistant action ${action.id} privileged action missing requiredScopes`, errors)
    }
  }

  const contextStatuses = new Set(contextBindings.contextStatuses || [])
  const bindingStatuses = new Set(contextBindings.bindingStatuses || [])

  assertUnique(contextBindings.contexts.map((context) => context.id), 'context', errors)
  for (const context of contextBindings.contexts) {
    assert(RULE_ID_RE.test(context.id), `context id has invalid format: ${context.id}`, errors)
    assert(entityById.has(context.entityId), `context ${context.id} references unknown entity: ${context.entityId}`, errors)
    assert(contextStatuses.has(context.status), `context ${context.id} has invalid status: ${context.status}`, errors)
    assert(context.surface, `context ${context.id} missing surface`, errors)
    assert(context.sourceSystem, `context ${context.id} missing sourceSystem`, errors)
    assertRefsObject(context.refs, `context ${context.id}`, errors)
    if (context.parentContextId) {
      assertContextRefs([context.parentContextId], contextById, `context ${context.id}`, errors)
    }
  }

  assertUnique(contextBindings.bindingTypes.map((bindingType) => bindingType.id), 'binding type', errors)
  for (const bindingType of contextBindings.bindingTypes) {
    assert(RULE_ID_RE.test(bindingType.id), `binding type id has invalid format: ${bindingType.id}`, errors)
    assertRelationRef(bindingType.relationId, relationById, `binding type ${bindingType.id}`, errors)
    assertEntityRefs(bindingType.fromEntityIds, entityById, `binding type ${bindingType.id}.fromEntityIds`, errors)
    assertEntityRefs(bindingType.toEntityIds, entityById, `binding type ${bindingType.id}.toEntityIds`, errors)
  }

  assertUnique(contextBindings.bindings.map((binding) => binding.id), 'binding', errors)
  for (const binding of contextBindings.bindings) {
    assert(RULE_ID_RE.test(binding.id), `binding id has invalid format: ${binding.id}`, errors)
    assert(bindingStatuses.has(binding.status), `binding ${binding.id} has invalid status: ${binding.status}`, errors)
    assert(bindingTypeById.has(binding.typeId), `binding ${binding.id} references unknown binding type: ${binding.typeId}`, errors)
    assertContextRefs([binding.fromContextId, binding.toContextId], contextById, `binding ${binding.id}`, errors)
  }

  return {
    ok: errors.length === 0,
    errors,
    warnings,
    counts: {
      entities: entities.entities.length,
      relations: relations.relations.length,
      aliases: aliases.aliases.length,
      guardrails: guardrails.rules.length,
      evidenceLedgers: evidence.ledgers.length,
      resolverRules: resolverRules.rules.length,
      assistantRoles: assistantAccessPolicy.roleVocabulary.length,
      assistantCapabilities: assistantAccessPolicy.capabilities.length,
      assistantOperations: assistantAccessPolicy.operations.length,
      assistantActions: assistantActions.actions.length,
      assistantApps: assistantActions.apps.length,
      assistantRiskRules: assistantRiskPolicy.globalRules.length,
      contexts: contextBindings.contexts.length,
      bindingTypes: contextBindings.bindingTypes.length,
      bindings: contextBindings.bindings.length,
      domainPackages: catalog.domainPackages.length,
    },
  }
}

function validateValueContract(valueContract, entityId, errors) {
  if (valueContract === undefined) return
  const label = `entity ${entityId}.valueContract`
  assert(valueContract && typeof valueContract === 'object' && !Array.isArray(valueContract), `${label} must be object`, errors)
  if (!valueContract || typeof valueContract !== 'object' || Array.isArray(valueContract)) return
  assert(FIELD_ID_RE.test(valueContract.field || ''), `${label}.field invalid`, errors)
  assert(Array.isArray(valueContract.values) && valueContract.values.length > 0, `${label}.values must be non-empty array`, errors)
  const values = []
  for (const [index, item] of (valueContract.values || []).entries()) {
    const itemLabel = `${label}.values[${index}]`
    assert(item && typeof item === 'object' && !Array.isArray(item), `${itemLabel} must be object`, errors)
    if (!item || typeof item !== 'object' || Array.isArray(item)) continue
    assert(FIELD_ID_RE.test(item.value || ''), `${itemLabel}.value invalid`, errors)
    assert(typeof item.label === 'string' && item.label.trim(), `${itemLabel}.label required`, errors)
    assert(typeof item.sourceLabel === 'string' && item.sourceLabel.trim(), `${itemLabel}.sourceLabel required`, errors)
    values.push(item.value)
  }
  assert(new Set(values).size === values.length, `${label}.values duplicate value`, errors)
  for (const key of ['sourcePaths', 'rules']) {
    assert(Array.isArray(valueContract[key]) && valueContract[key].length > 0, `${label}.${key} must be non-empty array`, errors)
    for (const item of valueContract[key] || []) {
      assert(typeof item === 'string' && item.trim(), `${label}.${key} contains empty item`, errors)
    }
  }
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const result = await validateCatalog()
  if (!result.ok) {
    console.error(JSON.stringify(result, null, 2))
    process.exit(1)
  }
  console.log(JSON.stringify(result, null, 2))
}
