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

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const contextBindingsPath = path.resolve(__dirname, '..', 'catalog', 'context-bindings.json')

function usage() {
  return [
    'Usage:',
    '  node src/registry.mjs list-contexts',
    '  node src/registry.mjs list-bindings',
    '  node src/registry.mjs add-context --input-json examples/engine-context.example.json [--dry-run]',
    '  node src/registry.mjs add-binding --input-json examples/ops-engine-binding.example.json [--dry-run]',
    '  node src/registry.mjs import-engine-context --input-json examples/engine-workflow-manifest.example.json [--dry-run]',
  ].join('\n')
}

function argValue(name) {
  const index = process.argv.indexOf(name)
  if (index < 0) return ''
  return process.argv[index + 1] || ''
}

async function readInputJson() {
  const inputPath = argValue('--input-json')
  const inputRaw = argValue('--input')
  if (inputRaw) return JSON.parse(inputRaw)
  if (!inputPath) throw new Error('missing --input-json or --input')
  const fullPath = path.resolve(process.cwd(), inputPath)
  return JSON.parse(await fs.readFile(fullPath, 'utf8'))
}

async function readContextBindings() {
  return JSON.parse(await fs.readFile(contextBindingsPath, 'utf8'))
}

async function writeContextBindings(next) {
  const raw = `${JSON.stringify(next, null, 2)}\n`
  await fs.writeFile(contextBindingsPath, raw, 'utf8')
}

function required(value, label) {
  if (value === undefined || value === null || String(value).trim() === '') {
    throw new Error(`${label} is required`)
  }
}

function isObject(value) {
  return value && typeof value === 'object' && !Array.isArray(value)
}

function slugPart(value) {
  return String(value || '')
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9а-яё]+/giu, '_')
    .replace(/^_+|_+$/g, '')
    .slice(0, 64) || 'unknown'
}

function validateDraftContext(draft, catalog, registry) {
  required(draft.id, 'context.id')
  required(draft.surface, 'context.surface')
  required(draft.entityId, 'context.entityId')
  required(draft.sourceSystem, 'context.sourceSystem')
  required(draft.label, 'context.label')
  required(draft.status, 'context.status')
  if (!catalog.entityById.has(draft.entityId)) throw new Error(`unknown context.entityId: ${draft.entityId}`)
  if (!registry.contextStatuses.includes(draft.status)) throw new Error(`invalid context.status: ${draft.status}`)
  if (!isObject(draft.refs)) throw new Error('context.refs must be an object')
  if (draft.parentContextId && !catalog.contextById.has(draft.parentContextId)) {
    throw new Error(`unknown context.parentContextId: ${draft.parentContextId}`)
  }
}

function validateDraftBinding(draft, catalog, registry) {
  required(draft.id, 'binding.id')
  required(draft.typeId, 'binding.typeId')
  required(draft.status, 'binding.status')
  required(draft.fromContextId, 'binding.fromContextId')
  required(draft.toContextId, 'binding.toContextId')
  if (!registry.bindingStatuses.includes(draft.status)) throw new Error(`invalid binding.status: ${draft.status}`)
  if (!catalog.bindingTypeById.has(draft.typeId)) throw new Error(`unknown binding.typeId: ${draft.typeId}`)
  if (!catalog.contextById.has(draft.fromContextId)) throw new Error(`unknown binding.fromContextId: ${draft.fromContextId}`)
  if (!catalog.contextById.has(draft.toContextId)) throw new Error(`unknown binding.toContextId: ${draft.toContextId}`)
}

function validateDraftBindingWithKnownContextIds(draft, catalog, registry, knownContextIds) {
  required(draft.id, 'binding.id')
  required(draft.typeId, 'binding.typeId')
  required(draft.status, 'binding.status')
  required(draft.fromContextId, 'binding.fromContextId')
  required(draft.toContextId, 'binding.toContextId')
  if (!registry.bindingStatuses.includes(draft.status)) throw new Error(`invalid binding.status: ${draft.status}`)
  if (!catalog.bindingTypeById.has(draft.typeId)) throw new Error(`unknown binding.typeId: ${draft.typeId}`)
  if (!knownContextIds.has(draft.fromContextId)) throw new Error(`unknown binding.fromContextId: ${draft.fromContextId}`)
  if (!knownContextIds.has(draft.toContextId)) throw new Error(`unknown binding.toContextId: ${draft.toContextId}`)
}

function upsertById(list, item) {
  const index = list.findIndex((existing) => existing.id === item.id)
  if (index >= 0) {
    const next = [...list]
    next[index] = item
    return { list: next, action: 'updated' }
  }
  return { list: [...list, item], action: 'created' }
}

async function updateRegistry(mutator, dryRun) {
  const beforeRaw = await fs.readFile(contextBindingsPath, 'utf8')
  const registry = JSON.parse(beforeRaw)
  const next = structuredClone(registry)
  const result = mutator(next)

  if (dryRun) return { dryRun: true, ...result, registry: next }

  await writeContextBindings(next)
  const validation = await validateCatalog()
  if (!validation.ok) {
    await fs.writeFile(contextBindingsPath, beforeRaw, 'utf8')
    const err = new Error('registry validation failed; reverted context-bindings.json')
    err.validation = validation
    throw err
  }
  return { dryRun: false, ...result, validation: validation.counts }
}

async function listContexts() {
  const catalog = await loadCatalog()
  return catalog.contextBindings.contexts.map((context) => ({
    id: context.id,
    surface: context.surface,
    entityId: context.entityId,
    sourceSystem: context.sourceSystem,
    label: context.label,
    status: context.status,
    refs: context.refs,
    parentContextId: context.parentContextId || null,
  }))
}

async function listBindings() {
  const catalog = await loadCatalog()
  return catalog.contextBindings.bindings.map((binding) => ({
    id: binding.id,
    typeId: binding.typeId,
    status: binding.status,
    fromContextId: binding.fromContextId,
    toContextId: binding.toContextId,
    summary: binding.summary || '',
  }))
}

async function addContext(dryRun) {
  const draft = await readInputJson()
  const catalog = await loadCatalog()
  const registry = await readContextBindings()
  validateDraftContext(draft, catalog, registry)
  return updateRegistry((next) => {
    const out = upsertById(next.contexts, draft)
    next.contexts = out.list
    return { action: out.action, context: draft }
  }, dryRun)
}

async function addBinding(dryRun) {
  const draft = await readInputJson()
  const catalog = await loadCatalog()
  const registry = await readContextBindings()
  validateDraftBinding(draft, catalog, registry)
  return updateRegistry((next) => {
    const out = upsertById(next.bindings, draft)
    next.bindings = out.list
    return { action: out.action, binding: draft }
  }, dryRun)
}

function buildEngineContextFromManifest(manifest) {
  if (!isObject(manifest)) throw new Error('manifest must be an object')
  const workflow = manifest.workflow || {}
  required(workflow.id, 'workflow.id')
  required(workflow.label, 'workflow.label')

  const refs = {
    workflow_id: String(workflow.id),
  }
  if (workflow.nodeId) refs.node_id = String(workflow.nodeId)
  if (workflow.runtimeWorkflowId) refs.runtime_workflow_id = String(workflow.runtimeWorkflowId)
  if (workflow.runtimeInstanceId) refs.runtime_instance_id = String(workflow.runtimeInstanceId)

  return {
    id: `context.engine.${slugPart(workflow.id)}`,
    surface: 'engine',
    entityId: workflow.runtimeWorkflowId ? 'engine.workflow_l2' : 'engine.workflow_l1',
    sourceSystem: String(manifest.context?.sourceSystem || 'nodedc-engine'),
    label: String(workflow.label),
    status: String(manifest.context?.status || 'planned'),
    refs,
  }
}

function buildOpsEngineBindingFromManifest(manifest, engineContext) {
  const bind = manifest.bindToOps || {}
  if (bind.enabled !== true) return null
  required(bind.fromContextId, 'bindToOps.fromContextId')
  return {
    id: `binding.ops.${slugPart(bind.fromContextId)}.engine.${slugPart(engineContext.id)}`,
    typeId: 'binding_type.ops_card_to_engine_workflow',
    status: String(bind.status || 'planned'),
    fromContextId: String(bind.fromContextId),
    toContextId: engineContext.id,
    summary: String(bind.summary || `Binds ${bind.fromContextId} to ${engineContext.id}.`),
  }
}

async function importEngineContext(dryRun) {
  const manifest = await readInputJson()
  const catalog = await loadCatalog()
  const registry = await readContextBindings()
  const engineContext = buildEngineContextFromManifest(manifest)
  const binding = buildOpsEngineBindingFromManifest(manifest, engineContext)

  validateDraftContext(engineContext, catalog, registry)
  if (binding) {
    const knownContextIds = new Set([
      ...catalog.contextById.keys(),
      engineContext.id,
    ])
    validateDraftBindingWithKnownContextIds(binding, catalog, registry, knownContextIds)
  }

  return updateRegistry((next) => {
    const contextOut = upsertById(next.contexts, engineContext)
    next.contexts = contextOut.list
    let bindingOut = null
    if (binding) {
      bindingOut = upsertById(next.bindings, binding)
      next.bindings = bindingOut.list
    }
    return {
      action: bindingOut ? `${contextOut.action}_context_${bindingOut.action}_binding` : `${contextOut.action}_context`,
      context: engineContext,
      binding,
    }
  }, dryRun)
}

async function main() {
  const command = process.argv[2]
  const dryRun = process.argv.includes('--dry-run')
  let output

  if (command === 'list-contexts') output = await listContexts()
  else if (command === 'list-bindings') output = await listBindings()
  else if (command === 'add-context') output = await addContext(dryRun)
  else if (command === 'add-binding') output = await addBinding(dryRun)
  else if (command === 'import-engine-context') output = await importEngineContext(dryRun)
  else {
    console.error(usage())
    process.exit(2)
  }

  console.log(JSON.stringify({ ok: true, serviceRoot, command, output }, null, 2))
}

if (import.meta.url === `file://${process.argv[1]}`) {
  main().catch((err) => {
    console.error(JSON.stringify({
      ok: false,
      error: err.message || String(err),
      validation: err.validation || undefined,
    }, null, 2))
    process.exit(1)
  })
}
