/**
 * Explorer Agent - Investigates theories/approaches
 * Uses llama-70b (via Groq API) with technique guidance
 *
 * ARCHITECTURE: Exploratory mode - investigates theories to see if they show promise
 * Does NOT assume it knows the solution
 */

import { Technique, Theory, ExplorationResult, ExplorerConfig } from '../lib/types';
import { sanitizeErrorMessage, createSafeError } from '../lib/input-validation';

const GROQ_API_KEY = process.env.GROQ_API_KEY;

// Validate API key is set
if (!GROQ_API_KEY) {
  console.warn('[Explorer] Warning: GROQ_API_KEY not set in environment');
}

interface GroqMessage {
  role: 'system' | 'user' | 'assistant';
  content: string;
}

/**
 * Generates exploration prompt for investigating a theory
 * CRITICAL: This is exploratory, not solution-oriented
 */
function generateExplorationPrompt(
  theory: Theory,
  techniques: Technique[]
): string {
  // Find techniques matching theory's categories
  const relevantTechniques = techniques.filter(
    t => theory.technique_categories.some(cat =>
      t.category.toLowerCase().includes(cat.toLowerCase()) ||
      cat.toLowerCase().includes(t.category.toLowerCase())
    ) || (t.similarity && t.similarity >= 0.5)
  ).slice(0, 3);

  const techniqueContext = relevantTechniques.map(t => {
    const steps = JSON.parse(t.step_pattern);
    return `• ${t.name}
  Pattern: ${t.key_insight}
  When to use: ${t.recognition_pattern}
  Steps: ${steps.join(' → ')}`;
  }).join('\n\n');

  const questionsSection = theory.exploration_questions.length > 0
    ? `EXPLORATION QUESTIONS:\n${theory.exploration_questions.map((q, i) => `${i + 1}. ${q}`).join('\n')}`
    : '';

  return `EXPLORE this theory to see if it shows promise for solving the problem.

THEORY TO INVESTIGATE:
Approach: ${theory.approach}
Hypothesis: ${theory.hypothesis}
Source: ${theory.source === 'ruvector' ? 'Evidence-based (worked on similar problems)' : 'Novel/Creative'}
${theory.technique_name ? `Technique: ${theory.technique_name}` : ''}

${questionsSection}

TECHNIQUE GUIDANCE (if applicable):
${techniqueContext || 'No specific techniques matched - explore creatively'}

YOUR TASK: INVESTIGATE (not solve)
You are NOT expected to solve the complete problem.
You ARE expected to explore this theory and report what you discover.

INSTRUCTIONS:
1. Try examples - test the theory on small cases
2. Look for patterns - what works? what doesn't?
3. Identify obstacles - where does this theory struggle?
4. Report discoveries - what did you learn?
5. Assess promise - does this theory look worth pursuing?

Output your investigation in this format:

EXAMPLES_TRIED:
[Describe what examples/cases you tested]

DISCOVERIES:
- [Discovery 1: What you learned]
- [Discovery 2: Pattern you noticed]
- [Discovery 3: Obstacle you found]

SHOWS_PROMISE: yes/no
[Explain why this theory looks promising or why it's a dead end]

CONFIDENCE: [0.0-1.0]
[How confident are you in your assessment]

PARTIAL_RESULT:
[Any partial progress or insights, even if incomplete]

DEAD_END_REASON:
[If this is a dead end, explain why. Otherwise leave blank]`;
}

/**
 * Calls Groq API to explore theory with retry logic for rate limits
 */
async function callGroq(
  messages: GroqMessage[],
  model: string,
  maxTokens: number,
  temperature: number,
  timeoutMs: number,
  maxRetries: number = 3
): Promise<{ content: string; latency: number }> {
  const startTime = Date.now();
  let lastError: Error | null = null;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

    try {
      const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${GROQ_API_KEY}`,
        },
        body: JSON.stringify({
          model: mapModelToGroq(model),
          messages,
          temperature,
          max_tokens: maxTokens,
        }),
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      // Handle rate limiting with exponential backoff
      if (response.status === 429) {
        clearTimeout(timeoutId);

        if (attempt < maxRetries) {
          const backoffMs = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
          console.warn(`[Explorer] Rate limited (429), retrying in ${backoffMs}ms (attempt ${attempt + 1}/${maxRetries})`);
          await new Promise(resolve => setTimeout(resolve, backoffMs));
          continue;
        } else {
          throw new Error(`Groq API rate limit exceeded after ${maxRetries} retries`);
        }
      }

      if (!response.ok) {
        const error = await response.text();
        const sanitized = sanitizeErrorMessage(error);
        throw new Error(`Groq API error: ${response.status} - ${sanitized}`);
      }

      const data = await response.json();
      const latency = Date.now() - startTime;

      if (attempt > 0) {
        console.log(`[Explorer] Retry succeeded after ${attempt} attempts`);
      }

      return {
        content: data.choices[0].message.content,
        latency,
      };
    } catch (err) {
      clearTimeout(timeoutId);
      lastError = err as Error;

      // Don't retry on timeout or abort errors
      if (err instanceof Error && (err.name === 'AbortError' || err.message.includes('abort'))) {
        throw new Error(`Request timeout after ${timeoutMs}ms`);
      }

      // Don't retry on non-retryable errors
      if (err instanceof Error && !err.message.includes('429') && !err.message.includes('rate limit')) {
        throw err;
      }

      // Retry on other errors if we have attempts left
      if (attempt < maxRetries) {
        const backoffMs = Math.pow(2, attempt) * 1000;
        console.warn(`[Explorer] Request failed, retrying in ${backoffMs}ms (attempt ${attempt + 1}/${maxRetries}): ${err}`);
        await new Promise(resolve => setTimeout(resolve, backoffMs));
        continue;
      }
    }
  }

  throw lastError || new Error('Max retries exceeded');
}

/**
 * Maps generic model names to Groq model IDs
 */
function mapModelToGroq(model: string): string {
  const mapping: Record<string, string> = {
    'llama-70b': 'llama-3.3-70b-versatile',
    'oss-20b': 'llama-3.1-8b-instant', // Use 8b as proxy for 20b
    'oss-120b': 'llama-3.3-70b-versatile', // Use 70b as proxy for 120b
  };
  return mapping[model] || 'llama-3.3-70b-versatile';
}

/**
 * Estimates cost based on model and token usage
 */
function estimateExplorationCost(model: string, inputTokens: number, outputTokens: number): number {
  // Cost per 1M tokens
  const costs: Record<string, { input: number; output: number }> = {
    'llama-70b': { input: 0.59, output: 0.79 }, // Groq llama-3.3-70b
    'oss-20b': { input: 0.05, output: 0.08 },   // Groq llama-3.1-8b
    'oss-120b': { input: 0.59, output: 0.79 },
  };

  const cost = costs[model] || costs['llama-70b'];
  return (inputTokens * cost.input + outputTokens * cost.output) / 1_000_000;
}

/**
 * Parses exploration response to extract findings
 */
function parseExplorationResponse(response: string, theoryId: string): {
  shows_promise: boolean;
  discoveries: string[];
  partial_result: string;
  confidence: number;
  dead_end_reason: string | null;
} {
  // Extract shows promise
  const promiseMatch = response.match(/SHOWS_PROMISE:\s*(yes|no)/i);
  const shows_promise = promiseMatch ? promiseMatch[1].toLowerCase() === 'yes' : false;

  // Extract discoveries (look for bullet points after DISCOVERIES:)
  const discoveriesSection = response.match(/DISCOVERIES:\s*([\s\S]*?)(?=\n\n|SHOWS_PROMISE:|CONFIDENCE:|$)/i);
  const discoveries: string[] = [];
  if (discoveriesSection) {
    const bullets = discoveriesSection[1].match(/[-•]\s*(.+)/g);
    if (bullets) {
      discoveries.push(...bullets.map(b => b.replace(/^[-•]\s*/, '').trim()));
    }
  }

  // Extract partial result
  const partialMatch = response.match(/PARTIAL_RESULT:\s*([\s\S]*?)(?=\n\nDEAD_END_REASON:|CONFIDENCE:|$)/i);
  const partial_result = partialMatch ? partialMatch[1].trim() : '';

  // Extract confidence
  const confidenceMatch = response.match(/CONFIDENCE:\s*(0\.\d+|1\.0)/i);
  let confidence = confidenceMatch ? parseFloat(confidenceMatch[1]) : 0.5;

  // Extract dead end reason
  const deadEndMatch = response.match(/DEAD_END_REASON:\s*([\s\S]*?)$/i);
  const dead_end_reason = deadEndMatch && deadEndMatch[1].trim().length > 0
    ? deadEndMatch[1].trim()
    : null;

  // Adjust confidence based on promise
  if (!shows_promise && dead_end_reason) {
    confidence *= 0.3; // Dead end = low confidence
  }

  return { shows_promise, discoveries, partial_result, confidence, dead_end_reason };
}

/**
 * Explores a theory using the configured model
 * @param theory The theory to investigate
 * @param techniques Relevant techniques from library
 * @param config Explorer configuration
 * @returns Exploration result with discoveries and assessment
 */
export async function explore(
  theory: Theory,
  techniques: Technique[],
  config: ExplorerConfig
): Promise<ExplorationResult> {
  const startTime = Date.now();

  try {
    // Generate prompt
    const prompt = generateExplorationPrompt(theory, techniques);

    // Call Groq API
    const messages: GroqMessage[] = [
      { role: 'user', content: prompt }
    ];

    const { content, latency } = await callGroq(
      messages,
      config.model,
      config.max_tokens,
      config.temperature,
      config.timeout_ms
    );

    // Parse response
    const parsed = parseExplorationResponse(content, theory.id);

    // Estimate cost
    const inputTokens = Math.ceil(prompt.length / 4);
    const outputTokens = Math.ceil(content.length / 4);
    const cost = estimateExplorationCost(config.model, inputTokens, outputTokens);

    return {
      theory_id: theory.id,
      shows_promise: parsed.shows_promise,
      discoveries: parsed.discoveries,
      partial_result: parsed.partial_result,
      reasoning: content,
      confidence: parsed.confidence,
      dead_end_reason: parsed.dead_end_reason,
      cost,
      latency_ms: latency,
      model_used: config.model,
    };
  } catch (err) {
    const latency = Date.now() - startTime;
    const safeError = createSafeError(err, `exploring ${theory.id}`);
    console.error(`[Explorer] Failed to explore ${theory.id}:`, safeError.message);

    // Return failure result with sanitized error message
    return {
      theory_id: theory.id,
      shows_promise: false,
      discoveries: [],
      partial_result: '',
      reasoning: `Error: ${safeError.message}`,
      confidence: 0,
      dead_end_reason: `Exploration failed: ${safeError.message}`,
      cost: 0,
      latency_ms: latency,
      model_used: config.model,
    };
  }
}

/**
 * Default explorer configuration
 */
export const DEFAULT_EXPLORER_CONFIG: ExplorerConfig = {
  model: 'llama-70b',
  max_tokens: 2000,
  temperature: 0.2,  // Slightly higher for exploratory creativity
  timeout_ms: 30000, // 30 seconds
};
