import http from 'node:http';
import type { LlmProvider, CompletionOptions, CompletionResult, ToolCall } from './types.js';

export interface OllamaConfig {
  baseUrl?: string;
  defaultModel?: string;
}

/**
 * Ollama provider for local model inference.
 * Uses the Ollama HTTP API at /api/chat.
 */
export class OllamaProvider implements LlmProvider {
  readonly name = 'ollama';
  private baseUrl: string;
  private defaultModel: string;

  constructor(config?: OllamaConfig) {
    this.baseUrl = (config?.baseUrl ?? 'http://localhost:11434').replace(/\/$/, '');
    this.defaultModel = config?.defaultModel ?? 'llama3.2';
  }

  async complete(options: CompletionOptions): Promise<CompletionResult> {
    const model = options.model ?? this.defaultModel;
    const body: Record<string, unknown> = {
      model,
      messages: options.messages.map((m) => ({
        role: m.role === 'tool' ? 'assistant' : m.role,
        content: m.content,
      })),
      stream: false,
    };
    if (options.temperature !== undefined) {
      body.options = { temperature: options.temperature };
    }

    if (options.tools && options.tools.length > 0) {
      body.tools = options.tools.map((t) => ({
        type: 'function',
        function: {
          name: t.name,
          description: t.description,
          parameters: t.inputSchema,
        },
      }));
    }

    const response = await this.request('/api/chat', body);
    return parseOllamaResponse(response);
  }

  async listModels(): Promise<string[]> {
    const response = await this.request('/api/tags', undefined, 'GET') as {
      models?: Array<{ name: string }>;
    };
    return (response.models ?? []).map((m) => m.name);
  }

  async isAvailable(): Promise<boolean> {
    try {
      await this.listModels();
      return true;
    } catch {
      return false;
    }
  }

  private request(path: string, body?: unknown, method = 'POST'): Promise<unknown> {
    return new Promise((resolve, reject) => {
      const url = new URL(path, this.baseUrl);
      const payload = body !== undefined ? JSON.stringify(body) : undefined;
      const opts: http.RequestOptions = {
        hostname: url.hostname,
        port: url.port || 11434,
        path: url.pathname,
        method,
        timeout: 300000, // Ollama can be slow on first inference
        headers: {
          'Content-Type': 'application/json',
          ...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
        },
      };

      const req = http.request(opts, (res) => {
        const chunks: Buffer[] = [];
        res.on('data', (chunk: Buffer) => chunks.push(chunk));
        res.on('end', () => {
          const raw = Buffer.concat(chunks).toString('utf-8');
          try {
            resolve(JSON.parse(raw));
          } catch {
            reject(new Error(`Invalid JSON from Ollama: ${raw.slice(0, 200)}`));
          }
        });
      });
      req.on('error', reject);
      req.on('timeout', () => {
        req.destroy();
        reject(new Error('Ollama request timed out'));
      });
      if (payload) req.write(payload);
      req.end();
    });
  }
}

function parseOllamaResponse(raw: unknown): CompletionResult {
  const data = raw as {
    message?: {
      content?: string;
      tool_calls?: Array<{
        function: { name: string; arguments: Record<string, unknown> };
      }>;
    };
    prompt_eval_count?: number;
    eval_count?: number;
  };

  const toolCalls: ToolCall[] = (data.message?.tool_calls ?? []).map((tc, i) => ({
    id: `ollama-${i}`,
    name: tc.function.name,
    arguments: tc.function.arguments,
  }));

  const promptTokens = data.prompt_eval_count ?? 0;
  const completionTokens = data.eval_count ?? 0;

  return {
    content: data.message?.content ?? '',
    toolCalls,
    usage: {
      promptTokens,
      completionTokens,
      totalTokens: promptTokens + completionTokens,
    },
    finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
  };
}
