/**
 * AI Node Router — RMP Content Pipeline
 *
 * Routes generation tasks to AI_001 (port 8765) with AI_002 (port 8766) as fallback.
 * Uses AI_NODE_API_KEY for authentication.
 * If AI_001 queue depth > 10, routes to AI_002.
 * If both offline, queues jobs and retries every 5 minutes.
 */

import aiNodes from '../../../config/ai-nodes.json';

export type ContentModel = 'llama3.1:70b' | 'qwen2.5-coder:32b' | 'deepseek-r1';

export interface NodeStatus {
  name: string;
  online: boolean;
  queueDepth: number;
  latencyMs: number;
  host: string;
  port: number;
}

export interface GenerateOptions {
  model?: ContentModel;
  maxTokens?: number;
  temperature?: number;
}

export interface GenerateResult {
  text: string;
  node: string;
  model: string;
  latencyMs: number;
}

// In-memory retry queue (survives within a single serverless instance)
const retryQueue: Array<{
  id: string;
  messages: Array<{ role: string; content: string }>;
  options: GenerateOptions;
  resolve: (v: GenerateResult) => void;
  reject: (e: Error) => void;
  addedAt: number;
}> = [];

let retryTimer: ReturnType<typeof setTimeout> | null = null;

function getApiKey(): string {
  return process.env.AI_NODE_API_KEY || '';
}

function nodeBaseUrl(node: (typeof aiNodes.nodes)[0]): string {
  return `http://${node.host}:${node.port}`;
}

async function getQueueDepth(node: (typeof aiNodes.nodes)[0]): Promise<number> {
  try {
    const res = await fetch(`${nodeBaseUrl(node)}/queue`, {
      headers: { Authorization: `Bearer ${getApiKey()}` },
      signal: AbortSignal.timeout(4_000),
    });
    if (!res.ok) return 999;
    const data = await res.json();
    return typeof data?.depth === 'number' ? data.depth : 0;
  } catch {
    return 999;
  }
}

async function pingNode(node: (typeof aiNodes.nodes)[0]): Promise<{ online: boolean; latencyMs: number; queueDepth: number }> {
  const start = Date.now();
  try {
    const res = await fetch(`${nodeBaseUrl(node)}/ping`, {
      signal: AbortSignal.timeout(6_000),
    });
    const latencyMs = Date.now() - start;
    if (!res.ok) return { online: false, latencyMs, queueDepth: 999 };
    const queueDepth = await getQueueDepth(node);
    return { online: true, latencyMs, queueDepth };
  } catch {
    return { online: false, latencyMs: Date.now() - start, queueDepth: 999 };
  }
}

export async function getNodeStatuses(): Promise<NodeStatus[]> {
  const results = await Promise.all(
    aiNodes.nodes.map(async (node) => {
      const { online, latencyMs, queueDepth } = await pingNode(node);
      return {
        name: node.name,
        online,
        queueDepth,
        latencyMs,
        host: node.host,
        port: node.port,
      };
    })
  );
  return results;
}

async function callNode(
  node: (typeof aiNodes.nodes)[0],
  messages: Array<{ role: string; content: string }>,
  options: GenerateOptions
): Promise<string> {
  const model = options.model || 'llama3.1:70b';
  const payload = {
    model,
    messages,
    stream: false,
    options: {
      temperature: options.temperature ?? 0.7,
      num_predict: options.maxTokens ?? 3000,
    },
  };

  const res = await fetch(`${nodeBaseUrl(node)}/v1/chat/completions`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${getApiKey()}`,
    },
    body: JSON.stringify(payload),
    signal: AbortSignal.timeout(120_000),
  });

  if (!res.ok) throw new Error(`Node ${node.name} HTTP ${res.status}`);
  const data = await res.json();
  const text = data?.choices?.[0]?.message?.content || '';
  if (!text) throw new Error(`Empty response from ${node.name}`);
  return text;
}

function scheduleRetry() {
  if (retryTimer) return;
  retryTimer = setTimeout(async () => {
    retryTimer = null;
    if (retryQueue.length === 0) return;

    const statuses = await getNodeStatuses();
    const available = statuses.find((s) => s.online);
    if (!available) {
      scheduleRetry();
      return;
    }

    const node = aiNodes.nodes.find((n) => n.name === available.name)!;
    const batch = retryQueue.splice(0, 5);
    for (const job of batch) {
      try {
        const text = await callNode(node, job.messages, job.options);
        job.resolve({ text, node: node.name, model: job.options.model || 'llama3.1:70b', latencyMs: 0 });
      } catch (e) {
        job.reject(e instanceof Error ? e : new Error('Retry failed'));
      }
    }

    if (retryQueue.length > 0) scheduleRetry();
  }, 5 * 60 * 1000); // 5 minutes
}

export async function generateContent(
  messages: Array<{ role: string; content: string }>,
  options: GenerateOptions = {}
): Promise<GenerateResult> {
  const [node1Status, node2Status] = await Promise.all([
    pingNode(aiNodes.nodes[0]),
    pingNode(aiNodes.nodes[1]),
  ]);

  const start = Date.now();

  // Route: AI_001 if online and queue <= 10, else AI_002, else queue
  if (node1Status.online && node1Status.queueDepth <= 10) {
    const text = await callNode(aiNodes.nodes[0], messages, options);
    return { text, node: 'AI_001', model: options.model || 'llama3.1:70b', latencyMs: Date.now() - start };
  }

  if (node2Status.online) {
    const text = await callNode(aiNodes.nodes[1], messages, options);
    return { text, node: 'AI_002', model: options.model || 'llama3.1:70b', latencyMs: Date.now() - start };
  }

  // Both offline — queue and retry
  return new Promise((resolve, reject) => {
    retryQueue.push({
      id: `job_${Date.now()}_${Math.random().toString(36).slice(2)}`,
      messages,
      options,
      resolve,
      reject,
      addedAt: Date.now(),
    });
    scheduleRetry();
    // Immediately reject with queue info so caller knows it's queued
    reject(new Error('QUEUED: Both AI nodes offline. Job queued for retry in 5 minutes.'));
  });
}
