import { normalizeQuery, NormalizedQuery } from './normalizer';
import { checkSafety } from './safety-filter';
import { computeTTL } from './ttl-strategy';
import { findMatch, storeEntry, recordHit, acquireDedup, releaseDedup, CacheMatch } from './similarity';
import { logEvent } from './metrics';

/**
 * Main cache service — orchestrates normalization, safety checks,
 * TTL computation, similarity matching, and storage.
 */

export interface CacheLookupResult {
  hit: boolean;
  response: string | null;
  matchType: 'exact' | 'fuzzy' | 'none';
  similarityScore: number;
  entryId: string | null;
  normalized: NormalizedQuery | null;
  latencyMs: number;
}

export interface CacheStoreResult {
  stored: boolean;
  entryId: string | null;
  reason?: string;
}

// ── Circuit Breaker ──
// Trips open after consecutive failures, bypasses cache for a cooldown period.
const BREAKER_FAILURE_THRESHOLD = 5;
const BREAKER_COOLDOWN_MS = 30_000; // 30 seconds
let breakerFailures = 0;
let breakerState: 'closed' | 'open' | 'half_open' = 'closed';
let breakerLastTrip = 0;

function breakerRecordSuccess() {
  breakerFailures = 0;
  breakerState = 'closed';
}

function breakerRecordFailure() {
  breakerFailures++;
  if (breakerFailures >= BREAKER_FAILURE_THRESHOLD) {
    breakerState = 'open';
    breakerLastTrip = Date.now();
    console.warn(`[CIRCUIT BREAKER] OPEN after ${breakerFailures} consecutive failures`);
  }
}

function breakerAllow(): boolean {
  if (breakerState === 'closed') return true;
  if (breakerState === 'open') {
    if (Date.now() - breakerLastTrip > BREAKER_COOLDOWN_MS) {
      breakerState = 'half_open';
      console.log('[CIRCUIT BREAKER] half_open — testing one request');
      return true;
    }
    return false;
  }
  // half_open: allow one probe
  return true;
}

/**
 * Get circuit breaker status for dashboard monitoring.
 */
export function getBreakerStatus(): { state: string; failures: number; lastTrip: number } {
  return { state: breakerState, failures: breakerFailures, lastTrip: breakerLastTrip };
}

/**
 * Look up a query in the cache.
 * Returns the cached response if found, or null to indicate a miss.
 * Circuit breaker bypasses cache during systemic failures.
 */
export async function lookupCache(rawQuery: string, agent: string): Promise<CacheLookupResult> {
  const startTime = Date.now();

  // Normalize
  const normalized = normalizeQuery(rawQuery, agent);
  if (!normalized) {
    return {
      hit: false, response: null, matchType: 'none',
      similarityScore: 0, entryId: null, normalized: null,
      latencyMs: Date.now() - startTime,
    };
  }

  // Safety check (query only, no response yet)
  const safety = checkSafety(normalized.normalized);
  if (!safety.isCacheable) {
    return {
      hit: false, response: null, matchType: 'none',
      similarityScore: 0, entryId: null, normalized,
      latencyMs: Date.now() - startTime,
    };
  }

  // Circuit breaker check
  if (!breakerAllow()) {
    const latencyMs = Date.now() - startTime;
    return {
      hit: false, response: null, matchType: 'none',
      similarityScore: 0, entryId: null, normalized,
      latencyMs,
    };
  }

  // Try to find a match
  let match: CacheMatch | null = null;

  try {
    match = await findMatch(agent, normalized.cacheKey, normalized.normalized, normalized.category, normalized.dataVersion);
  } catch (err) {
    console.error('Cache lookup error:', err);
    breakerRecordFailure();
  }

  const latencyMs = Date.now() - startTime;

  if (match) {
    // Cache hit — reset circuit breaker
    breakerRecordSuccess();
    await recordHit(match.id);
    await logEvent({
      cacheEntryId: match.id,
      eventType: 'hit',
      agent,
      normalizedQuery: normalized.normalized,
      similarityScore: match.similarityScore,
      tokensSaved: match.responseTokens || 0,
      latencyMs,
    });

    return {
      hit: true,
      response: match.responsePayload,
      matchType: match.matchType,
      similarityScore: match.similarityScore,
      entryId: match.id,
      normalized,
      latencyMs,
    };
  }

  // Cache miss (lookup succeeded, just no match — breaker healthy)
  breakerRecordSuccess();
  await logEvent({
    cacheEntryId: null,
    eventType: 'miss',
    agent,
    normalizedQuery: normalized.normalized,
    latencyMs,
  });

  return {
    hit: false, response: null, matchType: 'none',
    similarityScore: 0, entryId: null, normalized,
    latencyMs,
  };
}

/**
 * Store a query/response pair in the cache after a cache miss.
 * Performs safety check on the response and computes appropriate TTL.
 */
export async function storeInCache(
  normalized: NormalizedQuery,
  agent: string,
  responsePayload: string,
  options?: {
    modelUsed?: string;
    tokensIn?: number;
    tokensOut?: number;
    source?: 'live' | 'warm';
    gameState?: string;
  }
): Promise<CacheStoreResult> {
  // Safety check with response
  const safety = checkSafety(normalized.normalized, responsePayload);
  if (!safety.isCacheable) {
    return { stored: false, entryId: null, reason: 'safety_filter' };
  }

  // Compute TTL
  const { ttlSeconds, gameState } = await computeTTL(
    normalized.category,
    normalized.league,
    normalized.teams,
    normalized.gameDate,
  );

  if (ttlSeconds === 0) {
    return { stored: false, entryId: null, reason: `live_game_${gameState}` };
  }

  // Estimate response tokens (rough: ~4 chars per token)
  const responseTokens = Math.ceil(responsePayload.length / 4);

  // Acquire dedup lock
  const acquired = await acquireDedup(agent, normalized.cacheKey);
  if (!acquired) {
    return { stored: false, entryId: null, reason: 'dedup_locked' };
  }

  try {
    const entryId = await storeEntry({
      cacheKey: normalized.cacheKey,
      normalizedQuery: normalized.normalized,
      originalQuery: normalized.original,
      agent,
      category: normalized.category,
      responsePayload,
      responseTokens,
      league: normalized.league,
      teams: normalized.teams,
      gameDate: normalized.gameDate,
      ttlSeconds,
      modelUsed: options?.modelUsed || null,
      tokensIn: options?.tokensIn || null,
      tokensOut: options?.tokensOut || null,
      source: options?.source || 'live',
      dataVersion: normalized.dataVersion,
      gameState: options?.gameState || gameState,
    });

    return { stored: !!entryId, entryId };
  } finally {
    await releaseDedup(agent, normalized.cacheKey);
  }
}
