import Redis from 'ioredis';
import pool from '../db';

/**
 * 2-stage similarity matching:
 * Stage 1: Exact SHA-256 hash lookup in Redis (sub-millisecond)
 * Stage 2: pg_trgm fuzzy match in PostgreSQL (if exact miss)
 */

const REDIS_URL = process.env.REDIS_URL || 'redis://127.0.0.1:6379';
const SIMILARITY_THRESHOLD = parseFloat(process.env.CACHE_SIMILARITY_THRESHOLD || '0.85');

let redis: Redis | null = null;

export function getRedis(): Redis {
  if (!redis) {
    redis = new Redis(REDIS_URL, {
      maxRetriesPerRequest: 3,
      retryStrategy(times) {
        if (times > 5) return null;
        return Math.min(times * 200, 2000);
      },
      lazyConnect: true,
    });
    redis.on('error', (err) => {
      console.error('Redis error:', err.message);
    });
  }
  return redis;
}

export interface CacheMatch {
  id: string;
  responsePayload: string;
  responseTokens: number | null;
  matchType: 'exact' | 'fuzzy';
  similarityScore: number;
  cacheKey: string;
}

/**
 * Stage 1: Exact match via Redis hash lookup
 */
export async function exactMatch(agent: string, cacheKey: string): Promise<CacheMatch | null> {
  try {
    const r = getRedis();
    const redisKey = `qcache:${agent}:${cacheKey}`;
    const cached = await r.get(redisKey);

    if (!cached) return null;

    const data = JSON.parse(cached);
    return {
      id: data.id,
      responsePayload: data.responsePayload,
      responseTokens: data.responseTokens || null,
      matchType: 'exact',
      similarityScore: 1.0,
      cacheKey,
    };
  } catch (err) {
    console.error('Redis exact match error:', err);
    return null;
  }
}

/**
 * Extract numeric values from a query for flip-guard validation.
 * Prevents fuzzy matching "Lakers +7.5" with "Lakers -7.5".
 */
function extractNumbers(text: string): number[] {
  const matches = text.match(/[+-]?\d+\.?\d*/g);
  return matches ? matches.map(Number).sort((a, b) => a - b) : [];
}

function numbersMatch(a: number[], b: number[]): boolean {
  if (a.length === 0 && b.length === 0) return true;
  if (a.length !== b.length) return false;
  return a.every((v, i) => v === b[i]);
}

/**
 * Stage 2: Fuzzy match via pg_trgm in PostgreSQL
 * Includes numeric flip guard to prevent matching queries where
 * key numbers (spreads, totals, odds) differ.
 * Scoped by category + data_version to prevent cross-category and stale matches.
 */
export async function fuzzyMatch(
  agent: string,
  normalizedQuery: string,
  category?: string,
  dataVersion?: string | null,
): Promise<CacheMatch | null> {
  try {
    // Build optional scoping filters
    const params: any[] = [normalizedQuery, agent, SIMILARITY_THRESHOLD];
    let extraWhere = '';
    let paramIdx = 4;

    if (category) {
      extraWhere += ` AND query_category = $${paramIdx}`;
      params.push(category);
      paramIdx++;
    }
    if (dataVersion) {
      extraWhere += ` AND data_version = $${paramIdx}`;
      params.push(dataVersion);
      paramIdx++;
    }

    const result = await pool.query(`
      SELECT id, cache_key, normalized_query, response_payload, response_tokens,
             similarity(normalized_query, $1) AS sim_score
      FROM query_cache
      WHERE agent = $2
        AND expires_at > NOW()
        AND is_cacheable = true
        AND similarity(normalized_query, $1) >= $3
        ${extraWhere}
      ORDER BY sim_score DESC
      LIMIT 5
    `, params);

    if (result.rows.length === 0) return null;

    // Numeric flip guard: ensure numbers in query match candidate
    const queryNums = extractNumbers(normalizedQuery);

    for (const row of result.rows) {
      const candidateNums = extractNumbers(row.normalized_query);
      if (numbersMatch(queryNums, candidateNums)) {
        return {
          id: row.id,
          responsePayload: row.response_payload,
          responseTokens: row.response_tokens,
          matchType: 'fuzzy',
          similarityScore: parseFloat(row.sim_score),
          cacheKey: row.cache_key,
        };
      }
    }

    // All candidates had numeric mismatches
    return null;
  } catch (err) {
    console.error('pg_trgm fuzzy match error:', err);
    return null;
  }
}

/**
 * Combined lookup: tries exact first, then fuzzy.
 * Category + dataVersion scope the fuzzy search to prevent
 * cross-category pollution and stale-data matches.
 */
export async function findMatch(
  agent: string,
  cacheKey: string,
  normalizedQuery: string,
  category?: string,
  dataVersion?: string | null,
): Promise<CacheMatch | null> {
  // Stage 1: exact
  const exact = await exactMatch(agent, cacheKey);
  if (exact) return exact;

  // Stage 2: fuzzy (scoped by category + data_version)
  return fuzzyMatch(agent, normalizedQuery, category, dataVersion);
}

/**
 * Store a cache entry in both Redis and PostgreSQL
 */
export async function storeEntry(params: {
  cacheKey: string;
  normalizedQuery: string;
  originalQuery: string;
  agent: string;
  category: string;
  responsePayload: string;
  responseTokens: number | null;
  league: string | null;
  teams: string[];
  gameDate: string | null;
  ttlSeconds: number;
  modelUsed: string | null;
  tokensIn: number | null;
  tokensOut: number | null;
  source: 'live' | 'warm';
  dataVersion?: string | null;
  gameState?: string | null;
}): Promise<string | null> {
  const expiresAt = new Date(Date.now() + params.ttlSeconds * 1000);

  try {
    // Upsert into PostgreSQL
    const result = await pool.query(`
      INSERT INTO query_cache (
        cache_key, normalized_query, original_query, agent, query_category,
        response_payload, response_tokens, league, teams, game_date,
        expires_at, model_used, original_tokens_in, original_tokens_out, source,
        data_version, game_state
      ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
      ON CONFLICT (cache_key, agent) DO UPDATE SET
        response_payload = EXCLUDED.response_payload,
        response_tokens = EXCLUDED.response_tokens,
        expires_at = EXCLUDED.expires_at,
        model_used = EXCLUDED.model_used,
        original_tokens_in = EXCLUDED.original_tokens_in,
        original_tokens_out = EXCLUDED.original_tokens_out,
        source = EXCLUDED.source,
        data_version = EXCLUDED.data_version,
        game_state = EXCLUDED.game_state
      RETURNING id
    `, [
      params.cacheKey, params.normalizedQuery, params.originalQuery,
      params.agent, params.category, params.responsePayload,
      params.responseTokens, params.league,
      params.teams.length > 0 ? `{${params.teams.join(',')}}` : null,
      params.gameDate, expiresAt, params.modelUsed,
      params.tokensIn, params.tokensOut, params.source,
      params.dataVersion || null, params.gameState || null,
    ]);

    const entryId = result.rows[0].id;

    // Store in Redis with TTL
    const r = getRedis();
    const redisKey = `qcache:${params.agent}:${params.cacheKey}`;
    const redisValue = JSON.stringify({
      id: entryId,
      responsePayload: params.responsePayload,
      responseTokens: params.responseTokens,
    });
    await r.setex(redisKey, params.ttlSeconds, redisValue);

    return entryId;
  } catch (err) {
    console.error('Store cache entry error:', err);
    return null;
  }
}

/**
 * Record a cache hit (update hit_count + last_hit_at)
 */
export async function recordHit(entryId: string): Promise<void> {
  try {
    await pool.query(`
      UPDATE query_cache
      SET hit_count = hit_count + 1, last_hit_at = NOW()
      WHERE id = $1
    `, [entryId]);
  } catch (err) {
    console.error('Record hit error:', err);
  }
}

/**
 * Check dedup lock to prevent thundering herd
 */
export async function acquireDedup(agent: string, cacheKey: string): Promise<boolean> {
  try {
    const r = getRedis();
    const dedupKey = `qcache:dedup:${agent}:${cacheKey}`;
    const result = await r.set(dedupKey, 'processing', 'EX', 30, 'NX');
    return result === 'OK';
  } catch {
    return true; // If Redis is down, allow the request through
  }
}

/**
 * Release dedup lock
 */
export async function releaseDedup(agent: string, cacheKey: string): Promise<void> {
  try {
    const r = getRedis();
    await r.del(`qcache:dedup:${agent}:${cacheKey}`);
  } catch {}
}

/**
 * Clean up Redis connection on shutdown
 */
export async function closeRedis(): Promise<void> {
  if (redis) {
    await redis.quit();
    redis = null;
  }
}
