import fs from 'node:fs/promises'
import { canonicalizeEntityId, loadCatalog } from './catalog.mjs'
import { validateCatalog } from './validate.mjs'

function tokenize(value) {
  return String(value || '')
    .toLowerCase()
    .split(/[^a-zа-яё0-9_]+/iu)
    .filter(Boolean)
}

function scoreRule(rule, input) {
  let score = 0
  const entityId = input.entityId || ''
  if (entityId && rule.inputEntityIds.includes(entityId)) score += 10
  if (input.surface && input.surface === rule.fromSurface) score += 6
  if (score === 0) return 0

  const intentTokens = new Set(tokenize(input.intent))
  for (const hint of rule.intentHints || []) {
    const hintTokens = tokenize(hint)
    if (hintTokens.some((token) => intentTokens.has(token))) score += 2
  }
  return score
}

function refMatches(candidateRefs = {}, inputRefs = {}) {
  const candidateEntries = Object.entries(candidateRefs || {}).filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== '')
  const inputEntries = Object.entries(inputRefs || {}).filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== '')
  if (!candidateEntries.length || !inputEntries.length) return false

  const matches = (entries, refs) => entries.every(([key, value]) => String(refs[key] || '') === String(value))
  const candidateInsideInput = matches(candidateEntries, inputRefs)
  const inputInsideCandidate = matches(inputEntries, candidateRefs)
  if (!candidateInsideInput && !inputInsideCandidate) return false

  const matchedKeys = candidateEntries
    .map(([key]) => key)
    .filter((key) => inputRefs[key] !== undefined && String(inputRefs[key]) === String(candidateRefs[key]))
  const hasStrongRef = ['issue_id', 'workflow_id', 'runtime_workflow_id', 'node_id', 'grant_id'].some((key) => matchedKeys.includes(key))
  return hasStrongRef || matchedKeys.length >= 2
}

function contextScore(context, input) {
  let score = 0
  if (input.contextId && input.contextId === context.id) score += 100
  if (refMatches(context.refs, input.refs)) score += 40
  if (score === 0) return 0
  if (input.surface && input.surface === context.surface) score += 10
  if (input.entityId && input.entityId === context.entityId) score += 10
  return score
}

function findMatchingContexts(catalog, input) {
  return catalog.contextBindings.contexts
    .map((context) => ({ context, score: contextScore(context, input) }))
    .filter((item) => item.score > 0)
    .sort((a, b) => b.score - a.score || a.context.id.localeCompare(b.context.id))
}

function bindingTouchesContext(binding, contextIds) {
  return contextIds.has(binding.fromContextId) || contextIds.has(binding.toContextId)
}

function findBindingsForContexts(catalog, contexts) {
  const contextIds = new Set(contexts.map(({ context }) => context.id))
  return catalog.contextBindings.bindings
    .filter((binding) => bindingTouchesContext(binding, contextIds))
    .map((binding) => ({
      ...binding,
      type: catalog.bindingTypeById.get(binding.typeId) || null,
      fromContext: catalog.contextById.get(binding.fromContextId) || null,
      toContext: catalog.contextById.get(binding.toContextId) || null,
    }))
}

function findRequiredBindingTypes(catalog, selectedRule) {
  if (!selectedRule) return []
  return catalog.contextBindings.bindingTypes.filter((bindingType) => bindingType.relationId === selectedRule.relationId)
}

export async function resolveContext(input = {}) {
  const catalog = await loadCatalog()
  const entityId = canonicalizeEntityId(input.entityId || input.alias || input.term, catalog)
  const normalizedInput = {
    ...input,
    entityId,
  }
  const matchedContexts = findMatchingContexts(catalog, normalizedInput)
  if (!normalizedInput.entityId && matchedContexts[0]?.context?.entityId) {
    normalizedInput.entityId = matchedContexts[0].context.entityId
  }

  const candidates = catalog.resolverRules.rules
    .map((rule) => ({ rule, score: scoreRule(rule, normalizedInput) }))
    .filter((item) => item.score > 0)
    .sort((a, b) => b.score - a.score || a.rule.id.localeCompare(b.rule.id))

  const selected = candidates[0]?.rule || null
  const bindings = findBindingsForContexts(catalog, matchedContexts)
  const requiredBindingTypes = findRequiredBindingTypes(catalog, selected)
  const existingBindingTypeIds = new Set(bindings.map((binding) => binding.typeId))
  const missingBindingTypes = requiredBindingTypes.filter((bindingType) => !existingBindingTypeIds.has(bindingType.id))

  return {
    input: normalizedInput,
    canonicalEntity: catalog.entityById.get(normalizedInput.entityId) || null,
    matchedContexts: matchedContexts.map(({ context, score }) => ({
      id: context.id,
      score,
      surface: context.surface,
      entityId: context.entityId,
      label: context.label,
      sourceSystem: context.sourceSystem,
      refs: context.refs,
      status: context.status,
    })),
    bindings: bindings.map((binding) => ({
      id: binding.id,
      typeId: binding.typeId,
      status: binding.status,
      fromContextId: binding.fromContextId,
      toContextId: binding.toContextId,
      relationId: binding.type?.relationId || null,
      summary: binding.summary || binding.type?.summary || '',
    })),
    missingBindingTypes: missingBindingTypes.map((bindingType) => ({
      id: bindingType.id,
      relationId: bindingType.relationId,
      requiredFromRefs: bindingType.requiredFromRefs || [],
      requiredToRefs: bindingType.requiredToRefs || [],
      summary: bindingType.summary,
    })),
    selectedRule: selected,
    candidates: candidates.map(({ rule, score }) => ({
      id: rule.id,
      score,
      outputSurface: rule.outputSurface,
      outputEntityIds: rule.outputEntityIds,
      requiredBindings: rule.requiredBindings,
      summary: rule.summary,
    })),
  }
}

async function resolveFromCli() {
  const inputIndex = process.argv.indexOf('--input')
  const inputJsonIndex = process.argv.indexOf('--input-json')
  let input = null

  if (inputIndex >= 0) {
    input = JSON.parse(process.argv[inputIndex + 1] || '{}')
  } else if (inputJsonIndex >= 0) {
    const raw = await fs.readFile(process.argv[inputJsonIndex + 1], 'utf8')
    input = JSON.parse(raw)
  }

  if (!input) {
    console.error('Usage: node src/resolver.mjs --input-json examples/ops-card-to-engine-request.json')
    process.exit(2)
  }

  const out = await resolveContext(input)
  console.log(JSON.stringify(out, null, 2))
}

async function runSmoke() {
  const validation = await validateCatalog()
  if (!validation.ok) {
    console.error(JSON.stringify(validation, null, 2))
    process.exit(1)
  }

  const samples = [
    { surface: 'ops', alias: 'issue', intent: 'inspect engine workflow for this card' },
    {
      surface: 'ops',
      alias: 'issue',
      intent: 'inspect engine workflow for this ontology card',
      refs: {
        workspace_slug: 'nodedc',
        project_id: '86629a11-eaff-4ad2-9f89-e5245a344fcc',
        issue_id: '1312519a-0eda-4ea0-9e34-2113c3724ecf',
      },
    },
    { surface: 'engine', entityId: 'engine.workflow_l1', intent: 'создай карточку в ops по текущему workflow' },
    { surface: 'assistant', entityId: 'assistant.bridge', intent: 'select mcp gateway grant and tool' },
    { term: 'runtime workflow id', intent: 'runtime workflow id lookup' },
  ]

  const results = []
  for (const sample of samples) {
    const out = await resolveContext(sample)
    results.push({
      sample,
      canonicalEntityId: out.canonicalEntity?.id || out.input.entityId || null,
      selectedRuleId: out.selectedRule?.id || null,
      outputSurface: out.selectedRule?.outputSurface || null,
      outputEntityIds: out.selectedRule?.outputEntityIds || [],
      matchedContextIds: out.matchedContexts.map((context) => context.id),
      missingBindingTypeIds: out.missingBindingTypes.map((bindingType) => bindingType.id),
    })
  }

  console.log(JSON.stringify({ ok: true, validation: validation.counts, results }, null, 2))
}

if (import.meta.url === `file://${process.argv[1]}` && process.argv.includes('--smoke')) {
  await runSmoke()
} else if (import.meta.url === `file://${process.argv[1]}`) {
  await resolveFromCli()
}
