/**
 * Weft LLM Utilities
 * Multi-provider LLM calling with fallback support
 */

export type LLMProvider = 'abacus' | 'openai' | 'grok';

export interface LLMMessage {
  role: 'system' | 'user' | 'assistant';
  content: string;
}

export interface LLMOptions {
  model?: string;
  temperature?: number;
  maxTokens?: number;
  jsonMode?: boolean;
}

const PROVIDER_CONFIGS = {
  abacus: {
    url: 'https://routellm.abacus.ai/v1/chat/completions',
    keyEnv: 'ABACUSAI_API_KEY',
    defaultModel: 'claude-sonnet-4-20250514',
  },
  openai: {
    url: 'https://api.openai.com/v1/chat/completions',
    keyEnv: 'OPENAI_API_KEY',
    defaultModel: 'gpt-4o',
  },
  grok: {
    url: 'https://api.x.ai/v1/chat/completions',
    keyEnv: 'XAI_API_KEY',
    defaultModel: 'grok-3-latest',
  },
} as const;

export async function callLLM(
  messages: LLMMessage[],
  options: LLMOptions = {},
  provider: LLMProvider = 'abacus'
): Promise<string> {
  const config = PROVIDER_CONFIGS[provider];
  const apiKey = process.env[config.keyEnv];

  if (!apiKey) {
    throw new Error(`${config.keyEnv} is not configured`);
  }

  const body: Record<string, unknown> = {
    model: options.model || config.defaultModel,
    messages,
    max_tokens: options.maxTokens || 4000,
    temperature: options.temperature ?? 0.3,
  };

  if (options.jsonMode) {
    body.response_format = { type: 'json_object' };
  }

  console.log(`[Weft LLM] Calling ${provider} with model ${body.model}`);

  // Create AbortController for timeout
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 90000); // 90 second timeout

  try {
    const response = await fetch(config.url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiKey}`,
      },
      body: JSON.stringify(body),
      signal: controller.signal,
    });

    clearTimeout(timeoutId);

    if (!response.ok) {
      const errorText = await response.text();
      console.error(`[Weft LLM] API error: ${response.status} - ${errorText.substring(0, 200)}`);
      throw new Error(`LLM API error (${provider}): ${response.status} - ${errorText.substring(0, 200)}`);
    }

    const data = await response.json();
    const content = data.choices?.[0]?.message?.content;

    if (!content) {
      console.error('[Weft LLM] No content in response:', JSON.stringify(data).substring(0, 200));
      throw new Error('No content in LLM response');
    }

    console.log(`[Weft LLM] Success - received ${content.length} chars`);
    return content;
  } catch (error) {
    clearTimeout(timeoutId);
    if (error instanceof Error && error.name === 'AbortError') {
      console.error('[Weft LLM] Request timed out after 90 seconds');
      throw new Error('LLM request timed out. Please try a simpler request.');
    }
    throw error;
  }
}

export async function callLLMWithFallback(
  messages: LLMMessage[],
  options: LLMOptions = {},
  providers: LLMProvider[] = ['abacus', 'openai', 'grok']
): Promise<{ content: string; provider: LLMProvider }> {
  let lastError: Error | null = null;

  for (const provider of providers) {
    try {
      const content = await callLLM(messages, options, provider);
      return { content, provider };
    } catch (error) {
      lastError = error instanceof Error ? error : new Error(String(error));
      console.warn(`LLM call failed for ${provider}:`, lastError.message);
    }
  }

  throw lastError || new Error('All LLM providers failed');
}

/**
 * Repair common JSON issues from LLM output
 */
function repairJSON(jsonStr: string): string {
  let repaired = jsonStr;
  
  // Remove trailing commas before } or ]
  repaired = repaired.replace(/,(\s*[}\]])/g, '$1');
  
  // Fix unquoted property names (simple cases)
  repaired = repaired.replace(/(\{|\,)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
  
  // Fix single quotes to double quotes (but not inside strings)
  repaired = repaired.replace(/:\s*'([^']*)'/g, ':"$1"');
  
  // Remove comments (// and /* */)
  repaired = repaired.replace(/\/\/[^\n]*/g, '');
  repaired = repaired.replace(/\/\*[\s\S]*?\*\//g, '');
  
  // Fix truncated arrays - close any unclosed arrays/objects
  const openBrackets = (repaired.match(/\[/g) || []).length;
  const closeBrackets = (repaired.match(/\]/g) || []).length;
  const openBraces = (repaired.match(/\{/g) || []).length;
  const closeBraces = (repaired.match(/\}/g) || []).length;
  
  // Add missing closing brackets
  for (let i = 0; i < openBrackets - closeBrackets; i++) {
    repaired += ']';
  }
  for (let i = 0; i < openBraces - closeBraces; i++) {
    repaired += '}';
  }
  
  // Remove any trailing incomplete key-value pairs
  repaired = repaired.replace(/,\s*"[^"]*"\s*:\s*$/g, '');
  repaired = repaired.replace(/,\s*"[^"]*"\s*$/g, '');
  
  return repaired;
}

export function parseJSONResponse<T>(content: string): T {
  // Try to extract JSON from markdown code blocks
  const jsonMatch = content.match(/```(?:json)?\n?([\s\S]*?)```/);
  let jsonStr = jsonMatch ? jsonMatch[1].trim() : content.trim();
  
  // First attempt: direct parse
  try {
    return JSON.parse(jsonStr) as T;
  } catch {
    // Continue to repair attempts
  }
  
  // Try to find JSON object or array in the content
  const objectMatch = jsonStr.match(/\{[\s\S]*\}/);
  const arrayMatch = jsonStr.match(/\[[\s\S]*\]/);
  const match = objectMatch || arrayMatch;
  
  if (match) {
    jsonStr = match[0];
  }
  
  // Second attempt: repair and parse
  try {
    const repaired = repairJSON(jsonStr);
    return JSON.parse(repaired) as T;
  } catch (e) {
    // Third attempt: more aggressive repair
    try {
      // Try to extract just the essential structure
      let aggressive = repairJSON(jsonStr);
      
      // If still failing, try to find the last valid closing brace/bracket
      const lastBrace = aggressive.lastIndexOf('}');
      const lastBracket = aggressive.lastIndexOf(']');
      
      if (lastBrace > 0 || lastBracket > 0) {
        const cutPoint = Math.max(lastBrace, lastBracket) + 1;
        aggressive = aggressive.substring(0, cutPoint);
        aggressive = repairJSON(aggressive);
        return JSON.parse(aggressive) as T;
      }
    } catch {
      // Fall through to error
    }
    
    throw new Error(`Failed to parse JSON after repair attempts: ${e}`);
  }
}
