/**
 * Gergy native agentic adapter — multi-provider API with tool use.
 *
 * Calls LLM APIs directly via streaming SSE, detects tool_use blocks,
 * executes tools locally, and loops. Supports Anthropic, OpenAI, xAI,
 * and Google Gemini via a unified streaming client.
 *
 * Activate by setting "cli": "gergy" in config.json and providing
 * the relevant API key (env var or config.agentic.apiKey).
 *
 * Provider is configured via config.agentic.provider (e.g. "anthropic", "openai", "xai", "gemini").
 */

import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { readFileSync, existsSync } from 'fs';
import { PassThrough } from 'stream';
import { streamMessages, PROVIDERS } from '../agentic-tools/api-client.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const AGENT_ROOT = join(__dirname, '..');
// In packaged Electron, config.json lives in AGENT_DATA_DIR (userData), not the app bundle
const CONFIG_DIR = process.env.AGENT_DATA_DIR || AGENT_ROOT;

export const name = 'gergy';

/**
 * Read agentic config from config.json without importing config.mjs
 * (avoids circular dependency since llm.mjs imports adapters dynamically).
 */
function readAgenticConfig() {
  try {
    const raw = readFileSync(join(CONFIG_DIR, 'config.json'), 'utf-8');
    const config = JSON.parse(raw);
    return config.agentic || {};
  } catch {
    return {};
  }
}

/**
 * Read worker proxy config from config.json.
 */
function readWorkerConfig() {
  try {
    const raw = readFileSync(join(CONFIG_DIR, 'config.json'), 'utf-8');
    const config = JSON.parse(raw);
    return config.worker || {};
  } catch {
    return {};
  }
}

// First available provider key (used as fallback if nothing configured)
const DEFAULT_PROVIDER = Object.keys(PROVIDERS)[0];

/**
 * Resolve the active provider config.
 * Falls back to worker proxy when no direct API key is available.
 */
function getProviderConfig() {
  const agenticCfg = readAgenticConfig();
  const providerName = agenticCfg.provider || DEFAULT_PROVIDER;
  const base = PROVIDERS[providerName];
  if (!base) {
    const fallback = PROVIDERS[DEFAULT_PROVIDER];
    return { ...fallback, providerName: DEFAULT_PROVIDER };
  }
  const merged = { ...base, ...agenticCfg, providerName };

  // If no direct API key, fall back to worker proxy
  if (!merged.apiKey && !(base.envKey && process.env[base.envKey])) {
    const workerCfg = readWorkerConfig();
    if (workerCfg.url) {
      merged.baseUrl = workerCfg.url + '/api';
      merged.workerMode = true;
    }
  }
  return merged;
}

/**
 * Get the model map for the active provider.
 */
function getModelMap() {
  const provider = getProviderConfig();
  return provider.models || PROVIDERS[DEFAULT_PROVIDER].models;
}

export const modelMap = {
  fast: 'fast',
  balanced: 'balanced',
  powerful: 'powerful'
};

function resolveModel(model) {
  const key = String(model || 'balanced').trim().toLowerCase();
  const map = getModelMap();
  if (key === 'fast' || key === 'balanced' || key === 'powerful') {
    return map[key] || model;
  }
  return String(model || map.balanced);
}

/**
 * Resolve the API key from env or config.
 * Falls back to worker token when no direct key is available.
 */
function getApiKey() {
  const provider = getProviderConfig();
  // Check config.agentic.apiKey first
  if (provider.apiKey) return provider.apiKey;
  // Then check provider-specific env var
  if (provider.envKey && process.env[provider.envKey]) return process.env[provider.envKey];
  // Fall back to worker token
  const workerCfg = readWorkerConfig();
  if (workerCfg.url && workerCfg.token) return workerCfg.token;
  return null;
}

/**
 * Build extra headers for worker proxy mode (user tracking).
 */
function getExtraHeaders() {
  const provider = getProviderConfig();
  if (!provider.workerMode) return {};
  try {
    const config = JSON.parse(readFileSync(join(CONFIG_DIR, 'config.json'), 'utf-8'));
    const userName = config.userName;
    if (userName) return { 'x-user-id': userName };
  } catch {}
  return {};
}

/**
 * Check if the adapter is available (API key present).
 */
export function isAvailable() {
  return !!getApiKey();
}

/**
 * Get workspace root for tool execution context.
 */
function getWorkspaceDir() {
  try {
    const config = JSON.parse(readFileSync(join(AGENT_ROOT, 'config.json'), 'utf-8'));
    const root = config.workspaceRoot || join(AGENT_ROOT, 'workspaces');
    const activeId = config.activeWorkspace || 'default';
    return join(root, activeId);
  } catch {
    return join(AGENT_ROOT, 'workspaces', 'default');
  }
}

/**
 * Build the agentic system prompt.
 */
function buildSystemPrompt(workspaceRoot) {
  return `You are an autonomous AI agent with full access to tools for working with files and executing commands.

## Workspace
Your workspace is: ${workspaceRoot}
All file operations should target this directory unless otherwise specified.

## Tool Execution
All tool calls are automatically approved and executed immediately. You have full read/write access to the workspace. There is no approval step, no permission gate, and no user confirmation needed.

IMPORTANT: Never output placeholder text like "pending approval", "once you approve", "waiting for confirmation", or similar. Every tool call you make executes immediately. When writing files, always include the complete, final content — never partial drafts or summaries of what you would write.

## Guidelines
- Read files before editing them
- Use absolute paths for all file operations
- Keep file operations within the workspace
- For complex tasks, break them into steps using tools
- When writing or editing files, always provide the full intended content
- Never mention file permissions, sandbox limitations, or approval workflows
- If you need clarification from the user, use AskUserQuestion`;
}

/**
 * Stream a conversation with the LLM API, handling the agentic tool loop.
 */
async function* agenticLoop(messages, opts = {}) {
  const apiKey = getApiKey();
  if (!apiKey) {
    const provider = getProviderConfig();
    throw new Error(`API key not set. Provide ${provider.envKey || 'an API key'} env var or set config.agentic.apiKey`);
  }

  const provider = getProviderConfig();
  const model = resolveModel(opts.model);
  const maxTurns = opts.maxTurns || 50;
  const workspaceRoot = getWorkspaceDir();

  console.log(`[gergy] Starting agentic loop — provider: ${provider.providerName}, model: ${model}, workspace: ${workspaceRoot}`);

  // Lazy-load tool definitions and executor
  let toolDefs = [];
  let toolExecutors = null;
  let executeToolUseFn = null;
  try {
    const toolIndex = await import('../agentic-tools/index.mjs');
    // Ensure lazy tools (bash, websearch, task, askuser, etc.) are loaded
    if (typeof toolIndex.loadLazyTools === 'function') {
      await toolIndex.loadLazyTools();
    }
    toolDefs = toolIndex.getToolDefinitions(workspaceRoot, AGENT_ROOT);
    toolExecutors = toolIndex.getToolExecutors(workspaceRoot, AGENT_ROOT);
    const executor = await import('../agentic-tools/executor.mjs');
    executeToolUseFn = executor.executeToolUse;
    console.log(`[gergy] Tools loaded: ${toolDefs.length} definitions`);
  } catch (err) {
    console.log(`[gergy] Tools not available: ${err.message}`);
  }

  const system = [{ type: 'text', text: buildSystemPrompt(workspaceRoot) }];
  let currentMessages = [...messages];
  let turn = 0;

  while (turn < maxTurns) {
    turn++;
    console.log(`[gergy] Turn ${turn}/${maxTurns} — ${currentMessages.length} messages`);

    const request = {
      model,
      max_tokens: opts.maxTokens || 16384,
      messages: currentMessages,
      system
    };

    if (toolDefs.length > 0) {
      request.tools = toolDefs;
    }

    const contentBlocks = [];
    let currentBlock = null;
    const abortController = new AbortController();
    const timeout = opts.timeout || 600_000;
    const timer = setTimeout(() => abortController.abort(), timeout);

    try {
      for await (const event of streamMessages(request, {
        provider: provider.providerName,
        apiKey,
        baseUrl: provider.baseUrl,
        signal: abortController.signal,
        extraHeaders: getExtraHeaders()
      })) {
        switch (event.type) {
          case 'content_block_start':
            if (event.content_block?.type === 'text') {
              currentBlock = { type: 'text', text: '' };
            } else if (event.content_block?.type === 'tool_use') {
              currentBlock = {
                type: 'tool_use',
                id: event.content_block.id,
                name: event.content_block.name,
                input: {},
                _partialJson: ''
              };
              console.log(`[gergy] Tool call starting: ${event.content_block.name}`);
            } else {
              currentBlock = null;
            }
            break;

          case 'content_block_delta':
            if (!currentBlock) break;
            if (event.delta?.type === 'text_delta' && currentBlock.type === 'text') {
              currentBlock.text += event.delta.text;
              yield { type: 'text_delta', text: event.delta.text };
            } else if (event.delta?.type === 'input_json_delta' && currentBlock.type === 'tool_use') {
              currentBlock._partialJson += event.delta.partial_json;
              try {
                currentBlock.input = JSON.parse(currentBlock._partialJson);
              } catch {}
            }
            break;

          case 'content_block_stop':
            if (currentBlock) {
              if (currentBlock.type === 'tool_use') {
                console.log(`[gergy] Tool call ready: ${currentBlock.name}(${JSON.stringify(currentBlock.input).slice(0, 200)})`);
                delete currentBlock._partialJson;
              }
              contentBlocks.push(currentBlock);
              currentBlock = null;
            }
            break;

          case 'message_stop':
            break;

          case 'error':
            throw new Error(event.error?.message || 'Stream error');
        }
      }
    } catch (err) {
      if (err.name === 'AbortError') {
        throw new Error(`API timed out after ${timeout}ms`);
      }
      throw err;
    } finally {
      clearTimeout(timer);
    }

    const textBlocks = contentBlocks.filter(b => b.type === 'text');
    const toolUses = contentBlocks.filter(b => b.type === 'tool_use');
    console.log(`[gergy] Turn ${turn} response: ${textBlocks.length} text block(s) (${textBlocks.reduce((n, b) => n + b.text.length, 0)} chars), ${toolUses.length} tool call(s)`);

    const assistantContent = contentBlocks.map(b => {
      if (b.type === 'text') return { type: 'text', text: b.text };
      if (b.type === 'tool_use') return { type: 'tool_use', id: b.id, name: b.name, input: b.input };
      return b;
    });

    if (toolUses.length === 0 || !executeToolUseFn || !toolExecutors) {
      console.log(`[gergy] No tool calls — done after ${turn} turn(s)`);
      yield { type: 'done', contentBlocks };
      return;
    }

    // Execute tools
    const toolResults = [];
    for (const toolUse of toolUses) {
      console.log(`[gergy] Executing tool: ${toolUse.name}`);
      yield { type: 'tool_start', name: toolUse.name, id: toolUse.id };

      const result = await executeToolUseFn(toolUse, toolExecutors, {
        workspaceRoot,
        agentRoot: AGENT_ROOT,
        readFileTimestamps: opts._readFileTimestamps || {},
        ...opts.toolContext
      });

      const resultPreview = typeof result.content === 'string' ? result.content.slice(0, 150) : JSON.stringify(result.content).slice(0, 150);
      console.log(`[gergy] Tool ${toolUse.name} ${result.is_error ? 'FAILED' : 'OK'}: ${resultPreview}`);

      toolResults.push({
        type: 'tool_result',
        tool_use_id: toolUse.id,
        content: result.content,
        is_error: result.is_error || false
      });

      yield { type: 'tool_done', name: toolUse.name, id: toolUse.id, is_error: result.is_error };
    }

    currentMessages = [
      ...currentMessages,
      { role: 'assistant', content: assistantContent },
      { role: 'user', content: toolResults }
    ];
  }

  console.log(`[gergy] Max turns (${maxTurns}) reached`);
  yield { type: 'max_turns' };
}

/**
 * Send a prompt and return the final text result.
 * Conforms to the adapter interface: ask(prompt, opts) -> string
 */
export async function ask(prompt, { model = 'balanced', timeout = 600_000, sandboxed = true, onStream, extraArgs = [] } = {}) {
  const messages = [{ role: 'user', content: prompt }];
  let accumulatedText = '';
  const readFileTimestamps = {};
  let retries = 0;
  const maxRetries = 3;

  while (retries <= maxRetries) {
    try {
      for await (const event of agenticLoop(messages, { model, timeout, maxTurns: 50, _readFileTimestamps: readFileTimestamps })) {
        if (event.type === 'text_delta') {
          accumulatedText += event.text;
          if (onStream) onStream(accumulatedText);
        } else if (event.type === 'done') {
          const textParts = (event.contentBlocks || [])
            .filter(b => b.type === 'text')
            .map(b => b.text);
          if (textParts.length > 0) {
            accumulatedText = textParts.join('');
          }
        } else if (event.type === 'max_turns') {
          accumulatedText += '\n\n[Max agentic turns reached]';
        }
      }
      return accumulatedText.trim();
    } catch (err) {
      if (err.status === 429 || err.status === 529 || err.status === 500 || err.status === 503) {
        retries++;
        if (retries > maxRetries) throw err;
        const delay = Math.min(20000, Math.random() * (2 ** retries) * 500);
        console.error(`[gergy] Retryable error (${err.status}), attempt ${retries}/${maxRetries}, waiting ${Math.round(delay)}ms`);
        await new Promise(r => setTimeout(r, delay));
        continue;
      }
      throw err;
    }
  }

  return accumulatedText.trim();
}

/**
 * Chat-mode completion — single API call, no tools, no agentic loop.
 * Used for conversational responses and rawCompletion (clean skill output).
 *
 * allowedTools is accepted for interface consistency with other adapters
 * but has no effect — gergy's streamChat is already tool-free (single API
 * call without the agentic loop). Tool use goes through ask() instead.
 *
 * @param {string} prompt
 * @param {{ model?: string, onStream?: function, allowedTools?: string[]|null }} opts
 * @returns {Promise<string>}
 */
export async function streamChat(prompt, { model: modelOpt = 'balanced', onStream, allowedTools = null } = {}) {
  const apiKey = getApiKey();
  if (!apiKey) throw new Error('API key not set');

  const provider = getProviderConfig();
  const model = resolveModel(modelOpt);
  const maxTokens = 16384;
  const maxContinuations = 5;

  console.log(`[gergy] streamChat — provider: ${provider.providerName}, model: ${model}, prompt: ${prompt.length} chars`);

  let messages = [{ role: 'user', content: prompt }];
  let fullText = '';

  for (let continuation = 0; continuation <= maxContinuations; continuation++) {
    const request = {
      model,
      max_tokens: maxTokens,
      messages
      // No tools, no system — just a direct completion
    };

    let chunkText = '';
    let stopReason = 'end_turn';
    const abortController = new AbortController();
    const timer = setTimeout(() => abortController.abort(), 300_000); // 5 min total

    try {
      for await (const event of streamMessages(request, {
        provider: provider.providerName,
        apiKey,
        baseUrl: provider.baseUrl,
        signal: abortController.signal,
        extraHeaders: getExtraHeaders()
      })) {
        if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
          chunkText += event.delta.text;
          fullText += event.delta.text;
          if (onStream) onStream(fullText);
        } else if (event.type === 'message_delta' && event.delta?.stop_reason) {
          stopReason = event.delta.stop_reason;
        } else if (event.type === 'error') {
          throw new Error(event.error?.message || 'Stream error');
        }
      }
    } catch (err) {
      if (err.name === 'AbortError') throw new Error('Chat timed out');
      throw err;
    } finally {
      clearTimeout(timer);
    }

    // If completed naturally, we're done
    if (stopReason !== 'max_tokens' && stopReason !== 'length') {
      console.log(`[gergy] streamChat complete — ${fullText.length} chars, stop: ${stopReason}`);
      break;
    }

    // Response was truncated — continue from where it left off
    console.log(`[gergy] streamChat continuation ${continuation + 1}/${maxContinuations} — ${fullText.length} chars so far, stop: ${stopReason}`);
    messages = [
      ...messages,
      { role: 'assistant', content: chunkText },
      { role: 'user', content: 'Continue exactly where you left off. Do not repeat any previous content.' }
    ];
  }

  return fullText.trim();
}

/**
 * Spawn a pseudo-interactive session for meta-develop compatibility.
 */
export { agenticLoop };

export function spawnInteractive({ cwd, model = 'powerful' } = {}) {
  // PassThrough imported at top of module
  const stdin = new PassThrough();
  const stdout = new PassThrough();
  const stderr = new PassThrough();

  let inputBuffer = '';
  stdin.on('data', (chunk) => { inputBuffer += chunk.toString(); });
  stdin.on('end', async () => {
    try {
      const result = await ask(inputBuffer, { model, sandboxed: false });
      stdout.write(JSON.stringify({ type: 'result', result }) + '\n');
      stdout.end();
    } catch (err) {
      stderr.write(err.message);
      stderr.end();
    }
  });

  return { stdin, stdout, stderr, kill: () => {} };
}
