import { Jexl } from '@pawel-up/jexl/Jexl.js'
import type { DomainEntity } from '../../modeling/DomainEntity.js'
import {
  type AppliedDataSemantic,
  DataSemantics,
  type SemanticCondition,
  SemanticOperation,
  SemanticType,
} from '../../modeling/Semantics.js'
import type { TransformFunction } from '@pawel-up/jexl'

/**
 * Cache for JEXL instances to avoid recreation overhead
 */
const jexlCache = new Map<string, Jexl>()

/**
 * Get or create a JEXL instance with optional custom transforms
 */
export function getJexlInstance(transforms?: Record<string, TransformFunction>): Jexl {
  const cacheKey = transforms ? JSON.stringify(Object.keys(transforms).sort()) : 'default'

  if (!jexlCache.has(cacheKey)) {
    const jexl = new Jexl()
    if (transforms) {
      Object.entries(transforms).forEach(([name, fn]) => {
        jexl.addTransform(name, fn)
      })
    }
    jexlCache.set(cacheKey, jexl)
  }

  return jexlCache.get(cacheKey) as Jexl
}

/**
 * Context object passed to semantic condition evaluation
 */
export interface SemanticExecutionContext {
  /**
   * The entity data being processed
   */
  entity: Record<string, unknown>
  /**
   * Current user context (if authenticated)
   */
  user?: {
    id?: string
    authenticated?: boolean
    roles?: string[]
    [key: string]: unknown
  }
  /**
   * The current database operation
   */
  operation: SemanticOperation
  /**
   * Applied semantics with their field mappings
   */
  appliedSemantics: AppliedDataSemantic[]
  /**
   * Configuration for the current semantic being evaluated
   */
  config?: Record<string, unknown>
}

/**
 * Builds a semantics object for JEXL evaluation that maps semantic types to actual field names.
 *
 * The resulting map should be used in JEXL expressions to reference semantic fields.
 *
 * @returns A map where keys are semantic type identifiers and values are the field names.
 *
 * @example
 * const semanticFieldMap = buildSemanticFieldMap(entity);
 * const result = await jexl.eval("semantics.CreatedTimestamp == null", {
 *  semantics: semanticFieldMap,
 *  ...
 * }
 */
export function buildSemanticFieldMap(entity: DomainEntity): Record<string, string> {
  const semanticFieldMap: Record<string, string> = {}

  for (const property of entity.properties) {
    if (!property.info.name) {
      continue // Skip properties without a name
    }
    for (const semantic of property.semantics) {
      // We use the truncated `semantic.id` as the key, e.g. 'Password' so that it is possible to do something like:
      // `entity[semantics.Password] != null && !entity[semantics.Password].startsWith('$')`
      // Where the `semantics` object is the object returned by this function and
      // passed to the JEXL context.
      const semanticKey = semantic.id.replace('Semantic#', '')
      semanticFieldMap[semanticKey] = property.info.name
    }
  }
  return semanticFieldMap
}

/**
 * Evaluates a semantic condition against the execution context
 * In a real implementation, this would use JEXL to evaluate the expression
 *
 * @param condition The semantic condition to evaluate
 * @param context The execution context containing entity data, user info, etc.
 * @param semanticFieldMap A map of semantic field names to their actual field names.
 *                         Use the `buildSemanticFieldMap()` function to create this map.
 * @param jexl Optional JEXL instance to use for evaluation, defaults to a cached instance.
 * @returns A promise that resolves to true if the condition is met, false otherwise
 */
export function evaluateSemanticCondition(
  condition: SemanticCondition,
  context: SemanticExecutionContext,
  semanticFieldMap: Record<string, string>,
  jexl: Jexl = getJexlInstance()
): Promise<boolean> {
  // Build the evaluation context
  const evalContext = {
    entity: context.entity,
    user: context.user,
    operation: context.operation,
    config: context.config,
    semantics: semanticFieldMap,
  }
  return jexl.eval(condition.expression, evalContext)
}

/**
 * Check if semantic should execute based on all its conditions
 *
 * @param semantic The semantic to check
 * @param context The execution context containing entity data, user info, etc.
 * @param semanticFieldMap A map of semantic field names to their actual field names.
 *                         Use the `buildSemanticFieldMap()` function to create this map.
 * @return A promise that resolves to true if all conditions are met, false otherwise
 */
export async function shouldSemanticExecute(
  semantic: AppliedDataSemantic,
  context: SemanticExecutionContext,
  semanticFieldMap: Record<string, string>
): Promise<boolean> {
  const definition = DataSemantics[semantic.id]
  const conditions = definition?.runtime?.conditions

  if (!conditions || conditions.length === 0) {
    return true
  }

  const results = await Promise.all(
    conditions.map((condition) => evaluateSemanticCondition(condition, context, semanticFieldMap))
  )
  // All conditions must evaluate to true
  return results.every((result) => result === true)
}

/**
 * Helper to get the actual field name for a semantic type
 */
export function getFieldNameForSemantic(
  semanticType: SemanticType,
  semanticFieldMap: Record<string, string>
): string | undefined {
  const semanticKey = semanticType.replace('Semantic#', '')
  return semanticFieldMap[semanticKey]
}

/**
 * Validates that all semantic references in conditions are available
 */
export function validateSemanticConditions(
  semantic: AppliedDataSemantic,
  semanticFieldMap: Record<string, string>
): string[] {
  const definition = DataSemantics[semantic.id]
  const conditions = definition?.runtime?.conditions || []
  const errors: string[] = []

  conditions.forEach((condition, index) => {
    // Extract semantic references from the condition expression
    // Look for patterns like "semantics.CreatedTimestamp" or "entity[semantics.Password]"
    const semanticMatches = condition.expression.match(/semantics\.(\w+)/g)

    if (semanticMatches) {
      semanticMatches.forEach((match) => {
        const semanticKey = match.replace('semantics.', '')
        if (!semanticFieldMap[semanticKey]) {
          errors.push(
            `Condition ${index + 1} references semantic "${semanticKey}" but no field with that semantic was found`
          )
        }
      })
    }
  })

  return errors
}
