/**
 * Template-Spec v1 — Rule Engine (Phase B)
 *
 * Evaluates ruleset.v1.json against a RuleInput and produces flags,
 * applied defaults, validation errors/warnings, and removed block IDs.
 *
 * Design: zero external dependencies — minimal JSONLogic evaluator inline.
 */

import ruleset from './ruleset.v1.json'
import type { TemplateContext, ComputedFlags } from './contextBridge'

// =============================================================================
// Types
// =============================================================================

export interface RuleInput {
  doc_type: string
  render: { lang: string; variant: string }
  context: TemplateContext
  modules: { enabled: string[] }
}

export interface RuleViolation {
  id: string
  severity: 'ERROR' | 'WARN'
  message: string
  phase: string
}

export interface RuleEngineResult {
  computedFlags: ComputedFlags
  contextAfterDefaults: TemplateContext
  violations: RuleViolation[]
  warnings: RuleViolation[]
  removedBlocks: string[]
  appliedDefaults: string[]
}

// =============================================================================
// Minimal JSONLogic Evaluator
// Supports: var, ==, !=, in, and, or, >=, <=
// =============================================================================

type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue }

interface Envelope {
  doc_type: string
  render: { lang: string; variant: string }
  context: TemplateContext
  computed_flags: ComputedFlags
  modules: { enabled: string[] }
}

function getPath(obj: unknown, path: string): unknown {
  if (path === '') return obj
  const parts = path.split('.')
  let cur: unknown = obj
  for (const part of parts) {
    if (cur == null || typeof cur !== 'object') return null
    cur = (cur as Record<string, unknown>)[part]
  }
  return cur ?? null
}

function evaluate(expr: JsonValue, env: Envelope): unknown {
  if (expr === null || typeof expr !== 'object' || Array.isArray(expr)) {
    return expr
  }

  const ops = Object.keys(expr as Record<string, unknown>)
  if (ops.length === 0) return null
  const op = ops[0]
  const args = (expr as Record<string, JsonValue>)[op]

  switch (op) {
    case 'var': {
      const path = String(args)
      return getPath(env, path)
    }
    case '==': {
      const [a, b] = args as JsonValue[]
      return evaluate(a, env) == evaluate(b, env) // eslint-disable-line eqeqeq
    }
    case '!=': {
      const [a, b] = args as JsonValue[]
      return evaluate(a, env) != evaluate(b, env) // eslint-disable-line eqeqeq
    }
    case '>=': {
      const [a, b] = args as JsonValue[]
      const av = evaluate(a, env)
      const bv = evaluate(b, env)
      if (av == null || bv == null) return false
      return Number(av) >= Number(bv)
    }
    case '<=': {
      const [a, b] = args as JsonValue[]
      const av = evaluate(a, env)
      const bv = evaluate(b, env)
      if (av == null || bv == null) return false
      return Number(av) <= Number(bv)
    }
    case 'in': {
      const [needle, haystack] = args as JsonValue[]
      const n = evaluate(needle, env)
      const h = evaluate(haystack, env)
      if (!Array.isArray(h)) return false
      return h.includes(n as never)
    }
    case 'and': {
      const items = args as JsonValue[]
      return items.every((item) => !!evaluate(item, env))
    }
    case 'or': {
      const items = args as JsonValue[]
      return items.some((item) => !!evaluate(item, env))
    }
    default:
      return null
  }
}

// =============================================================================
// Deep clone helper
// =============================================================================

function deepClone<T>(obj: T): T {
  return JSON.parse(JSON.stringify(obj))
}

// =============================================================================
// Set value at dot-path (mutates object for defaults application)
// =============================================================================

function setNestedPath(obj: Record<string, unknown>, dotPath: string, value: unknown): void {
  const parts = dotPath.split('.')
  let cur: Record<string, unknown> = obj
  for (let i = 0; i < parts.length - 1; i++) {
    const part = parts[i]
    if (cur[part] == null || typeof cur[part] !== 'object') {
      cur[part] = {}
    }
    cur = cur[part] as Record<string, unknown>
  }
  cur[parts[parts.length - 1]] = value
}

// =============================================================================
// Phase runner helpers
// =============================================================================

type ComputeFlagEntry = {
  id: string
  set: string
  expr: JsonValue
}

type AutoDefaultEntry = {
  id: string
  when: JsonValue
  actions: { type: string; path: string; value: unknown }[]
}

type ValidationEntry = {
  id: string
  severity: string
  when: JsonValue
  assert_all: JsonValue[]
  message: string
}

type BlockRemoveEntry = {
  id: string
  when: JsonValue
  actions: { type: string; block_id: string }[]
}

type ModuleRequirementEntry = {
  id: string
  severity: string
  when: JsonValue
  assert_all: JsonValue[]
  message: string
}

type WarningEntry = {
  id: string
  severity: string
  when: JsonValue
  assert_all: JsonValue[]
  message: string
}

// =============================================================================
// Phase 1: compute_flags
// =============================================================================

function runComputeFlags(env: Envelope): ComputedFlags {
  const flags: Record<string, boolean> = {}
  for (const rule of (ruleset.compute_flags as unknown) as ComputeFlagEntry[]) {
    const result = !!evaluate(rule.expr as JsonValue, env)
    // set path like "computed_flags.IS_B2C"
    const key = rule.set.replace('computed_flags.', '')
    flags[key] = result
  }
  return flags as unknown as ComputedFlags
}

// =============================================================================
// Phase 2: auto_defaults
// =============================================================================

function runAutoDefaults(env: Envelope): string[] {
  const applied: string[] = []
  for (const rule of ruleset.auto_defaults as AutoDefaultEntry[]) {
    if (!!evaluate(rule.when as JsonValue, env)) {
      for (const action of rule.actions) {
        if (action.type === 'set') {
          // path like "context.SECURITY.LOG_RETENTION_DAYS"
          const contextPath = action.path.replace('context.', '')
          setNestedPath(env.context as unknown as Record<string, unknown>, contextPath, action.value)
        }
      }
      applied.push(rule.id)
    }
  }
  return applied
}

// =============================================================================
// Phase 4 / 5 / 6: hard_validations, module_requirements, warnings
// =============================================================================

function runValidations(
  rules: ValidationEntry[],
  env: Envelope,
  phase: string
): RuleViolation[] {
  const result: RuleViolation[] = []
  for (const rule of rules) {
    if (!evaluate(rule.when as JsonValue, env)) continue
    // assert_all: if empty → always fires (WARN_LEGAL_REVIEW pattern)
    const allPass =
      rule.assert_all.length === 0
        ? false
        : rule.assert_all.every((assertion) => !!evaluate(assertion as JsonValue, env))
    if (!allPass) {
      result.push({
        id: rule.id,
        severity: rule.severity as 'ERROR' | 'WARN',
        message: rule.message,
        phase,
      })
    }
  }
  return result
}

// =============================================================================
// Phase 5: auto_remove_blocks
// =============================================================================

function runAutoRemoveBlocks(env: Envelope): string[] {
  const removed: string[] = []
  for (const rule of ruleset.auto_remove_blocks as BlockRemoveEntry[]) {
    if (!!evaluate(rule.when as JsonValue, env)) {
      for (const action of rule.actions) {
        if (action.type === 'remove_block') {
          removed.push(action.block_id)
        }
      }
    }
  }
  return removed
}

// =============================================================================
// Main: runRuleset
// =============================================================================

export function runRuleset(input: RuleInput): RuleEngineResult {
  // Deep clone context so we don't mutate caller's state
  const clonedContext = deepClone(input.context)

  const env: Envelope = {
    doc_type: input.doc_type,
    render: input.render,
    context: clonedContext,
    computed_flags: {} as ComputedFlags,
    modules: input.modules,
  }

  // Phase 1: compute_flags
  env.computed_flags = runComputeFlags(env)

  // Phase 2: auto_defaults — mutates clonedContext
  const appliedDefaults = runAutoDefaults(env)

  // Phase 3: compute_flags again (after defaults)
  env.computed_flags = runComputeFlags(env)

  // Phase 4: hard_validations
  const allHardViolations = runValidations(
    (ruleset.hard_validations as unknown) as ValidationEntry[],
    env,
    'hard_validations'
  )

  // Phase 5: auto_remove_blocks
  const removedBlocks = runAutoRemoveBlocks(env)

  // Phase 6a: module_requirements
  const moduleViolations = runValidations(
    (ruleset.module_requirements as unknown) as ModuleRequirementEntry[],
    env,
    'module_requirements'
  )

  // Phase 6b: warnings
  const warningViolations = runValidations(
    (ruleset.warnings as unknown) as WarningEntry[],
    env,
    'warnings'
  )

  const allViolations = [...allHardViolations, ...moduleViolations, ...warningViolations]

  return {
    computedFlags: env.computed_flags,
    contextAfterDefaults: clonedContext,
    violations: allViolations.filter((v) => v.severity === 'ERROR'),
    warnings: allViolations.filter((v) => v.severity === 'WARN'),
    removedBlocks,
    appliedDefaults,
  }
}

// =============================================================================
// getDocType: maps (templateType, language) → doc_type string
// =============================================================================

const DOC_TYPE_MAP: Record<string, string> = {
  'nda+de': 'nda_de',
  'nda+en': 'nda_en',
  'sla+de': 'sla_de',
  'acceptable_use+en': 'acceptable_use_en',
  'community_guidelines+de': 'community_de',
  'copyright_policy+de': 'copyright_de',
  'cloud_service_agreement+de': 'cloud_contract_de',
  'data_usage_clause+de': 'data_usage_clause_de',
  'cookie_banner+de': 'cookie_banner_de',
  'agb+de': 'agb_de',
  'agb_clause+de': 'agb_de',
  'clause+en': 'liability_clause_en',
}

export function getDocType(templateType: string, language: string): string {
  const key = `${templateType}+${language}`
  return DOC_TYPE_MAP[key] ?? `${templateType}_${language}`
}

// =============================================================================
// buildBoolContext: combines computed flags + derived + FEATURES booleans
// =============================================================================

export function buildBoolContext(ctx: TemplateContext, flags: ComputedFlags): Record<string, boolean> {
  const f = ctx.FEATURES
  return {
    // --- From computed flags ---
    IS_B2C:           flags.IS_B2C,
    IS_B2B:           flags.IS_B2B,
    SERVICE_IS_SAAS:  flags.SERVICE_IS_SAAS,
    SERVICE_IS_HYBRID: flags.SERVICE_IS_HYBRID,
    HAS_PENALTY:      flags.HAS_PENALTY,
    HAS_ANALYTICS:    flags.HAS_ANALYTICS,
    ANALYTICS_ENABLED: flags.HAS_ANALYTICS,   // alias used in cookie banner template
    HAS_MARKETING:    flags.HAS_MARKETING,
    MARKETING_ENABLED: flags.HAS_MARKETING,   // alias

    // --- Derived from PROVIDER ---
    HAS_REGISTER: !!(ctx.PROVIDER.REGISTER_COURT),
    HAS_VAT_ID:   !!(ctx.PROVIDER.VAT_ID),
    CONTACT_PHONE: !!(ctx.PROVIDER.PHONE),

    // --- Derived from PRIVACY ---
    HAS_DPO: !!(ctx.PRIVACY.DPO_NAME),

    // --- From FEATURES booleans ---
    THIRD_COUNTRY_POSSIBLE: f.HAS_THIRD_COUNTRY,
    HAS_THIRD_COUNTRY:      f.HAS_THIRD_COUNTRY,
    FUNCTIONAL_ENABLED:     f.HAS_FUNCTIONAL_COOKIES,
    HAS_FUNCTIONAL_COOKIES: f.HAS_FUNCTIONAL_COOKIES,
    CMP_LOGS_CONSENTS:      f.CMP_LOGS_CONSENTS,
    HAS_NEWSLETTER:         f.HAS_NEWSLETTER,
    HAS_ACCOUNT:            f.HAS_ACCOUNT,
    HAS_PAYMENTS:           f.HAS_PAYMENTS,
    HAS_SUPPORT:            f.HAS_SUPPORT,
    HAS_SOCIAL_MEDIA:       f.HAS_SOCIAL_MEDIA,
    HAS_PAID_PLANS:         f.HAS_PAID_PLANS,
    HAS_SLA:                f.HAS_SLA,
    HAS_EXPORT_POLICY:      f.HAS_EXPORT_POLICY,
    HAS_WITHDRAWAL:         f.HAS_WITHDRAWAL,
    HAS_REGULATED_PROFESSION:   f.HAS_REGULATED_PROFESSION,
    HAS_EDITORIAL_RESPONSIBLE:  f.HAS_EDITORIAL_RESPONSIBLE,
    HAS_DISPUTE_RESOLUTION:     f.HAS_DISPUTE_RESOLUTION,
  }
}

// =============================================================================
// applyConditionalBlocks: processes {{#IF}}/{{#IF_NOT}}/{{#IF_ANY}} directives
// Runs BEFORE placeholder substitution.
// =============================================================================

export function applyConditionalBlocks(content: string, boolCtx: Record<string, boolean>): string {
  let result = content

  // {{#IF_NOT COND}}...{{/IF_NOT}}  — process before IF to avoid conflicts
  result = result.replace(
    /\{\{#IF_NOT ([A-Z_]+)\}\}([\s\S]*?)\{\{\/IF_NOT\}\}/g,
    (_, cond: string, body: string) => (boolCtx[cond] ? '' : body)
  )

  // {{#IF_ANY A B C}}...{{/IF_ANY}}
  result = result.replace(
    /\{\{#IF_ANY ([A-Z_ ]+)\}\}([\s\S]*?)\{\{\/IF_ANY\}\}/g,
    (_, conds: string, body: string) =>
      conds.trim().split(/\s+/).some((c) => boolCtx[c]) ? body : ''
  )

  // {{#IF COND}}...{{/IF}}
  result = result.replace(
    /\{\{#IF ([A-Z_]+)\}\}([\s\S]*?)\{\{\/IF\}\}/g,
    (_, cond: string, body: string) => (boolCtx[cond] ? body : '')
  )

  return result
}

// =============================================================================
// applyBlockRemoval: removes [BLOCK:ID]…[/BLOCK:ID] markers from content
// =============================================================================

export function applyBlockRemoval(content: string, removedBlocks: string[]): string {
  let result = content
  for (const blockId of removedBlocks) {
    // Matches [BLOCK:ID]...content...[/BLOCK:ID] including optional trailing newline
    const escaped = blockId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
    const pattern = new RegExp(
      `\\[BLOCK:${escaped}\\][\\s\\S]*?\\[\\/BLOCK:${escaped}\\]\\n?`,
      'g'
    )
    result = result.replace(pattern, '')
  }
  return result
}
