/**
 * Decomposer Agent - Generates theories/approaches to explore
 * Uses gpt-5.1 (via OpenAI API) with technique guidance
 *
 * ARCHITECTURE: Generates both evidence-based theories (from RuVector)
 * and novel theories (creative exploration) in 70/30 ratio
 */

import { Technique, Theory, Decomposition, DecomposerConfig } from '../lib/types';
import { sanitizeErrorMessage, createSafeError } from '../lib/input-validation';

const OPENAI_API_KEY = process.env.OPENAI_API_KEY;

// Validate API key is set
if (!OPENAI_API_KEY) {
  console.warn('[Decomposer] Warning: OPENAI_API_KEY not set in environment');
}

interface OpenAIMessage {
  role: 'system' | 'user' | 'assistant';
  content: string;
}

/**
 * Generates decomposition prompt for theory generation
 * User's intent: "For similar problems, approaches X, Y, Z worked based on ruvector,
 * let's explore those and also A, B, C"
 */
function generateTheoryPrompt(
  problem: string,
  techniques: Technique[],
  config: DecomposerConfig
): string {
  // Separate high-similarity (evidence-based) from others
  const evidenceBased = techniques
    .filter(t => t.similarity && t.similarity >= 0.5)
    .slice(0, 5);

  const evidenceContext = evidenceBased.map(t => {
    const steps = JSON.parse(t.step_pattern);
    return `• ${t.name} (${(t.similarity! * 100).toFixed(1)}% match on similar problems)
  Category: ${t.category}
  Key Insight: ${t.key_insight}
  When to use: ${t.recognition_pattern}
  Steps: ${steps.join(' → ')}`;
  }).join('\n\n');

  const targetTheories = config.max_theories;
  const evidenceTheories = Math.floor(targetTheories * (1 - config.novel_theory_ratio));
  const novelTheories = targetTheories - evidenceTheories;

  return `You are an expert at generating THEORIES/APPROACHES to explore for olympiad math problems.

PROBLEM:
${problem}

TECHNIQUES FROM LIBRARY (evidence-based - worked on similar problems):
${evidenceContext}

YOUR TASK:
Generate ${targetTheories} different THEORIES/APPROACHES to explore:
- ${evidenceTheories} theories based on the library techniques above (evidence-based)
- ${novelTheories} creative/novel theories that might also work

CRITICAL: You are NOT solving the problem or creating sub-tasks.
You are generating APPROACHES to EXPLORE.

Each theory should specify:
1. approach: High-level strategy (e.g., "Combinatorial counting with digit selection")
2. source: "ruvector" (based on library) or "novel" (creative)
3. technique_name: Name from library if source="ruvector", null otherwise
4. hypothesis: What we think will work and why
5. technique_categories: Relevant categories (e.g., ["combinatorics", "counting"])
6. exploration_questions: 3-5 questions to investigate (NOT steps to solve)
7. confidence: Initial promise level (0.0-1.0)

EXPLORATION QUESTIONS should be investigative, not solution-oriented:
✓ GOOD: "How many ways can we select 2 digits from 0-9?"
✓ GOOD: "What patterns emerge when we try small examples?"
✓ GOOD: "Does this approach handle edge cases (leading zeros)?"
✗ BAD: "Count the 2-digit numbers" (that's a task, not exploration)

Output valid JSON:
{
  "theories": [
    {
      "id": "theory-1",
      "approach": "Combinatorial counting with digit selection",
      "source": "ruvector",
      "technique_name": "Combination Formula",
      "hypothesis": "If we select 1 or 2 digits, then count valid numbers using those digits, we can enumerate all possibilities",
      "technique_categories": ["combinatorics", "counting-principles"],
      "exploration_questions": [
        "How many 1-digit numbers can we form?",
        "For 2 digits selected, how many n-digit numbers can we form?",
        "How do we handle repeated digits vs distinct digits?",
        "What about leading zero constraints?"
      ],
      "confidence": 0.75
    },
    {
      "id": "theory-2",
      "approach": "Direct case-by-case enumeration",
      "source": "novel",
      "technique_name": null,
      "hypothesis": "Break into 1-digit, 2-digit, 3-digit, 4-digit cases and count each separately",
      "technique_categories": ["enumeration", "case-analysis"],
      "exploration_questions": [
        "What's the count for 1-digit numbers?",
        "Can we find a pattern for 2-digit numbers with constraint?",
        "Does the pattern generalize to 3-digit and 4-digit?"
      ],
      "confidence": 0.65
    }
  ],
  "synthesis_strategy": "Explore all theories in parallel, keep promising ones, combine insights",
  "estimated_difficulty": 7
}

IMPORTANT: Return ONLY valid JSON, no additional text.`;
}

/**
 * Calls OpenAI API with theory generation prompt
 * Includes fallback from gpt-5.1 to gpt-4o if first attempt fails
 */
async function callOpenAI(
  messages: OpenAIMessage[],
  model: string
): Promise<string> {
  const primaryModel = model === 'gpt-5.1' ? 'gpt-4o' : model;

  try {
    // Try primary model
    const response = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${OPENAI_API_KEY}`,
      },
      body: JSON.stringify({
        model: primaryModel,
        messages,
        temperature: 0.3,  // Increased for creative theory generation
        max_tokens: 3000,
      }),
    });

    if (!response.ok) {
      const error = await response.text();
      const sanitized = sanitizeErrorMessage(error);
      throw new Error(`OpenAI API error: ${response.status} - ${sanitized}`);
    }

    const data = await response.json();
    return data.choices[0].message.content;
  } catch (err) {
    const safeError = createSafeError(err, 'calling OpenAI API');

    // Fallback to gpt-4o if primary model was gpt-5.1
    if (model === 'gpt-5.1' && primaryModel === 'gpt-4o') {
      console.warn('[Decomposer] Primary model failed, already using fallback gpt-4o');
      throw safeError;
    }

    // If we haven't tried gpt-4o yet, try it as fallback
    if (primaryModel !== 'gpt-4o') {
      console.warn(`[Decomposer] Model ${primaryModel} failed, falling back to gpt-4o`);

      try {
        const fallbackResponse = await fetch('https://api.openai.com/v1/chat/completions', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${OPENAI_API_KEY}`,
          },
          body: JSON.stringify({
            model: 'gpt-4o',
            messages,
            temperature: 0.3,
            max_tokens: 3000,
          }),
        });

        if (!fallbackResponse.ok) {
          const fallbackError = await fallbackResponse.text();
          const sanitized = sanitizeErrorMessage(fallbackError);
          throw new Error(`OpenAI fallback error: ${fallbackResponse.status} - ${sanitized}`);
        }

        const fallbackData = await fallbackResponse.json();
        console.log('[Decomposer] Fallback to gpt-4o succeeded');
        return fallbackData.choices[0].message.content;
      } catch (fallbackErr) {
        console.error('[Decomposer] Both primary and fallback models failed');
        const safeFallbackError = createSafeError(fallbackErr, 'fallback model');
        throw safeFallbackError;
      }
    }

    throw safeError;
  }
}

/**
 * Estimates cost based on model and token usage
 */
function estimateDecompositionCost(model: string, inputTokens: number, outputTokens: number): number {
  // Cost per 1M tokens (approximate)
  const costs: Record<string, { input: number; output: number }> = {
    'gpt-5.1': { input: 2.50, output: 10.00 }, // gpt-4o pricing
    'gpt-4o': { input: 2.50, output: 10.00 },
    'oss-120b': { input: 0.50, output: 0.50 },
    'llama-70b': { input: 0.10, output: 0.10 },
  };

  const cost = costs[model] || costs['gpt-4o'];
  return (inputTokens * cost.input + outputTokens * cost.output) / 1_000_000;
}

/**
 * Decomposes a problem into theories/approaches to explore
 * @param problem The original problem statement
 * @param relevantTechniques Techniques from library with similarity scores
 * @param config Decomposer configuration
 * @returns Decomposition with theories and synthesis strategy
 */
export async function decompose(
  problem: string,
  relevantTechniques: Technique[],
  config: DecomposerConfig
): Promise<Decomposition> {
  const startTime = Date.now();

  // Generate prompt with technique context
  const prompt = generateTheoryPrompt(problem, relevantTechniques, config);

  // Call OpenAI API
  const messages: OpenAIMessage[] = [
    { role: 'user', content: prompt }
  ];

  const responseText = await callOpenAI(messages, config.model);

  // Parse JSON response with improved error handling
  let decompositionData;
  try {
    // Try direct JSON parse first (fastest path)
    decompositionData = JSON.parse(responseText);
  } catch (directErr) {
    try {
      // Fallback: Extract JSON from response (handle markdown code blocks)
      const jsonMatch = responseText.match(/\{[\s\S]*?\n\s*"theories"\s*:[\s\S]*?\n\s*"synthesis_strategy"[\s\S]*?\}/);
      if (!jsonMatch) {
        throw new Error('No valid JSON structure found in response');
      }
      decompositionData = JSON.parse(jsonMatch[0]);
    } catch (regexErr) {
      // Last resort: try basic JSON extraction
      const basicMatch = responseText.match(/\{[\s\S]*\}/);
      if (!basicMatch) {
        const safeError = createSafeError(directErr, 'parsing decomposition response');
        console.error('Failed to parse decomposition response:', safeError.message);
        throw safeError;
      }
      try {
        decompositionData = JSON.parse(basicMatch[0]);
      } catch (finalErr) {
        const safeError = createSafeError(finalErr, 'parsing decomposition JSON');
        console.error('Failed to parse decomposition response:', safeError.message);
        throw safeError;
      }
    }
  }

  // Validate and normalize theories
  const theories: Theory[] = decompositionData.theories || [];

  if (theories.length < 3 || theories.length > config.max_theories) {
    console.warn(
      `Decomposer returned ${theories.length} theories, expected ${config.max_theories}. Adjusting...`
    );
  }

  // Validate theory structure
  for (const theory of theories) {
    if (!theory.id || !theory.approach || !theory.hypothesis) {
      throw new Error(`Invalid theory structure: ${JSON.stringify(theory)}`);
    }
    // Ensure exploration_questions is an array
    if (!Array.isArray(theory.exploration_questions)) {
      theory.exploration_questions = [];
    }
    // Ensure technique_categories is an array
    if (!Array.isArray(theory.technique_categories)) {
      theory.technique_categories = [theory.technique_categories || 'unknown'];
    }
  }

  // Estimate cost (approximate token counts)
  const inputTokens = Math.ceil(prompt.length / 4);
  const outputTokens = Math.ceil(responseText.length / 4);
  const cost = estimateDecompositionCost(config.model, inputTokens, outputTokens);

  const latencyMs = Date.now() - startTime;

  const evidenceCount = theories.filter(t => t.source === 'ruvector').length;
  const novelCount = theories.filter(t => t.source === 'novel').length;

  console.log(
    `[Decomposer] Generated ${theories.length} theories ` +
    `(${evidenceCount} evidence-based, ${novelCount} novel) ` +
    `in ${latencyMs}ms ($${cost.toFixed(4)})`
  );

  return {
    theories,
    synthesis_strategy: decompositionData.synthesis_strategy || 'Explore theories in parallel, combine successful ones',
    estimated_difficulty: decompositionData.estimated_difficulty || 5,
    cost,
  };
}

/**
 * Default decomposer configuration
 */
export const DEFAULT_DECOMPOSER_CONFIG: DecomposerConfig = {
  model: 'gpt-5.1',
  max_theories: 10,
  technique_context_size: 5,
  novel_theory_ratio: 0.3,  // 30% novel, 70% evidence-based
};
