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}`);
    // Try gergy adapter as fallback (works with worker proxy, no CLI needed)
    try {
      adapter = await import(pathToFileURL(join(__dirname, 'adapters', 'gergy.mjs')).href);
      console.log(`[llm] Fell back to gergy adapter (worker proxy mode)`);
    } catch {
      console.error(`[llm] No adapter available. UI will load but LLM features will be disabled.`);
      return;
    }
  }

  if (!adapter.isAvailable()) {
    // Try gergy adapter as fallback before giving up
    if (cli !== 'gergy') {
      try {
        adapter = await import(pathToFileURL(join(__dirname, 'adapters', 'gergy.mjs')).href);
        if (adapter.isAvailable()) {
          console.log(`[llm] CLI "${cli}" not found — fell back to gergy adapter (worker proxy mode)`);
          return;
        }
      } catch {}
    }
    console.warn(`[llm] CLI "${cli}" is not available on this system.`);
    console.warn(`[llm] The UI will load so you can configure your LLM provider in Settings.`);
    adapter = null;
    return;
  }

  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 getAdapter() {
  return adapter;
}

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('No LLM adapter available. Configure your AI provider in Settings.');

  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;

  // rawCompletion: direct LLM call without tools.
  // Used by skills that need raw output (HTML, markdown, JSON).
  // For adapters with streamChat (claude, gergy), calls streamChat
  // with no allowedTools — clean output with no tool traces.
  let response;
  if (opts.rawCompletion && typeof adapter.streamChat === 'function') {
    response = await adapter.streamChat(prompt, { model: resolvedModel || opts.model });
  } else {
    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;

  // All adapters support onStream via the opts parameter to ask().
  // Always use the streaming path.
  if (typeof adapter.ask !== 'function') {
    throw new Error('Adapter does not implement ask()');
  }

  // 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
  console.log(`[streamLLM] Starting adapter.ask (${adapter.name || 'unknown'} adapter)...`);
  const adapterPromise = adapter.ask(prompt, streamOpts).then(result => {
    console.log(`[streamLLM] adapter.ask resolved (${(result || '').length} chars)`);
    finalResult = result;
    finished = true;
    // Wake up the generator one last time
    const wake = notify;
    waiting = new Promise(resolve => { notify = resolve; });
    wake();
  }).catch(err => {
    console.error(`[streamLLM] adapter.ask error: ${err.message}`);
    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 };
}

/**
 * Check if the active adapter supports streamChat.
 */
export function hasStreamChat() {
  return typeof adapter?.streamChat === 'function';
}

/**
 * Streaming chat with optional tool control.
 * Uses adapter.streamChat() for adapters that support it (claude, gergy).
 * Yields { text, done } chunks like streamLLM.
 *
 * @param {string} prompt
 * @param {object} opts
 * @param {string} opts.model - tier or model name
 * @param {string[]|null} opts.allowedTools - tools to allow (null = none)
 */
export async function* streamChatLLM(prompt, opts = {}) {
  if (!adapter) throw new Error('LLM not initialized — call initLLM() first');
  if (typeof adapter.streamChat !== 'function') {
    throw new Error('Adapter does not support streamChat');
  }

  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 queue = [];
  let finished = false;
  let finalResult = '';
  let error = null;
  let notify = null;
  let waiting = new Promise(resolve => { notify = resolve; });

  const chatPromise = adapter.streamChat(prompt, {
    model: resolvedModel || opts.model,
    allowedTools: opts.allowedTools || null,
    onStream: (text) => {
      queue.push(text);
      const wake = notify;
      waiting = new Promise(resolve => { notify = resolve; });
      wake();
    }
  }).then(result => {
    finalResult = result;
    finished = true;
    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();
  });

  while (true) {
    while (queue.length > 0) {
      yield { text: queue.shift(), done: false };
    }
    if (finished) break;
    await waiting;
  }
  while (queue.length > 0) {
    yield { text: queue.shift(), done: false };
  }

  if (error) throw error;

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

  yield { text: finalResult, done: true };
}

export { TIER_MAP };
