// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.

/**
 * client-pi.js — pi-ai LLM client adapter for NERF.
 *
 * Wraps pi-mono's pi-ai unified API in the Anthropic-SDK-compatible
 * interface that NERF's gateway and auto-mode runtime expect.
 *
 * Package resolution: pi-mono publishes as @nerf/* on npm.
 * We use pi-imports.js to resolve packages transparently.
 *
 * This gives NERF access to 26+ LLM providers through a single adapter:
 * - Anthropic (Claude) — including OAuth for Pro/Max
 * - OpenAI (GPT-4o, o1, etc.)
 * - Google (Gemini)
 * - Ollama (local models)
 * - And 22+ more providers
 *
 * Usage:
 *   const { client, model } = await makePiClient({ provider: 'anthropic', model: 'claude-sonnet-4-6' });
 *   // client.messages.create() and client.messages.stream() work identically to Anthropic SDK
 */

import { importPiAi } from './pi-imports.js';

const debug = process.env.NERF_DEBUG === '1'
  ? (/** @type {string} */ msg) => process.stderr.write(`[pi-ai] ${msg}\n`)
  : () => {};

// ---------------------------------------------------------------------------
// pi-ai → Anthropic adapter
// ---------------------------------------------------------------------------

/**
 * Build an Anthropic-compatible client from a pi-ai model.
 *
 * @param {object} piModel - pi-ai model instance from getModel()
 * @param {string} modelName - model identifier string
 * @returns {object} Anthropic-SDK-compatible client with messages.create() and messages.stream()
 */
function buildPiAdapter(piModel, modelName) {
  // Build options from model-level config (apiKey, headers, etc.)
  // These get passed as the third arg to pi-ai stream()/complete()
  const piOptions = {};
  if (piModel.apiKey) piOptions.apiKey = piModel.apiKey;

  return {
    messages: {
      /**
       * Non-streaming completion with tool-use support.
       * Translates between Anthropic message format and pi-ai context format.
       */
      async create({ model, max_tokens, system, messages, tools }) {
        const { complete, Type: _Type } = await importPiAi();

        const context = {
          systemPrompt: system || '',
          messages: convertMessagesToPiAi(messages),
          tools: tools ? convertToolsToPiAi(tools) : undefined,
          maxTokens: max_tokens,
        };

        const result = await complete(piModel, context, piOptions);

        return convertPiAiToAnthropic(result, modelName);
      },

      /**
       * Streaming completion — returns object with .on('text', cb) pattern.
       * Matches the Anthropic Node.js SDK streaming interface.
       *
       * NOTE: Tool-use in streaming mode relies on the finalResult.response
       * containing the complete message including tool_use blocks. If pi-ai's
       * stream end event doesn't include tool calls, use create() instead.
       */
      stream({ model, max_tokens, system, messages, tools }) {
        const listeners = [];
        let finalMessageCb = null;
        let streamPromise = null;

        const streamObj = {
          on(event, cb) {
            if (event === 'finalMessage') {
              finalMessageCb = cb;
            } else {
              listeners.push({ event, cb });
            }
            return streamObj;
          },

          async finalMessage() {
            if (!streamPromise) {
              streamPromise = _consumeStream();
            }
            return streamPromise;
          },

          async *[Symbol.asyncIterator]() {
            const { stream: piStream } = await importPiAi();

            const context = {
              systemPrompt: system || '',
              messages: convertMessagesToPiAi(messages),
              tools: tools ? convertToolsToPiAi(tools) : undefined,
              maxTokens: max_tokens,
            };

            const s = piStream(piModel, context, piOptions);
            for await (const event of s) {
              if (event.type === 'text_delta') {
                yield event.delta;
              }
            }
          },
        };

        async function _consumeStream() {
          const { stream: piStream } = await importPiAi();

          const context = {
            systemPrompt: system || '',
            messages: convertMessagesToPiAi(messages),
            tools: tools ? convertToolsToPiAi(tools) : undefined,
            maxTokens: max_tokens,
          };

          const fullText = [];
          const s = piStream(piModel, context, piOptions);
          let finalResult = null;

          for await (const event of s) {
            if (event.type === 'text_delta') {
              fullText.push(event.delta);
              for (const { event: eName, cb } of listeners) {
                if (eName === 'text') cb(event.delta);
              }
            }
            if (event.type === 'end') {
              finalResult = event;
            }
          }

          const msg = finalResult
            ? convertPiAiToAnthropic(finalResult.response || { content: [{ type: 'text', text: fullText.join('') }] }, modelName)
            : { content: [{ text: fullText.join(''), type: 'text' }], model: modelName, role: 'assistant' };

          if (finalMessageCb) finalMessageCb(msg);
          return msg;
        }

        return streamObj;
      },
    },
  };
}

// ---------------------------------------------------------------------------
// Format converters
// ---------------------------------------------------------------------------

/**
 * Convert Anthropic-style messages to pi-ai context messages.
 */
function convertMessagesToPiAi(messages) {
  return messages.map((msg) => {
    if (typeof msg.content === 'string') {
      return { role: msg.role, content: msg.content };
    }
    // Handle Anthropic content block arrays
    if (Array.isArray(msg.content)) {
      // For assistant messages, preserve the full structure including tool_use
      if (msg.role === 'assistant') {
        const parts = [];
        for (const block of msg.content) {
          if (block.type === 'text' && block.text) {
            parts.push(block.text);
          } else if (block.type === 'tool_use') {
            parts.push(`[Tool call: ${block.name}(${JSON.stringify(block.input)})]`);
          }
        }
        return { role: msg.role, content: parts.join('\n') || '' };
      }

      // For user messages, extract text and tool results
      const textParts = msg.content
        .filter((b) => b.type === 'text')
        .map((b) => b.text);
      const toolResults = msg.content.filter((b) => b.type === 'tool_result');

      if (toolResults.length > 0) {
        const resultTexts = toolResults.map((tr) =>
          typeof tr.content === 'string' ? tr.content : JSON.stringify(tr.content)
        );
        return {
          role: msg.role,
          content: [...textParts, ...resultTexts].join('\n'),
        };
      }
      if (textParts.length > 0) {
        return { role: msg.role, content: textParts.join('\n') };
      }
    }
    return { role: msg.role, content: JSON.stringify(msg.content) };
  });
}

/**
 * Convert Anthropic tool schemas to pi-ai tool format.
 */
function convertToolsToPiAi(tools) {
  return tools.map((t) => ({
    name: t.name,
    description: t.description || '',
    parameters: t.input_schema || { type: 'object', properties: {} },
  }));
}

let _toolIdCounter = 0;

/**
 * Convert pi-ai response to Anthropic SDK response format.
 */
function convertPiAiToAnthropic(result, modelName) {
  const content = [];

  if (result.message) {
    // pi-ai complete() response
    const msg = result.message;
    if (msg.content) {
      if (typeof msg.content === 'string') {
        content.push({ type: 'text', text: msg.content });
      } else if (Array.isArray(msg.content)) {
        for (const part of msg.content) {
          if (part.type === 'text') {
            content.push({ type: 'text', text: part.text });
          } else if (part.type === 'tool_use' || part.type === 'tool_call') {
            content.push({
              type: 'tool_use',
              id: part.id || `toolu_pi_${++_toolIdCounter}`,
              name: part.name,
              input: part.input || part.arguments || {},
            });
          }
        }
      }
    }
  } else if (result.content) {
    // Direct content array
    for (const part of Array.isArray(result.content) ? result.content : [result.content]) {
      if (typeof part === 'string') {
        content.push({ type: 'text', text: part });
      } else {
        content.push(part);
      }
    }
  }

  if (content.length === 0) {
    content.push({ type: 'text', text: '' });
  }

  const hasToolUse = content.some((c) => c.type === 'tool_use');

  return {
    content,
    model: modelName,
    role: 'assistant',
    stop_reason: hasToolUse ? 'tool_use' : 'end_turn',
    usage: {
      input_tokens: result.usage?.inputTokens || result.usage?.input_tokens || 0,
      output_tokens: result.usage?.outputTokens || result.usage?.output_tokens || 0,
    },
  };
}

// ---------------------------------------------------------------------------
// Public factory
// ---------------------------------------------------------------------------

/**
 * Map pi-ai API identifiers to provider types.
 * These correspond to the streaming provider implementations in pi-ai.
 */
const PROVIDER_TO_API = {
  'anthropic':              'anthropic-messages',
  'openai-responses':       'openai-responses',
  'openai-completions':     'openai-completions',
  'openai-codex':           'openai-codex-responses',
  'google':                 'google',
  'google-vertex':          'google-vertex',
  'google-gemini-cli':      'google-gemini-cli',
  'mistral':                'mistral',
  'azure-openai-responses': 'azure-openai-responses',
};

/**
 * Create a NERF-compatible LLM client using pi-ai.
 *
 * For built-in providers (anthropic, openai, etc.), the model is looked up
 * from pi-ai's static registry. For custom providers (ollama, openrouter,
 * openai-compat), a model object is constructed directly with the provided
 * baseUrl and apiKey.
 *
 * @param {object} opts
 * @param {string} opts.provider - pi-ai provider or API name
 * @param {string} opts.model - Model identifier
 * @param {object} [opts.options] - Provider-specific options (apiKey, baseUrl, etc.)
 * @returns {Promise<{ client: object, model: string }>}
 */
export async function makePiClient(opts) {
  const piAi = await importPiAi();

  // Try the static model registry first (works for anthropic, openai, google, etc.)
  let piModel = piAi.getModel(opts.provider, opts.model);

  if (!piModel) {
    // Model not in static registry — build a model object directly.
    // This handles Ollama, OpenRouter, OpenAI-compat, and any custom endpoint.
    const api = PROVIDER_TO_API[opts.provider] || opts.provider;
    piModel = {
      id: opts.model,
      name: opts.model,
      api,
      provider: opts.provider,
      baseUrl: opts.options?.baseUrl || undefined,
      apiKey: opts.options?.apiKey || piAi.getEnvApiKey?.(opts.provider) || undefined,
      reasoning: false,
      input: ['text'],
      contextWindow: 131072,
      maxTokens: 8192,
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
    };
  } else if (opts.options) {
    // Model found in registry but caller wants to override baseUrl/apiKey
    if (opts.options.baseUrl) piModel = { ...piModel, baseUrl: opts.options.baseUrl };
    if (opts.options.apiKey) piModel = { ...piModel, apiKey: opts.options.apiKey };
  }

  // If model has no apiKey, check for OAuth token in AuthStorage (pi-coding-agent)
  if (!piModel.apiKey) {
    // Check environment first
    const envKey = piAi.getEnvApiKey?.(opts.provider);
    if (envKey) {
      piModel = { ...piModel, apiKey: envKey };
    } else {
      // Try OAuth auth storage
      try {
        const { importPiCodingAgent } = await import('./pi-imports.js');
        const { AuthStorage } = await importPiCodingAgent();
        const auth = AuthStorage.create();
        const oauthKey = await auth.getApiKey(opts.provider);
        if (oauthKey) {
          piModel = { ...piModel, apiKey: oauthKey };
          debug(`client: using OAuth token for ${opts.provider}`);
        }
      } catch {
        // pi-coding-agent not available, fall through
      }
    }
  }

  const adapter = buildPiAdapter(piModel, opts.model);

  debug(`client: pi-ai ${opts.provider}/${opts.model} (api: ${piModel.api})`);
  return { client: adapter, model: opts.model };
}

/**
 * List available providers and their models.
 *
 * @returns {Promise<object>}
 */
export async function listPiProviders() {
  try {
    const piAi = await importPiAi();
    const providers = piAi.getProviders();
    return {
      available: true,
      providers,
      note: 'Use makePiClient({ provider, model }) to create a client',
    };
  } catch {
    return { available: false, note: 'pi-ai not installed' };
  }
}
