import { BRAND } from '@/lib/white-label/constants';

/**
 * GFS LLM Client - The Ghost's connection to intelligence
 * 
 * Features: timeout via AbortController, retry with exponential backoff,
 * input validation, structured error handling.
 */

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

export interface LLMResponse {
  content: string;
  model: string;
  usage: {
    promptTokens: number;
    completionTokens: number;
    totalTokens: number;
  };
  finishReason: string;
}

export interface LLMConfig {
  model?: string;
  temperature?: number;
  maxTokens?: number;
  stream?: boolean;
  timeoutMs?: number;
  maxRetries?: number;
  /** Force cloud LLM even when local is enabled (use for critical/security tasks) */
  forceCloud?: boolean;
}

const DEFAULT_MODEL = 'gpt-4.1';
const CLOUD_API_URL = 'https://routellm.abacus.ai/v1/chat/completions';
const DEFAULT_TIMEOUT_MS = 90_000; // 90 seconds
const DEFAULT_MAX_RETRIES = 2;
const MAX_INPUT_LENGTH = 50_000; // chars per message content

/**
 * Check if local LLM is configured and should be used.
 * When LOCAL_LLM_ENABLED=true, simple/non-critical calls can route locally.
 * The core callLLM function supports a `forceCloud` option to bypass this.
 */
function getLocalConfig(): { enabled: boolean; endpoint: string; model: string } | null {
  if (process.env.LOCAL_LLM_ENABLED !== 'true') return null;
  return {
    enabled: true,
    endpoint: process.env.LOCAL_LLM_ENDPOINT || 'http://localhost:11434',
    model: process.env.LOCAL_LLM_MODEL || 'qwen3.5:35b'
  };
}

export class LLMError extends Error {
  constructor(
    message: string,
    public readonly statusCode?: number,
    public readonly retryable: boolean = false
  ) {
    super(message);
    this.name = 'LLMError';
  }
}

async function sleepMs(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * Try local LLM (Ollama). Returns null if local is unavailable or fails.
 * Automatically falls back to cloud — callers don't need to handle this.
 */
async function tryLocalLLM(
  messages: LLMMessage[],
  config: LLMConfig
): Promise<LLMResponse | null> {
  const local = getLocalConfig();
  if (!local) return null;

  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 30_000); // 30s for local

    const response = await fetch(`${local.endpoint}/api/chat`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        model: local.model,
        messages,
        stream: false,
        options: {
          temperature: config.temperature ?? 0.7,
          num_predict: config.maxTokens ?? 2000
        }
      }),
      signal: controller.signal
    });

    clearTimeout(timeoutId);

    if (!response.ok) {
      console.warn(`[LLM] Local LLM returned ${response.status}, falling back to cloud`);
      return null;
    }

    const data = await response.json();
    const content = data.message?.content || '';
    const tokensUsed = data.eval_count || Math.ceil(content.length / 4);

    console.log(`[LLM] ✅ Local LLM response (${tokensUsed} tokens, $0 cost)`);

    return {
      content,
      model: `local:${local.model}`,
      usage: {
        promptTokens: data.prompt_eval_count || 0,
        completionTokens: tokensUsed,
        totalTokens: (data.prompt_eval_count || 0) + tokensUsed
      },
      finishReason: data.done ? 'stop' : 'unknown'
    };
  } catch (error) {
    console.warn(`[LLM] Local LLM unavailable (${error instanceof Error ? error.message : 'unknown'}), falling back to cloud`);
    return null;
  }
}

export async function callLLM(
  messages: LLMMessage[],
  config: LLMConfig = {}
): Promise<LLMResponse> {
  // Validate input lengths
  for (const msg of messages) {
    if (typeof msg.content === 'string' && msg.content.length > MAX_INPUT_LENGTH) {
      throw new LLMError(
        `Message content exceeds maximum length of ${MAX_INPUT_LENGTH} characters`,
        400,
        false
      );
    }
  }

  // Try local LLM first (unless forceCloud is set)
  if (!config.forceCloud) {
    const localResult = await tryLocalLLM(messages, config);
    if (localResult) return localResult;
  }

  // Cloud path
  const apiKey = process.env.ABACUSAI_API_KEY;
  if (!apiKey) {
    throw new LLMError('ABACUSAI_API_KEY not configured', undefined, false);
  }

  const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
  const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
  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(CLOUD_API_URL, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${apiKey}`,
        },
        body: JSON.stringify({
          model: config.model || DEFAULT_MODEL,
          messages,
          temperature: config.temperature ?? 0.7,
          max_tokens: config.maxTokens ?? 2000,
          stream: false,
        }),
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      if (!response.ok) {
        const errorText = await response.text();
        const retryable = response.status === 429 || response.status >= 500;
        if (retryable && attempt < maxRetries) {
          const backoffMs = Math.min(1000 * Math.pow(2, attempt), 8000);
          console.warn(`[LLM] Retryable error ${response.status}, retrying in ${backoffMs}ms (attempt ${attempt + 1}/${maxRetries})`);
          await sleepMs(backoffMs);
          continue;
        }
        throw new LLMError(`LLM API error: ${response.status} - ${errorText}`, response.status, retryable);
      }

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

      return {
        content: choice?.message?.content || '',
        model: data.model,
        usage: {
          promptTokens: data.usage?.prompt_tokens || 0,
          completionTokens: data.usage?.completion_tokens || 0,
          totalTokens: data.usage?.total_tokens || 0,
        },
        finishReason: choice?.finish_reason || 'unknown',
      };
    } catch (error) {
      clearTimeout(timeoutId);

      if (error instanceof LLMError) throw error;

      const isAbort = error instanceof DOMException && error.name === 'AbortError';
      if (isAbort) {
        lastError = new LLMError(`LLM request timed out after ${timeoutMs}ms`, undefined, true);
        if (attempt < maxRetries) {
          console.warn(`[LLM] Timeout, retrying (attempt ${attempt + 1}/${maxRetries})`);
          continue;
        }
        throw lastError;
      }

      // Network errors are retryable
      if (attempt < maxRetries) {
        const backoffMs = Math.min(1000 * Math.pow(2, attempt), 8000);
        console.warn(`[LLM] Network error, retrying in ${backoffMs}ms (attempt ${attempt + 1}/${maxRetries})`);
        await sleepMs(backoffMs);
        lastError = error instanceof Error ? error : new Error(String(error));
        continue;
      }

      throw error;
    }
  }

  throw lastError || new LLMError('LLM call failed after retries');
}

export function buildAgentSystemPrompt(agent: {
  name: string;
  type: string;
  description?: string | null;
  personality?: unknown;
  capabilities?: unknown;
}): string {
  const personality = agent.personality as Record<string, unknown> || {};
  const capabilities = Array.isArray(agent.capabilities) ? agent.capabilities : [];

  return `You are ${agent.name}, a ${agent.type} agent in the ${BRAND.companyName} system.

${agent.description || 'You assist with operations tasks.'}

Your capabilities: ${capabilities.join(', ') || 'general assistance'}

Personality traits:
- Communication style: ${personality.style || 'professional'}
- Verbosity: ${personality.verbosity || 'moderate'}
- Tone: ${personality.tone || 'helpful'}

Guidelines:
1. Be concise and actionable
2. If you need more information, ask specific questions
3. When completing tasks, provide structured output
4. Flag any concerns or risks you identify
5. Always explain your reasoning briefly

Respond in a structured format when appropriate.`;
}
