import fs from 'node:fs/promises'
import { loadCatalog } from './catalog.mjs'
import { resolveAssistantAccess } from './assistant-policy.mjs'
import { validateCatalog } from './validate.mjs'

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

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

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

function actionSearchText(action) {
  return [
    action.id,
    action.app,
    action.domain,
    action.summary,
    ...(action.intentAliases || []),
  ].join(' ')
}

function scoreAction(action, input) {
  if (input.actionId && input.actionId === action.id) return 1000

  const query = normalizeText(input.intent || input.term || input.text || input.query)
  if (!query) return 0

  const aliases = (action.intentAliases || []).map(normalizeText)
  if (aliases.includes(query)) return 200
  if (aliases.some((alias) => alias && query.includes(alias))) return 160

  const queryTokens = new Set(tokenize(query))
  if (!queryTokens.size) return 0

  const actionTokens = new Set(tokenize(actionSearchText(action)))
  let score = 0
  for (const token of queryTokens) {
    if (actionTokens.has(token)) score += 10
  }
  if (input.app && input.app === action.app) score += 20
  return score
}

function findAction(catalog, input) {
  const candidates = catalog.assistantActions.actions
    .map((action) => ({ action, score: scoreAction(action, input) }))
    .filter((candidate) => candidate.score > 0)
    .sort((a, b) => b.score - a.score || a.action.id.localeCompare(b.action.id))

  return {
    selected: candidates[0]?.action || null,
    candidates: candidates.slice(0, 5).map(({ action, score }) => ({
      id: action.id,
      app: action.app,
      riskLevel: action.riskLevel,
      confirmationMode: action.confirmationMode,
      adapterStatus: action.adapterStatus,
      score,
      summary: action.summary,
    })),
  }
}

function collectSafeAlternativeIds(action, riskPolicy) {
  const ids = [...(action.safeAlternativeActionIds || [])]
  for (const denyId of action.hardDenyIds || []) {
    const deny = riskPolicy.hardDenies.find((candidate) => candidate.id === denyId)
    ids.push(...(deny?.safeAlternatives || []))
  }
  return unique(ids.filter((id) => id !== action.id))
}

function describeAlternatives(ids, actionById) {
  return ids.map((id) => {
    const action = actionById.get(id)
    return {
      id,
      app: action?.app || null,
      riskLevel: action?.riskLevel || null,
      confirmationMode: action?.confirmationMode || null,
      summary: action?.summary || null,
    }
  })
}

function missingCapabilities(action, access) {
  const allowed = new Set(access.allowedCapabilities || [])
  return (action.capabilityIds || []).filter((capabilityId) => !allowed.has(capabilityId))
}

function isSelfTarget(input) {
  if (!input.actorUserId || !input.targetUserId) return false
  return String(input.actorUserId) === String(input.targetUserId)
}

function makeDecision(decision, action, input, access, extra = {}) {
  return {
    ok: decision === 'allowed',
    decision,
    action: action ? {
      id: action.id,
      app: action.app,
      domain: action.domain,
      riskLevel: action.riskLevel,
      confirmationMode: action.confirmationMode,
      selfActionPolicy: action.selfActionPolicy,
      adapterId: action.adapterId,
      adapterStatus: action.adapterStatus,
      requiredScopes: action.requiredScopes || [],
      capabilityIds: action.capabilityIds || [],
      summary: action.summary,
    } : null,
    actor: {
      assistantRole: input.assistantRole || null,
      effectiveRole: access?.effectiveRole || null,
      adminScope: access?.adminScope || null,
    },
    execution: action ? {
      policyAllowed: decision === 'allowed',
      adapterReady: action.adapterStatus === 'implemented',
      executionReady: decision === 'allowed' && action.adapterStatus === 'implemented',
      requiresConfirmation: action.confirmationMode === 'explicit' || action.confirmationMode === 'typed',
      confirmed: Boolean(input.confirmed),
    } : null,
    ...extra,
  }
}

export async function resolveAssistantAction(input = {}, options = {}) {
  const catalog = options.catalog || await loadCatalog()
  const actionById = new Map(catalog.assistantActions.actions.map((action) => [action.id, action]))
  const riskPolicy = catalog.assistantRiskPolicy
  const found = findAction(catalog, input)
  const action = found.selected

  if (!action) {
    return {
      ok: false,
      decision: 'action_not_registered',
      reason: 'Request did not resolve to a registered assistant action card.',
      candidates: found.candidates,
    }
  }

  const access = await resolveAssistantAccess(input, { policy: catalog.assistantAccessPolicy })
  const safeAlternativeIds = collectSafeAlternativeIds(action, riskPolicy)
  const safeAlternatives = describeAlternatives(safeAlternativeIds, actionById)

  if (action.riskLevel === 'destructive' || action.confirmationMode === 'forbidden' || action.adapterStatus === 'forbidden') {
    return makeDecision('forbidden', action, input, access, {
      reason: 'destructive_or_forbidden_action',
      refusal: action.refusal || 'Assistant action is forbidden by risk policy.',
      hardDenyIds: action.hardDenyIds || [],
      safeAlternatives,
      candidates: found.candidates,
    })
  }

  if (action.selfActionPolicy === 'blocked' && isSelfTarget(input)) {
    return makeDecision('denied', action, input, access, {
      reason: 'self_targeting_blocked',
      hardDenyIds: action.hardDenyIds || [],
      safeAlternatives,
      candidates: found.candidates,
    })
  }

  if (action.selfActionPolicy === 'no_self_lockout' && isSelfTarget(input)) {
    const removesOwnAccess = Boolean(input.removesOwnAccess || input.targetRole === 'assistant_blocked' || input.targetStatus === 'disabled' || input.globalStatus === 'blocked')
    const hasAlternativeAdminPath = Boolean(input.hasAlternativeAdminPath)
    if (removesOwnAccess && !hasAlternativeAdminPath) {
      return makeDecision('denied', action, input, access, {
        reason: 'self_lockout_denied',
        hardDenyIds: unique([...(action.hardDenyIds || []), 'assistant.action.deny.self_lockout']),
        safeAlternatives,
        candidates: found.candidates,
      })
    }
  }

  const missing = missingCapabilities(action, access)
  if (missing.length) {
    return makeDecision('denied', action, input, access, {
      reason: 'assistant_role_missing_capability',
      missingCapabilities: missing,
      safeAlternatives,
      candidates: found.candidates,
    })
  }

  if (action.riskLevel === 'privileged' && access.effectiveRole !== 'assistant_admin') {
    return makeDecision('denied', action, input, access, {
      reason: 'privileged_action_requires_assistant_admin',
      safeAlternatives,
      candidates: found.candidates,
    })
  }

  if (action.adapterStatus === 'future') {
    return makeDecision('future_adapter', action, input, access, {
      reason: 'app_owned_adapter_not_available_yet',
      safeAlternatives,
      candidates: found.candidates,
    })
  }

  if ((action.confirmationMode === 'explicit' || action.confirmationMode === 'typed') && !input.confirmed) {
    return makeDecision('needs_confirmation', action, input, access, {
      reason: 'explicit_confirmation_required',
      confirmationRequest: {
        actionId: action.id,
        app: action.app,
        targetUserId: input.targetUserId || null,
        expectedEffect: action.summary,
      },
      safeAlternatives,
      candidates: found.candidates,
    })
  }

  return makeDecision('allowed', action, input, access, {
    reason: action.adapterStatus === 'implemented' ? 'policy_and_adapter_ready' : 'policy_allowed_adapter_not_implemented',
    safeAlternatives,
    candidates: found.candidates,
  })
}

async function readInputJson() {
  const inputIndex = process.argv.indexOf('--input-json')
  if (inputIndex >= 0) {
    const inputPath = process.argv[inputIndex + 1]
    if (!inputPath) throw new Error('--input-json requires a path')
    return JSON.parse(await fs.readFile(inputPath, 'utf8'))
  }

  const inlineIndex = process.argv.indexOf('--input')
  if (inlineIndex >= 0) {
    return JSON.parse(process.argv[inlineIndex + 1] || '{}')
  }

  return null
}

function assertSmoke(condition, message) {
  if (!condition) throw new Error(message)
}

async function runSmoke() {
  const validation = await validateCatalog()
  assertSmoke(validation.ok, 'catalog must validate before assistant action smoke')

  const cases = {
    deleteUser: await resolveAssistantAction({
      actionId: 'hub.user.delete',
      assistantRole: 'admin',
      membershipRole: 'client_admin',
      actorUserId: 'user_root',
      targetUserId: 'user_pupa',
      confirmed: true,
    }),
    blockAsMember: await resolveAssistantAction({
      actionId: 'hub.user.block',
      assistantRole: 'member',
      membershipRole: 'member',
      actorUserId: 'user_root',
      targetUserId: 'user_pupa',
      confirmed: true,
    }),
    blockNeedsConfirmation: await resolveAssistantAction({
      actionId: 'hub.user.block',
      assistantRole: 'admin',
      membershipRole: 'client_admin',
      actorUserId: 'user_root',
      targetUserId: 'user_pupa',
    }),
    blockAllowed: await resolveAssistantAction({
      actionId: 'hub.user.block',
      assistantRole: 'admin',
      membershipRole: 'client_admin',
      actorUserId: 'user_root',
      targetUserId: 'user_pupa',
      confirmed: true,
    }),
    selfLockout: await resolveAssistantAction({
      actionId: 'hub.membership.disable',
      assistantRole: 'admin',
      membershipRole: 'client_admin',
      actorUserId: 'user_root',
      targetUserId: 'user_root',
      removesOwnAccess: true,
      hasAlternativeAdminPath: false,
      confirmed: true,
    }),
    futureEngineAdapter: await resolveAssistantAction({
      actionId: 'engine.workflow.share',
      assistantRole: 'admin',
      membershipRole: 'client_admin',
      actorUserId: 'user_root',
      targetUserId: 'user_pupa',
      confirmed: true,
    }),
  }

  assertSmoke(cases.deleteUser.decision === 'forbidden', 'delete user must be forbidden')
  assertSmoke(cases.blockAsMember.decision === 'denied', 'member must not block users')
  assertSmoke(cases.blockNeedsConfirmation.decision === 'needs_confirmation', 'privileged write must require confirmation')
  assertSmoke(cases.blockAllowed.decision === 'allowed', 'confirmed assistant admin block should be policy-allowed')
  assertSmoke(cases.blockAllowed.execution.executionReady, 'implemented HUB allowlist adapter should be execution-ready after policy')
  assertSmoke(cases.selfLockout.decision === 'denied', 'self lockout must be denied')
  assertSmoke(cases.futureEngineAdapter.decision === 'future_adapter', 'future engine adapter must not execute')

  return { ok: true, validation: validation.counts, cases }
}

if (import.meta.url === `file://${process.argv[1]}`) {
  if (process.argv.includes('--smoke')) {
    console.log(JSON.stringify(await runSmoke(), null, 2))
  } else {
    const input = await readInputJson()
    if (!input) throw new Error('Use --smoke, --input <json>, or --input-json <path>')
    console.log(JSON.stringify(await resolveAssistantAction(input), null, 2))
  }
}
