import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
export const serviceRoot = path.resolve(__dirname, '..')
export const catalogRoot = path.resolve(
  process.env.NODEDC_ONTOLOGY_CATALOG_ROOT || path.join(serviceRoot, 'catalog'),
)
export const domainPackagesRoot = path.join(catalogRoot, 'domain-packages')

export async function readJson(relativePath) {
  const relative = String(relativePath || '').replace(/^catalog\//, '')
  const fullPath = path.join(catalogRoot, relative)
  const raw = await fs.readFile(fullPath, 'utf8')
  return JSON.parse(raw)
}

async function readJsonAt(fullPath) {
  const raw = await fs.readFile(fullPath, 'utf8')
  return JSON.parse(raw)
}

async function readOptionalJsonAt(fullPath, fallback) {
  try {
    return await readJsonAt(fullPath)
  } catch (error) {
    if (error?.code === 'ENOENT') return fallback
    throw error
  }
}

async function listDomainPackageDirs() {
  try {
    const entries = await fs.readdir(domainPackagesRoot, { withFileTypes: true })
    return entries
      .filter((entry) => entry.isDirectory())
      .map((entry) => path.join(domainPackagesRoot, entry.name))
      .sort()
  } catch (error) {
    if (error?.code === 'ENOENT') return []
    throw error
  }
}

function uniqueByValue(values) {
  return [...new Set(values)]
}

function mergeRoleMatrix(baseMatrix = [], patchMatrix = []) {
  const byRole = new Map(baseMatrix.map((entry) => [entry.roleId, { ...entry }]))

  for (const patch of patchMatrix || []) {
    const current = byRole.get(patch.roleId) || { roleId: patch.roleId }
    byRole.set(patch.roleId, {
      ...current,
      ...patch,
      allowedCapabilities: uniqueByValue([
        ...(current.allowedCapabilities || []),
        ...(patch.allowedCapabilities || []),
      ]),
      deniedCapabilities: uniqueByValue([
        ...(current.deniedCapabilities || []),
        ...(patch.deniedCapabilities || []),
      ]),
      deniedWhenNoAdminScope: uniqueByValue([
        ...(current.deniedWhenNoAdminScope || []),
        ...(patch.deniedWhenNoAdminScope || []),
      ]),
      conditionalCapabilities: [
        ...(current.conditionalCapabilities || []),
        ...(patch.conditionalCapabilities || []),
      ],
    })
  }

  return [...byRole.values()]
}

function mergeAssistantAccessPolicy(base, patch = {}) {
  return {
    ...base,
    roleVocabulary: [...(base.roleVocabulary || []), ...(patch.roleVocabulary || [])],
    roleAliases: [...(base.roleAliases || []), ...(patch.roleAliases || [])],
    sourceAuthorities: [...(base.sourceAuthorities || []), ...(patch.sourceAuthorities || [])],
    capabilities: [...(base.capabilities || []), ...(patch.capabilities || [])],
    roleMatrix: mergeRoleMatrix(base.roleMatrix, patch.roleMatrix),
    operations: [...(base.operations || []), ...(patch.operations || [])],
    hardDenies: [...(base.hardDenies || []), ...(patch.hardDenies || [])],
  }
}

function mergeCatalog(base, extension) {
  const next = {
    ...base,
    entities: {
      ...base.entities,
      statusVocabulary: uniqueByValue([
        ...(base.entities.statusVocabulary || []),
        ...(extension.entities.statusVocabulary || []),
      ]),
      entities: [...(base.entities.entities || []), ...(extension.entities.entities || [])],
    },
    relations: {
      ...base.relations,
      relations: [...(base.relations.relations || []), ...(extension.relations.relations || [])],
    },
    aliases: {
      ...base.aliases,
      aliases: [...(base.aliases.aliases || []), ...(extension.aliases.aliases || [])],
    },
    guardrails: {
      ...base.guardrails,
      rules: [...(base.guardrails.rules || []), ...(extension.guardrails.rules || [])],
      blockedConflations: [
        ...(base.guardrails.blockedConflations || []),
        ...(extension.guardrails.blockedConflations || []),
      ],
    },
    evidence: {
      ...base.evidence,
      sourceRoots: [...(base.evidence.sourceRoots || []), ...(extension.evidence.sourceRoots || [])],
      ledgers: [...(base.evidence.ledgers || []), ...(extension.evidence.ledgers || [])],
      baselineDocs: [...(base.evidence.baselineDocs || []), ...(extension.evidence.baselineDocs || [])],
      restrictions: [...(base.evidence.restrictions || []), ...(extension.evidence.restrictions || [])],
    },
    resolverRules: {
      ...base.resolverRules,
      rules: [...(base.resolverRules.rules || []), ...(extension.resolverRules.rules || [])],
    },
    assistantAccessPolicy: mergeAssistantAccessPolicy(
      base.assistantAccessPolicy,
      extension.assistantAccessPolicy,
    ),
    assistantActions: {
      ...base.assistantActions,
      apps: [...(base.assistantActions.apps || []), ...(extension.assistantActions.apps || [])],
      adapterStatuses: uniqueByValue([
        ...(base.assistantActions.adapterStatuses || []),
        ...(extension.assistantActions.adapterStatuses || []),
      ]),
      actions: [...(base.assistantActions.actions || []), ...(extension.assistantActions.actions || [])],
    },
    assistantRiskPolicy: {
      ...base.assistantRiskPolicy,
      riskLevels: [...(base.assistantRiskPolicy.riskLevels || []), ...(extension.assistantRiskPolicy.riskLevels || [])],
      confirmationModes: [
        ...(base.assistantRiskPolicy.confirmationModes || []),
        ...(extension.assistantRiskPolicy.confirmationModes || []),
      ],
      selfActionPolicies: [
        ...(base.assistantRiskPolicy.selfActionPolicies || []),
        ...(extension.assistantRiskPolicy.selfActionPolicies || []),
      ],
      hardDenies: [...(base.assistantRiskPolicy.hardDenies || []), ...(extension.assistantRiskPolicy.hardDenies || [])],
      globalRules: [
        ...(base.assistantRiskPolicy.globalRules || []),
        ...(extension.assistantRiskPolicy.globalRules || []),
      ],
    },
    contextBindings: {
      ...base.contextBindings,
      contextStatuses: uniqueByValue([
        ...(base.contextBindings.contextStatuses || []),
        ...(extension.contextBindings.contextStatuses || []),
      ]),
      bindingStatuses: uniqueByValue([
        ...(base.contextBindings.bindingStatuses || []),
        ...(extension.contextBindings.bindingStatuses || []),
      ]),
      contexts: [...(base.contextBindings.contexts || []), ...(extension.contextBindings.contexts || [])],
      bindingTypes: [
        ...(base.contextBindings.bindingTypes || []),
        ...(extension.contextBindings.bindingTypes || []),
      ],
      bindings: [...(base.contextBindings.bindings || []), ...(extension.contextBindings.bindings || [])],
    },
    domainPackages: [...(base.domainPackages || []), ...(extension.domainPackages || [])],
  }

  return next
}

async function loadDomainPackage(packageDir) {
  const relativeDir = path.relative(catalogRoot, packageDir)
  const metadata = await readOptionalJsonAt(path.join(packageDir, 'package.json'), {})

  return {
    entities: await readOptionalJsonAt(path.join(packageDir, 'entities.json'), { statusVocabulary: [], entities: [] }),
    relations: await readOptionalJsonAt(path.join(packageDir, 'relations.json'), { relations: [] }),
    aliases: await readOptionalJsonAt(path.join(packageDir, 'aliases.json'), { aliases: [] }),
    guardrails: await readOptionalJsonAt(path.join(packageDir, 'guardrails.json'), { rules: [], blockedConflations: [] }),
    evidence: await readOptionalJsonAt(path.join(packageDir, 'evidence.json'), {
      sourceRoots: [],
      ledgers: [],
      baselineDocs: [],
      restrictions: [],
    }),
    resolverRules: await readOptionalJsonAt(path.join(packageDir, 'resolver-rules.json'), { rules: [] }),
    assistantAccessPolicy: await readOptionalJsonAt(path.join(packageDir, 'assistant-access-policy.json'), {
      roleVocabulary: [],
      roleAliases: [],
      sourceAuthorities: [],
      capabilities: [],
      roleMatrix: [],
      operations: [],
      hardDenies: [],
    }),
    assistantActions: await readOptionalJsonAt(path.join(packageDir, 'assistant-actions.json'), {
      apps: [],
      adapterStatuses: [],
      actions: [],
    }),
    assistantRiskPolicy: await readOptionalJsonAt(path.join(packageDir, 'assistant-risk-policy.json'), {
      riskLevels: [],
      confirmationModes: [],
      selfActionPolicies: [],
      hardDenies: [],
      globalRules: [],
    }),
    contextBindings: await readOptionalJsonAt(path.join(packageDir, 'context-bindings.json'), {
      contextStatuses: [],
      bindingStatuses: [],
      contexts: [],
      bindingTypes: [],
      bindings: [],
    }),
    domainPackages: [{
      id: metadata.id || path.basename(packageDir),
      version: metadata.version || '',
      updatedAt: metadata.updatedAt || '',
      status: metadata.status || 'unknown',
      relativePath: relativeDir,
      summary: metadata.summary || '',
    }],
  }
}

async function loadDomainPackages() {
  const packageDirs = await listDomainPackageDirs()
  return Promise.all(packageDirs.map((packageDir) => loadDomainPackage(packageDir)))
}

export async function loadCatalog() {
  const [
    entities,
    relations,
    aliases,
    guardrails,
    evidence,
    resolverRules,
    assistantAccessPolicy,
    assistantActions,
    assistantRiskPolicy,
  ] = await Promise.all([
    readJson('catalog/entities.json'),
    readJson('catalog/relations.json'),
    readJson('catalog/aliases.json'),
    readJson('catalog/guardrails.json'),
    readJson('catalog/evidence.json'),
    readJson('catalog/resolver-rules.json'),
    readJson('catalog/assistant-access-policy.json'),
    readJson('catalog/assistant-actions.json'),
    readJson('catalog/assistant-risk-policy.json'),
  ])
  const contextBindings = await readJson('catalog/context-bindings.json')

  const domainPackages = await loadDomainPackages()
  const mergedCatalog = domainPackages.reduce((catalog, extension) => mergeCatalog(catalog, extension), {
    entities,
    relations,
    aliases,
    guardrails,
    evidence,
    resolverRules,
    assistantAccessPolicy,
    assistantActions,
    assistantRiskPolicy,
    contextBindings,
    domainPackages: [],
  })

  const entityById = new Map(mergedCatalog.entities.entities.map((entity) => [entity.id, entity]))
  const relationById = new Map(mergedCatalog.relations.relations.map((relation) => [relation.id, relation]))
  const contextById = new Map(mergedCatalog.contextBindings.contexts.map((context) => [context.id, context]))
  const bindingTypeById = new Map(mergedCatalog.contextBindings.bindingTypes.map((bindingType) => [bindingType.id, bindingType]))
  const aliasByKey = new Map(
    mergedCatalog.aliases.aliases.map((alias) => [normalizeAlias(alias.alias), alias.canonicalId]),
  )

  return {
    ...mergedCatalog,
    entityById,
    relationById,
    contextById,
    bindingTypeById,
    aliasByKey,
  }
}

export function normalizeAlias(value) {
  return String(value || '').trim().toLowerCase()
}

export function canonicalizeEntityId(value, catalog) {
  const raw = String(value || '').trim()
  if (!raw) return ''
  if (catalog.entityById.has(raw)) return raw
  return catalog.aliasByKey.get(normalizeAlias(raw)) || raw
}
