// @lifecycle canonical - Single source of truth for all injection decisions.

import {
  DISABLE_INJECT_MODIFIERS,
  FORCE_INJECT_MODIFIERS,
  INJECTION_TYPES,
  MODIFIER_EFFECTS,
} from './constants.js';
import { ConditionEvaluator, HierarchyResolver } from './internal/index.js';

import type {
  ExecutionContextType,
  InjectionConfig,
  InjectionDecision,
  InjectionDecisionInput,
  InjectionFrequency,
  InjectionRuntimeOverride,
  InjectionState,
  InjectionTarget,
  InjectionType,
} from './types.js';
import type { Logger } from '../../../../../infra/logging/index.js';

/**
 * Single source of truth for all injection decisions.
 *
 * All pipeline stages MUST consult this service instead of
 * making injection decisions independently. Decisions are computed
 * once per injection type and cached for the duration of the request.
 *
 * Resolution Priority:
 * 1. Modifiers (%clean, %lean, %judge) - highest priority
 * 2. Runtime overrides (session_control injection:override)
 * 3. Step config (step-specific rules)
 * 4. Chain config (chain-level rules)
 * 5. Category config (category-level rules)
 * 6. Global config (config.json defaults)
 * 7. System defaults (hardcoded fallbacks)
 *
 * @example
 * ```typescript
 * // In a pipeline stage
 * const decision = injectionService.decide({
 *   injectionType: 'system-prompt',
 *   currentStep: 2,
 *   totalSteps: 5,
 *   modifiers: context.executionPlan?.modifiers,
 *   categoryId: context.prompt?.category,
 *   chainId: context.mcpRequest.chain_id,
 * });
 *
 * if (decision.inject) {
 *   // Inject the system prompt
 * }
 * ```
 */
export class InjectionDecisionService {
  private readonly logger: Logger;
  private readonly resolver: HierarchyResolver;
  private readonly conditionEvaluator: ConditionEvaluator;

  /** Cached decisions by injection type. */
  private decisions: Map<InjectionType, InjectionDecision> = new Map();

  /** Active runtime overrides. */
  private runtimeOverrides: Map<InjectionType, InjectionRuntimeOverride> = new Map();

  /** Track last injection step for each type. */
  private lastInjectionStep: Map<InjectionType, number> = new Map();

  constructor(config: InjectionConfig, logger: Logger) {
    this.logger = logger;
    this.resolver = new HierarchyResolver(config, logger);
    this.conditionEvaluator = new ConditionEvaluator(logger);
  }

  /**
   * Replace all runtime overrides at once (e.g., from SessionOverrideResolver)
   * and clear cached decisions so new overrides take effect immediately.
   */
  syncRuntimeOverrides(overrides: Map<InjectionType, InjectionRuntimeOverride>): void {
    this.runtimeOverrides = new Map(overrides);
    this.decisions.clear();
    this.logger.debug('[InjectionDecisionService] Runtime overrides synced', {
      count: this.runtimeOverrides.size,
    });
  }

  /**
   * Get the injection decision for a specific type.
   * Computes on first call, returns cached thereafter.
   *
   * IMPORTANT: `inject: true` means INJECT, `inject: false` means SKIP.
   * No inversions, no confusion.
   */
  decide(input: InjectionDecisionInput): InjectionDecision {
    const cacheKey = input.injectionType;

    // Return cached decision if available
    const cached = this.decisions.get(cacheKey);
    if (cached) {
      return cached;
    }

    // Compute new decision
    const decision = this.computeDecision(input);

    // Cache it
    this.decisions.set(cacheKey, decision);

    // Track injection step if we're injecting
    if (decision.inject && input.currentStep !== undefined) {
      this.lastInjectionStep.set(input.injectionType, input.currentStep);
    }

    this.logger.debug('[InjectionDecisionService] Decision made', {
      type: input.injectionType,
      inject: decision.inject,
      reason: decision.reason,
      source: decision.source,
      currentStep: input.currentStep,
    });

    return decision;
  }

  /**
   * Decide for all injection types at once.
   * Useful for pipeline stages that need all decisions.
   */
  decideAll(input: Omit<InjectionDecisionInput, 'injectionType'>): InjectionState {
    const state: InjectionState = {};
    if (input.currentStep !== undefined) {
      state.currentStep = input.currentStep;
    }
    if (input.sessionOverrides) {
      state.sessionOverrides = input.sessionOverrides;
    }
    if (input.executionContext) {
      state.executionContext = input.executionContext;
    }

    for (const type of INJECTION_TYPES) {
      const decision = this.decide({ ...input, injectionType: type });

      switch (type) {
        case 'system-prompt':
          state.systemPrompt = decision;
          if (decision.inject && input.currentStep !== undefined) {
            state.lastSystemPromptStep = input.currentStep;
          }
          break;
        case 'gate-guidance':
          state.gateGuidance = decision;
          break;
        case 'style-guidance':
          state.styleGuidance = decision;
          break;
      }
    }

    return state;
  }

  /**
   * Check if a decision has been made for a type.
   */
  hasDecided(type: InjectionType): boolean {
    return this.decisions.has(type);
  }

  /**
   * Get cached decision without computing.
   */
  getCachedDecision(type: InjectionType): InjectionDecision | undefined {
    return this.decisions.get(type);
  }

  /**
   * Reset all cached decisions (for new request or testing).
   */
  reset(): void {
    this.decisions.clear();
    this.lastInjectionStep.clear();
  }

  /**
   * Set a runtime override for an injection type.
   */
  setRuntimeOverride(override: InjectionRuntimeOverride): void {
    this.runtimeOverrides.set(override.type, override);
    // Invalidate cached decision for this type
    this.decisions.delete(override.type);

    this.logger.debug('[InjectionDecisionService] Runtime override set', {
      type: override.type,
      enabled: override.enabled,
      scope: override.scope,
    });
  }

  /**
   * Clear a runtime override.
   */
  clearRuntimeOverride(type: InjectionType): void {
    this.runtimeOverrides.delete(type);
    // Invalidate cached decision
    this.decisions.delete(type);

    this.logger.debug('[InjectionDecisionService] Runtime override cleared', {
      type,
    });
  }

  /**
   * Clear all runtime overrides.
   */
  clearAllRuntimeOverrides(): void {
    this.runtimeOverrides.clear();
    this.decisions.clear();

    this.logger.debug('[InjectionDecisionService] All runtime overrides cleared');
  }

  /**
   * Get current runtime overrides for status reporting.
   */
  getRuntimeOverrides(): ReadonlyMap<InjectionType, InjectionRuntimeOverride> {
    return this.runtimeOverrides;
  }

  /**
   * Get last injection step for a type.
   */
  getLastInjectionStep(type: InjectionType): number | undefined {
    return this.lastInjectionStep.get(type);
  }

  /**
   * Compute the injection decision.
   */
  private computeDecision(input: InjectionDecisionInput): InjectionDecision {
    const timestamp = Date.now();

    // Priority 1: Check modifiers that disable injection
    const modifierDecision = this.checkModifiers(input, timestamp);
    if (modifierDecision) {
      return modifierDecision;
    }

    // Priority 2-6: Resolve from hierarchy
    const runtimeOverride = this.runtimeOverrides.get(input.injectionType);
    const resolved = this.resolver.resolve(input.injectionType, input, runtimeOverride);

    // Capture target from resolved config for later filtering
    const target = resolved.config.target ?? 'both';

    // Check if globally disabled
    if (!resolved.config.enabled) {
      return {
        inject: false,
        reason: `Disabled in ${resolved.source}`,
        source: resolved.source,
        decidedAt: timestamp,
        target,
      };
    }

    // Check conditional rules
    const conditionResult = this.conditionEvaluator.evaluate(resolved.config.conditions, input);

    if (conditionResult.matched) {
      if (conditionResult.action === 'skip') {
        return {
          inject: false,
          reason: conditionResult.reason,
          source: resolved.source,
          decidedAt: timestamp,
          target,
        };
      }
      if (conditionResult.action === 'inject') {
        // Apply target filtering before returning inject decision
        return this.applyTargetFilter(
          {
            inject: true,
            reason: conditionResult.reason,
            source: resolved.source,
            decidedAt: timestamp,
            target,
          },
          input.executionContext
        );
      }
      // 'inherit' falls through to frequency check
    }

    // Check frequency for chains
    if (input.currentStep !== undefined && resolved.config.frequency) {
      // Bypass frequency check for gate reviews when target includes gates
      // This ensures gate guidance always appears on review steps regardless of step number
      const isGateReviewWithGateTarget =
        input.executionContext === 'gate_review' && (target === 'both' || target === 'gates');

      if (isGateReviewWithGateTarget) {
        return {
          inject: true,
          reason: 'Gate review step (frequency bypassed)',
          source: resolved.source,
          decidedAt: timestamp,
          target,
        };
      }

      const frequencyDecision = this.checkFrequency(
        input.injectionType,
        input.currentStep,
        input.totalSteps,
        resolved.config.frequency,
        timestamp,
        resolved.source,
        target
      );
      // Apply target filtering to frequency decision
      return this.applyTargetFilter(frequencyDecision, input.executionContext);
    }

    // Default: inject (with target filtering)
    return this.applyTargetFilter(
      {
        inject: true,
        reason: `Enabled by ${resolved.source}`,
        source: resolved.source,
        decidedAt: timestamp,
        target,
      },
      input.executionContext
    );
  }

  /**
   * Apply target filtering to an injection decision.
   * If the decision is to inject but the target doesn't match the execution context,
   * convert it to a skip decision.
   *
   * @param decision - The base injection decision
   * @param executionContext - Current execution context ('step' or 'gate_review')
   * @returns Modified decision if target doesn't match, original otherwise
   */
  private applyTargetFilter(
    decision: InjectionDecision,
    executionContext?: ExecutionContextType
  ): InjectionDecision {
    // If not injecting, no filtering needed
    if (!decision.inject) {
      return decision;
    }

    const target = decision.target ?? 'both';

    // 'both' always matches
    if (target === 'both') {
      return decision;
    }

    // If no execution context provided, allow injection (backward compatibility)
    if (!executionContext) {
      return decision;
    }

    // Check if target matches execution context
    const matches =
      (target === 'steps' && executionContext === 'step') ||
      (target === 'gates' && executionContext === 'gate_review');

    if (matches) {
      return decision;
    }

    // Target doesn't match - convert to skip
    return {
      inject: false,
      reason: `Target '${target}' doesn't match execution context '${executionContext}'`,
      source: decision.source,
      decidedAt: decision.decidedAt,
      target,
    };
  }

  /**
   * Check modifiers for injection control.
   * Returns a decision if modifiers override normal behavior.
   */
  private checkModifiers(
    input: InjectionDecisionInput,
    timestamp: number
  ): InjectionDecision | null {
    if (!input.modifiers) {
      return null;
    }

    // Check force-inject modifiers (e.g., %judge)
    for (const modifier of FORCE_INJECT_MODIFIERS) {
      if (input.modifiers[modifier as keyof typeof input.modifiers]) {
        // %judge only affects system-prompt (forces injection for judge selection phase)
        if (modifier === 'judge' && input.injectionType === 'system-prompt') {
          return {
            inject: true,
            reason: `Forced by %${modifier} modifier`,
            source: 'modifier',
            decidedAt: timestamp,
          };
        }
      }
    }

    // Check disable-inject modifiers (e.g., %clean, %lean)
    for (const modifier of DISABLE_INJECT_MODIFIERS) {
      if (input.modifiers[modifier as keyof typeof input.modifiers]) {
        const affectedTypes = MODIFIER_EFFECTS[modifier];
        if (affectedTypes?.includes(input.injectionType)) {
          return {
            inject: false,
            reason: `Disabled by %${modifier} modifier`,
            source: 'modifier',
            decidedAt: timestamp,
          };
        }
      }
    }

    return null;
  }

  /**
   * Check if injection should happen based on frequency rules.
   */
  private checkFrequency(
    type: InjectionType,
    currentStep: number,
    totalSteps: number | undefined,
    frequency: InjectionFrequency,
    timestamp: number,
    source: InjectionDecision['source'],
    target: InjectionTarget = 'both'
  ): InjectionDecision {
    switch (frequency.mode) {
      case 'never':
        return {
          inject: false,
          reason: 'Frequency mode is never',
          source,
          decidedAt: timestamp,
          target,
        };

      case 'first-only':
        if (currentStep === 1) {
          return {
            inject: true,
            reason: 'First step (first-only mode)',
            source,
            decidedAt: timestamp,
            target,
          };
        }
        return {
          inject: false,
          reason: `Step ${currentStep} > 1 (first-only mode)`,
          source,
          decidedAt: timestamp,
          target,
        };

      case 'every': {
        const interval = frequency.interval ?? 1;

        // Step 1 always injects
        if (currentStep === 1) {
          return {
            inject: true,
            reason: 'First step always injects',
            source,
            decidedAt: timestamp,
            target,
          };
        }

        // For subsequent steps, check interval
        // With interval=2: inject on steps 1, 3, 5, etc.
        // Formula: inject if (step - 1) % interval === 0
        const shouldInject = (currentStep - 1) % interval === 0;

        if (shouldInject) {
          return {
            inject: true,
            reason: `Step ${currentStep} matches interval ${interval}`,
            source,
            decidedAt: timestamp,
            target,
          };
        }

        return {
          inject: false,
          reason: `Step ${currentStep} doesn't match interval ${interval}`,
          source,
          decidedAt: timestamp,
          target,
        };
      }

      default: {
        // Fallback for unknown mode
        const _exhaustive: never = frequency.mode;
        void _exhaustive;
        return {
          inject: true,
          reason: 'Unknown frequency mode, defaulting to inject',
          source,
          decidedAt: timestamp,
          target,
        };
      }
    }
  }
}
