import { readFile } from 'fs/promises';
import { join, dirname } from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import { getConfig } from './config.mjs';
import { recordApiCall, getDailyTotalCost } from './db.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));

let adapter = null;

const TIER_MAP = {
  health: 'fast',
  format: 'fast',
  classify: 'fast',
  chat: 'balanced',
  journal: 'balanced',
  analyze: 'balanced',
  summarize: 'balanced',
  'self-improve': 'powerful',
  complex: 'powerful',
  build: 'powerful',

  // v3.0 — Ingestion, KG, and cognitive tiers
  ingest: 'fast',
  extract: 'balanced',
  plan: 'balanced',
  reflect: 'balanced',
  discuss: 'powerful',
  vision: 'powerful',

  // v3.2 — Streaming
  stream: 'balanced'
};

function resolveModel(model) {
  if (!model) return null;

  const requested = String(model).trim();
  if (!requested) return null;

  const key = requested.toLowerCase();
  const { models = {} } = getConfig();

  if (key === 'fast' || key === 'balanced' || key === 'powerful') {
    return models[key] || adapter?.modelMap?.[key] || requested;
  }

  return requested;
}

export async function initLLM() {
  const { cli } = getConfig();

  try {
    const adapterPath = join(__dirname, 'adapters', `${cli}.mjs`);
    adapter = await import(pathToFileURL(adapterPath).href);
  } catch (err) {
    console.error(`[llm] Failed to load adapter "${cli}": ${err.message}`);
    process.exit(1);
  }

  if (!adapter.isAvailable()) {
    console.error(`[llm] CLI "${cli}" is not available on this system.`);
    console.error(`[llm] Install it or change the "cli" field in config.json to an available CLI.`);
    process.exit(1);
  }

  console.log(`[llm] Using ${cli} adapter`);
}

export function pickModel(taskType) {
  return TIER_MAP[taskType] || 'balanced';
}

// Rough cost estimation per tier ($ per 1K chars, ~4 chars per token)
const COST_PER_1K_CHARS = {
  fast: 0.0001,
  balanced: 0.0006,
  powerful: 0.003,
  default: 0.0006,
};

function estimateCost(prompt, response, tier) {
  const key = String(tier || '').toLowerCase();
  const modelKey = key === 'fast' || key === 'balanced' || key === 'powerful' ? key : 'default';
  const totalChars = (prompt?.length || 0) + (response?.length || 0);
  return (totalChars / 1000) * COST_PER_1K_CHARS[modelKey];
}

export function checkBudget() {
  const { dailyBudget } = getConfig();
  if (!dailyBudget) return true;
  const spent = getDailyTotalCost();
  return spent < dailyBudget;
}

export async function askLLM(prompt, opts = {}) {
  if (!adapter) throw new Error('LLM not initialized — call initLLM() first');

  const { dailyBudget } = getConfig();
  if (dailyBudget) {
    const spent = getDailyTotalCost();
    if (spent >= dailyBudget) {
      throw new Error(`Daily budget exhausted ($${spent.toFixed(2)} / $${dailyBudget.toFixed(2)})`);
    }
  }

  const resolvedModel = resolveModel(opts.model);
  const finalOpts = resolvedModel ? { ...opts, model: resolvedModel } : opts;
  const response = await adapter.ask(prompt, finalOpts);

  // Record usage
  const model = resolvedModel || opts.model || 'unknown';
  const cost = estimateCost(prompt, response, opts.model);
  try { recordApiCall(model, cost); } catch {}

  return response;
}

export async function askLLMWithContext(prompt, contextFiles = [], opts = {}) {
  let context = '';
  for (const filePath of contextFiles) {
    try {
      const content = await readFile(filePath, 'utf-8');
      context += `--- ${filePath} ---\n${content}\n\n`;
    } catch {
      context += `--- ${filePath} --- (could not read)\n\n`;
    }
  }
  const fullPrompt = context ? `${context}\n---\n\n${prompt}` : prompt;
  return askLLM(fullPrompt, opts);
}

export function spawnInteractiveLLM(opts) {
  if (!adapter) throw new Error('LLM not initialized');
  if (!adapter.spawnInteractive) throw new Error(`${adapter.name} adapter does not support interactive mode`);
  return adapter.spawnInteractive(opts);
}

/**
 * Streaming version of askLLM. Yields { text, done } chunks.
 *
 * Uses the adapter's onStream callback when available (e.g. Claude adapter).
 * Falls back to a single-chunk yield via askLLM for adapters without streaming.
 *
 * Budget checking and cost recording still apply.
 *
 * @param {string} prompt
 * @param {object} opts - same options as askLLM (model, timeout, etc.)
 * @yields {{ text: string, done: boolean }}
 */
export async function* streamLLM(prompt, opts = {}) {
  if (!adapter) throw new Error('LLM not initialized — call initLLM() first');

  // Budget check
  const { dailyBudget } = getConfig();
  if (dailyBudget) {
    const spent = getDailyTotalCost();
    if (spent >= dailyBudget) {
      throw new Error(`Daily budget exhausted ($${spent.toFixed(2)} / $${dailyBudget.toFixed(2)})`);
    }
  }

  const resolvedModel = resolveModel(opts.model);
  const finalOpts = resolvedModel ? { ...opts, model: resolvedModel } : opts;

  // Check if adapter supports streaming via onStream callback
  const adapterSupportsStream = typeof adapter.ask === 'function' &&
    adapter.ask.length >= 2; // adapter.ask(prompt, opts) — opts can include onStream

  if (!adapterSupportsStream) {
    // Fallback: call askLLM and yield the full result as a single chunk
    const result = await askLLM(prompt, opts);
    yield { text: result, done: true };
    return;
  }

  // Promise-based queue pattern for streaming
  const queue = [];
  let finished = false;
  let finalResult = '';
  let error = null;

  // Signal mechanism: a pending promise that resolves when new data arrives
  let notify = null;
  let waiting = new Promise(resolve => { notify = resolve; });

  const streamOpts = {
    ...finalOpts,
    onStream: (accumulatedText) => {
      queue.push(accumulatedText);
      // Wake up the generator
      const wake = notify;
      waiting = new Promise(resolve => { notify = resolve; });
      wake();
    }
  };

  // Start the adapter call in the background
  const adapterPromise = adapter.ask(prompt, streamOpts).then(result => {
    finalResult = result;
    finished = true;
    // Wake up the generator one last time
    const wake = notify;
    waiting = new Promise(resolve => { notify = resolve; });
    wake();
  }).catch(err => {
    error = err;
    finished = true;
    const wake = notify;
    waiting = new Promise(resolve => { notify = resolve; });
    wake();
  });

  // Yield chunks as they arrive
  while (true) {
    // Drain the queue
    while (queue.length > 0) {
      const text = queue.shift();
      yield { text, done: false };
    }

    // If the adapter has finished, break out
    if (finished) break;

    // Wait for the next chunk or completion
    await waiting;
  }

  // Drain any remaining items that arrived with/after completion
  while (queue.length > 0) {
    const text = queue.shift();
    yield { text, done: false };
  }

  if (error) throw error;

  // Record usage (same as askLLM)
  const model = resolvedModel || opts.model || 'unknown';
  const cost = estimateCost(prompt, finalResult, opts.model);
  try { recordApiCall(model, cost); } catch {}

  // Yield the final complete result
  yield { text: finalResult, done: true };
}

export { TIER_MAP };
