/**
 * SmartRelationshipExtractor - Unified relationship type extraction using ensemble of neural signals
 *
 * PRODUCTION-READY: Parallel to SmartExtractor but for verbs/relationships
 *
 * Design Philosophy:
 * - Simplicity over complexity (KISS principle)
 * - One class instead of multiple strategy layers
 * - Clear execution path for debugging
 * - Comprehensive relationship intelligence built-in
 *
 * Ensemble Architecture:
 * - VerbExactMatchSignal (40%) - Explicit keywords and phrases
 * - VerbEmbeddingSignal (35%) - Neural similarity with verb embeddings
 * - VerbPatternSignal (20%) - Regex patterns and structures
 * - VerbContextSignal (5%) - Entity type pair hints
 *
 * Performance:
 * - Parallel signal execution (~15-20ms total)
 * - LRU caching for hot relationships
 * - Confidence boosting when signals agree
 * - Graceful degradation on errors
 */

import type { Brainy } from '../brainy.js'
import type { VerbType, NounType } from '../types/graphTypes.js'
import { VerbExactMatchSignal } from './signals/VerbExactMatchSignal.js'
import { VerbEmbeddingSignal } from './signals/VerbEmbeddingSignal.js'
import { VerbPatternSignal } from './signals/VerbPatternSignal.js'
import { VerbContextSignal } from './signals/VerbContextSignal.js'
import type { VerbSignal as ExactVerbSignal } from './signals/VerbExactMatchSignal.js'
import type { VerbSignal as EmbeddingVerbSignal } from './signals/VerbEmbeddingSignal.js'
import type { VerbSignal as PatternVerbSignal } from './signals/VerbPatternSignal.js'
import type { VerbSignal as ContextVerbSignal } from './signals/VerbContextSignal.js'

/**
 * Extraction result with full traceability
 */
export interface RelationshipExtractionResult {
  type: VerbType
  confidence: number
  weight: number
  source: 'ensemble' | 'exact-match' | 'pattern' | 'embedding' | 'context'
  evidence: string
  metadata?: {
    signalResults?: Array<{
      signal: string
      type: VerbType
      confidence: number
      weight: number
    }>
    agreementBoost?: number
  }
}

/**
 * Options for SmartRelationshipExtractor
 */
export interface SmartRelationshipExtractorOptions {
  minConfidence?: number        // Minimum confidence threshold (default: 0.60)
  enableEnsemble?: boolean      // Use ensemble vs single best signal (default: true)
  cacheSize?: number           // LRU cache size (default: 2000)
  weights?: {                  // Custom signal weights (must sum to 1.0)
    exactMatch?: number        // Default: 0.40
    embedding?: number         // Default: 0.35
    pattern?: number           // Default: 0.20
    context?: number           // Default: 0.05
  }
}

/**
 * Internal signal result wrapper
 */
interface SignalResult {
  signal: 'exact-match' | 'embedding' | 'pattern' | 'context'
  type: VerbType | null
  confidence: number
  weight: number
  evidence: string
}

/**
 * SmartRelationshipExtractor - Unified relationship type classification
 *
 * This is the single entry point for all relationship type extraction.
 * It orchestrates all 4 signals, and combines results using ensemble weighting.
 *
 * Production features:
 * - Parallel signal execution for performance
 * - Ensemble voting with confidence boosting
 * - Comprehensive statistics and observability
 * - LRU caching for hot paths
 * - Graceful error handling
 */
export class SmartRelationshipExtractor {
  private brain: Brainy
  private options: Required<Omit<SmartRelationshipExtractorOptions, 'weights'>> & { weights: Required<NonNullable<SmartRelationshipExtractorOptions['weights']>> }

  // Signal instances
  private exactMatchSignal: VerbExactMatchSignal
  private embeddingSignal: VerbEmbeddingSignal
  private patternSignal: VerbPatternSignal
  private contextSignal: VerbContextSignal

  // LRU cache
  private cache: Map<string, RelationshipExtractionResult | null> = new Map()
  private cacheOrder: string[] = []

  // Statistics
  private stats = {
    calls: 0,
    cacheHits: 0,
    exactMatchWins: 0,
    embeddingWins: 0,
    patternWins: 0,
    contextWins: 0,
    ensembleWins: 0,
    agreementBoosts: 0,
    averageConfidence: 0,
    averageSignalsUsed: 0
  }

  constructor(brain: Brainy, options?: SmartRelationshipExtractorOptions) {
    this.brain = brain

    // Set default options
    this.options = {
      minConfidence: options?.minConfidence ?? 0.60,
      enableEnsemble: options?.enableEnsemble ?? true,
      cacheSize: options?.cacheSize ?? 2000,
      weights: {
        exactMatch: options?.weights?.exactMatch ?? 0.40,
        embedding: options?.weights?.embedding ?? 0.35,
        pattern: options?.weights?.pattern ?? 0.20,
        context: options?.weights?.context ?? 0.05
      }
    }

    // Validate weights sum to 1.0
    const weightSum = Object.values(this.options.weights).reduce((a, b) => a + b, 0)
    if (Math.abs(weightSum - 1.0) > 0.01) {
      throw new Error(`Signal weights must sum to 1.0, got ${weightSum}`)
    }

    // Initialize signals
    this.exactMatchSignal = new VerbExactMatchSignal(brain, {
      minConfidence: 0.50, // Lower threshold, ensemble will filter
      cacheSize: Math.floor(this.options.cacheSize / 4)
    })

    this.embeddingSignal = new VerbEmbeddingSignal(brain, {
      minConfidence: 0.50,
      cacheSize: Math.floor(this.options.cacheSize / 4)
    })

    this.patternSignal = new VerbPatternSignal(brain, {
      minConfidence: 0.50,
      cacheSize: Math.floor(this.options.cacheSize / 4)
    })

    this.contextSignal = new VerbContextSignal(brain, {
      minConfidence: 0.50,
      cacheSize: Math.floor(this.options.cacheSize / 4)
    })
  }

  /**
   * Infer relationship type using ensemble of signals
   *
   * Main entry point - orchestrates all signals and combines results
   *
   * @param subject Subject entity name (e.g., "Alice")
   * @param object Object entity name (e.g., "UCSF")
   * @param context Full context text (sentence or paragraph)
   * @param options Additional context for inference
   * @returns RelationshipExtractionResult with type and confidence
   */
  async infer(
    subject: string,
    object: string,
    context: string,
    options?: {
      subjectType?: NounType
      objectType?: NounType
      contextVector?: number[]
    }
  ): Promise<RelationshipExtractionResult | null> {
    this.stats.calls++

    // Check cache first
    const cacheKey = this.getCacheKey(subject, object, context)
    const cached = this.getFromCache(cacheKey)
    if (cached !== undefined) {
      this.stats.cacheHits++
      return cached
    }

    try {
      // Execute all signals in parallel
      const [exactMatch, embeddingMatch, patternMatch, contextMatch] = await Promise.all([
        this.exactMatchSignal.classify(context).catch(() => null),
        this.embeddingSignal.classify(context, options?.contextVector).catch(() => null),
        this.patternSignal.classify(subject, object, context).catch(() => null),
        this.contextSignal.classify(options?.subjectType, options?.objectType).catch(() => null)
      ])

      // Wrap results with weights
      const signalResults: SignalResult[] = [
        {
          signal: 'exact-match',
          type: exactMatch?.type || null,
          confidence: exactMatch?.confidence || 0,
          weight: this.options.weights.exactMatch,
          evidence: exactMatch?.evidence || ''
        },
        {
          signal: 'embedding',
          type: embeddingMatch?.type || null,
          confidence: embeddingMatch?.confidence || 0,
          weight: this.options.weights.embedding,
          evidence: embeddingMatch?.evidence || ''
        },
        {
          signal: 'pattern',
          type: patternMatch?.type || null,
          confidence: patternMatch?.confidence || 0,
          weight: this.options.weights.pattern,
          evidence: patternMatch?.evidence || ''
        },
        {
          signal: 'context',
          type: contextMatch?.type || null,
          confidence: contextMatch?.confidence || 0,
          weight: this.options.weights.context,
          evidence: contextMatch?.evidence || ''
        }
      ]

      // Combine using ensemble or best signal
      const result = this.options.enableEnsemble
        ? this.combineEnsemble(signalResults)
        : this.selectBestSignal(signalResults)

      // Cache result (including nulls to avoid recomputation)
      this.addToCache(cacheKey, result)

      // Update statistics
      if (result) {
        this.updateStatistics(result)
      }

      return result
    } catch (error) {
      // Graceful degradation
      console.warn(`SmartRelationshipExtractor error for "${subject} → ${object}":`, error)
      return null
    }
  }

  /**
   * Combine signal results using ensemble voting
   *
   * Applies weighted voting with confidence boosting when signals agree
   */
  private combineEnsemble(
    signalResults: SignalResult[]
  ): RelationshipExtractionResult | null {
    // Filter out null results
    const validResults = signalResults.filter(r => r.type !== null)

    if (validResults.length === 0) {
      return null
    }

    // Count votes by type with weighted confidence
    const typeScores = new Map<VerbType, { score: number; signals: SignalResult[] }>()

    for (const result of validResults) {
      if (!result.type) continue

      const weighted = result.confidence * result.weight
      const existing = typeScores.get(result.type)

      if (existing) {
        existing.score += weighted
        existing.signals.push(result)
      } else {
        typeScores.set(result.type, { score: weighted, signals: [result] })
      }
    }

    // Find best type
    let bestType: VerbType | null = null
    let bestScore = 0
    let bestSignals: SignalResult[] = []

    for (const [type, data] of typeScores.entries()) {
      // Apply agreement boost (multiple signals agree)
      let finalScore = data.score
      if (data.signals.length > 1) {
        const agreementBoost = 0.05 * (data.signals.length - 1)
        finalScore += agreementBoost
        this.stats.agreementBoosts++
      }

      if (finalScore > bestScore) {
        bestScore = finalScore
        bestType = type
        bestSignals = data.signals
      }
    }

    // Check minimum confidence threshold
    if (!bestType || bestScore < this.options.minConfidence) {
      return null
    }

    // Track signal contributions
    const usedSignals = bestSignals.length
    this.stats.averageSignalsUsed =
      (this.stats.averageSignalsUsed * (this.stats.calls - 1) + usedSignals) / this.stats.calls

    // Build evidence string
    const signalNames = bestSignals.map(s => s.signal).join(' + ')
    const evidence = `Ensemble: ${signalNames} (${bestSignals.length} signal${bestSignals.length > 1 ? 's' : ''} agree)`

    return {
      type: bestType,
      confidence: Math.min(bestScore, 1.0), // Cap at 1.0
      weight: Math.min(bestScore, 1.0),
      source: 'ensemble',
      evidence,
      metadata: {
        signalResults: bestSignals.map(s => ({
          signal: s.signal,
          type: s.type!,
          confidence: s.confidence,
          weight: s.weight
        })),
        agreementBoost: bestSignals.length > 1 ? 0.05 * (bestSignals.length - 1) : 0
      }
    }
  }

  /**
   * Select best single signal (when ensemble is disabled)
   */
  private selectBestSignal(
    signalResults: SignalResult[]
  ): RelationshipExtractionResult | null {
    // Filter valid results and sort by weighted confidence
    const validResults = signalResults
      .filter(r => r.type !== null)
      .map(r => ({ ...r, weightedScore: r.confidence * r.weight }))
      .sort((a, b) => b.weightedScore - a.weightedScore)

    if (validResults.length === 0) {
      return null
    }

    const best = validResults[0]

    if (best.weightedScore < this.options.minConfidence) {
      return null
    }

    return {
      type: best.type!,
      confidence: best.confidence,
      weight: best.confidence,
      source: best.signal as any,
      evidence: best.evidence,
      metadata: undefined
    }
  }

  /**
   * Update statistics based on result
   */
  private updateStatistics(result: RelationshipExtractionResult): void {
    // Track win counts
    if (result.source === 'ensemble') {
      this.stats.ensembleWins++
    } else if (result.source === 'exact-match') {
      this.stats.exactMatchWins++
    } else if (result.source === 'embedding') {
      this.stats.embeddingWins++
    } else if (result.source === 'pattern') {
      this.stats.patternWins++
    } else if (result.source === 'context') {
      this.stats.contextWins++
    }

    // Update rolling average confidence
    this.stats.averageConfidence =
      (this.stats.averageConfidence * (this.stats.calls - 1) + result.confidence) / this.stats.calls
  }

  /**
   * Get cache key from parameters
   */
  private getCacheKey(subject: string, object: string, context: string): string {
    const normalized = `${subject}:${object}:${context.substring(0, 100)}`.toLowerCase().trim()
    return normalized
  }

  /**
   * Get from LRU cache
   */
  private getFromCache(key: string): RelationshipExtractionResult | null | undefined {
    if (!this.cache.has(key)) return undefined

    const cached = this.cache.get(key)

    // Move to end (most recently used)
    this.cacheOrder = this.cacheOrder.filter(k => k !== key)
    this.cacheOrder.push(key)

    return cached ?? null
  }

  /**
   * Add to LRU cache with eviction
   */
  private addToCache(key: string, value: RelationshipExtractionResult | null): void {
    this.cache.set(key, value)
    this.cacheOrder.push(key)

    // Evict oldest if over limit
    if (this.cache.size > this.options.cacheSize) {
      const oldest = this.cacheOrder.shift()
      if (oldest) {
        this.cache.delete(oldest)
      }
    }
  }

  /**
   * Get comprehensive statistics
   */
  getStats() {
    return {
      ...this.stats,
      cacheSize: this.cache.size,
      cacheHitRate: this.stats.calls > 0 ? this.stats.cacheHits / this.stats.calls : 0,
      ensembleRate: this.stats.calls > 0 ? this.stats.ensembleWins / this.stats.calls : 0,
      signalStats: {
        exactMatch: this.exactMatchSignal.getStats(),
        embedding: this.embeddingSignal.getStats(),
        pattern: this.patternSignal.getStats(),
        context: this.contextSignal.getStats()
      }
    }
  }

  /**
   * Reset all statistics
   */
  resetStats(): void {
    this.stats = {
      calls: 0,
      cacheHits: 0,
      exactMatchWins: 0,
      embeddingWins: 0,
      patternWins: 0,
      contextWins: 0,
      ensembleWins: 0,
      agreementBoosts: 0,
      averageConfidence: 0,
      averageSignalsUsed: 0
    }

    this.exactMatchSignal.resetStats()
    this.embeddingSignal.resetStats()
    this.patternSignal.resetStats()
    this.contextSignal.resetStats()
  }

  /**
   * Clear all caches
   */
  clearCache(): void {
    this.cache.clear()
    this.cacheOrder = []

    this.exactMatchSignal.clearCache()
    this.embeddingSignal.clearCache()
    this.patternSignal.clearCache()
    this.contextSignal.clearCache()
  }

  /**
   * Add relationship to historical data (for embedding signal temporal boosting)
   */
  addToHistory(context: string, type: VerbType, vector: number[]): void {
    this.embeddingSignal.addToHistory(context, type, vector)
  }

  /**
   * Clear historical data
   */
  clearHistory(): void {
    this.embeddingSignal.clearHistory()
  }
}

/**
 * Create a new SmartRelationshipExtractor instance
 *
 * Convenience factory function
 */
export function createSmartRelationshipExtractor(
  brain: Brainy,
  options?: SmartRelationshipExtractorOptions
): SmartRelationshipExtractor {
  return new SmartRelationshipExtractor(brain, options)
}
