/**
 * Ollama Provider Adapter
 *
 * 本地推理引擎，支持 Tool Calling、Thinking 模式、NDJSON 流式。
 * 无需 API Key，连接本地 http://localhost:11434。
 *
 * 完整实现 Ollama API 文档中所有端点和参数：
 * - POST /api/chat (对话)
 * - POST /api/generate (补全)
 * - POST /api/embed (嵌入)
 * - GET /api/tags (列出模型)
 * - POST /api/show (模型详情)
 * - POST /api/pull (下载模型)
 * - GET /api/ps (运行中模型)
 * - GET /api/version (版本)
 * - think 参数（Thinking 模式）
 * - options 参数（temperature/top_k/top_p/stop/num_ctx/num_predict）
 * - format 参数（structured output）
 * - images 参数（多模态）
 * - tool_calls 流式处理
 *
 * @see apis/ollama-api-docs-20260518.html
 */

import { BaseAdapter } from './base-adapter';
import type { MetonaRequest, MetonaResponse, MetonaStreamEvent } from '../types';
import { MetonaFinishReason, MetonaStreamEventType } from '../types';

export class OllamaAdapter extends BaseAdapter {
  override readonly provider: string = 'ollama';
  readonly supportedModels = ['qwen3:latest', 'gemma3:latest', 'deepseek-r1:latest'];
  readonly supportsToolCalling = true;
  readonly supportsThinking = true;

  private baseURL: string;

  constructor(config: ConstructorParameters<typeof BaseAdapter>[0]) {
    super(config);
    this.baseURL = config.baseURL || 'http://localhost:11434';
  }

  // ===== POST /api/chat =====

  async chat(request: MetonaRequest): Promise<MetonaResponse> {
    const nativeRequest = this.toNativeRequest(request);

    const response = await fetch(`${this.baseURL}/api/chat`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ...nativeRequest, stream: false }),
      signal: AbortSignal.timeout(this.config.timeoutMs ?? 300_000),
    });

    if (!response.ok) {
      throw new Error(`Ollama API error: ${response.status}`);
    }

    const data = await response.json();
    return this.toMetonaResponse(data, request.meta.requestId);
  }

  async *chatStream(request: MetonaRequest): AsyncIterable<MetonaStreamEvent> {
    const nativeRequest = this.toNativeRequest(request);

    const response = await fetch(`${this.baseURL}/api/chat`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ...nativeRequest, stream: true }),
    });

    if (!response.ok || !response.body) {
      throw new Error(`Ollama stream error: ${response.status}`);
    }

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let seq = 0;
    let buffer = '';

    // 工具调用缓冲区
    const toolCallsBuffer = new Map<number, { name: string; argsBuffer: string }>();

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split('\n');
      buffer = lines.pop() ?? '';

      for (const line of lines) {
        const trimmed = line.trim();
        if (!trimmed) continue;

        try {
          const chunk = JSON.parse(trimmed);

          // 思考内容
          if (chunk.message?.thinking) {
            yield {
              type: MetonaStreamEventType.REASONING_DELTA,
              requestId: request.meta.requestId,
              sessionId: request.meta.sessionId,
              iteration: request.meta.iteration,
              seq: seq++,
              timestamp: Date.now(),
              delta: chunk.message.thinking,
            };
          }

          // 文本内容
          if (chunk.message?.content) {
            yield {
              type: MetonaStreamEventType.TEXT_DELTA,
              requestId: request.meta.requestId,
              sessionId: request.meta.sessionId,
              iteration: request.meta.iteration,
              seq: seq++,
              timestamp: Date.now(),
              delta: chunk.message.content,
            };
          }

          // 工具调用（Ollama 在最后一个 chunk 中整块返回）
          if (chunk.message?.tool_calls) {
            for (const tc of chunk.message.tool_calls) {
              yield {
                type: MetonaStreamEventType.TOOL_CALL_COMPLETE,
                requestId: request.meta.requestId,
                sessionId: request.meta.sessionId,
                iteration: request.meta.iteration,
                seq: seq++,
                timestamp: Date.now(),
                toolCall: {
                  id: `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
                  name: tc.function?.name ?? '',
                  args: tc.function?.arguments ?? {},
                  iteration: request.meta.iteration,
                  timestamp: Date.now(),
                },
              };
            }
          }

          // 流结束
          if (chunk.done) {
            // 发送 usage 信息
            yield {
              type: MetonaStreamEventType.USAGE,
              requestId: request.meta.requestId,
              sessionId: request.meta.sessionId,
              iteration: request.meta.iteration,
              seq: seq++,
              timestamp: Date.now(),
              usage: {
                inputTokens: chunk.prompt_eval_count ?? 0,
                outputTokens: chunk.eval_count ?? 0,
                totalTokens: (chunk.prompt_eval_count ?? 0) + (chunk.eval_count ?? 0),
              },
            };

            yield {
              type: MetonaStreamEventType.DONE,
              requestId: request.meta.requestId,
              sessionId: request.meta.sessionId,
              iteration: request.meta.iteration,
              seq: seq++,
              timestamp: Date.now(),
            };
            return;
          }
        } catch {
          // 跳过解析失败的行
        }
      }
    }
  }

  // ===== POST /api/generate =====

  async generate(params: {
    model: string;
    prompt: string;
    suffix?: string;
    system?: string;
    stream?: boolean;
    think?: boolean | string;
    format?: string | object;
    images?: string[];
    options?: Record<string, unknown>;
  }): Promise<{ response: string; thinking?: string; done: boolean; totalDuration: number; evalCount: number }> {
    const response = await fetch(`${this.baseURL}/api/generate`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ...params, stream: false }),
      signal: AbortSignal.timeout(300_000),
    });

    if (!response.ok) throw new Error(`Ollama generate error: ${response.status}`);
    const data = await response.json() as {
      response?: string; thinking?: string; done?: boolean;
      total_duration?: number; eval_count?: number;
    };

    return {
      response: data.response ?? '',
      thinking: data.thinking,
      done: data.done ?? true,
      totalDuration: data.total_duration ?? 0,
      evalCount: data.eval_count ?? 0,
    };
  }

  // ===== POST /api/embed =====

  async embed(params: {
    model: string;
    input: string | string[];
    dimensions?: number;
  }): Promise<{ embeddings: number[][]; totalDuration: number }> {
    const response = await fetch(`${this.baseURL}/api/embed`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(params),
      signal: AbortSignal.timeout(60_000),
    });

    if (!response.ok) throw new Error(`Ollama embed error: ${response.status}`);
    const data = await response.json() as { embeddings?: number[][]; total_duration?: number };

    return {
      embeddings: data.embeddings ?? [],
      totalDuration: data.total_duration ?? 0,
    };
  }

  // ===== GET /api/tags =====

  async listModels(): Promise<string[]> {
    try {
      const response = await fetch(`${this.baseURL}/api/tags`, {
        signal: AbortSignal.timeout(10_000),
      });
      if (!response.ok) return this.supportedModels;
      const data = await response.json() as { models?: Array<{ name: string }> };
      return data.models?.map((m) => m.name) ?? this.supportedModels;
    } catch {
      return this.supportedModels;
    }
  }

  // ===== POST /api/show =====

  async showModel(model: string): Promise<{ parameters: string; template: string; capabilities: string[] } | null> {
    try {
      const response = await fetch(`${this.baseURL}/api/show`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ model }),
        signal: AbortSignal.timeout(10_000),
      });
    if (!response.ok) return null;
    const data = await response.json() as { parameters?: string; template?: string; capabilities?: string[] };
      return {
        parameters: data.parameters ?? '',
        template: data.template ?? '',
        capabilities: data.capabilities ?? [],
      };
    } catch {
      return null;
    }
  }

  // ===== POST /api/pull =====

  async pullModel(model: string, onProgress?: (progress: { status: string; completed?: number; total?: number }) => void): Promise<void> {
    const response = await fetch(`${this.baseURL}/api/pull`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ model, stream: true }),
    });

    if (!response.ok || !response.body) throw new Error(`Ollama pull error: ${response.status}`);

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split('\n');
      buffer = lines.pop() ?? '';
      for (const line of lines) {
        if (!line.trim()) continue;
        try {
          const chunk = JSON.parse(line);
          onProgress?.({ status: chunk.status, completed: chunk.completed, total: chunk.total });
        } catch {}
      }
    }
  }

  // ===== GET /api/ps =====

  async listRunning(): Promise<Array<{ name: string; size: number; sizeVram: number; contextLength: number }>> {
    try {
      const response = await fetch(`${this.baseURL}/api/ps`, {
        signal: AbortSignal.timeout(10_000),
      });
      if (!response.ok) return [];
      const data = await response.json() as { models?: Array<{ name: string; size?: number; size_vram?: number; context_length?: number }> };
      return (data.models ?? []).map((m) => ({
        name: m.name ?? '',
        size: m.size ?? 0,
        sizeVram: m.size_vram ?? 0,
        contextLength: m.context_length ?? 0,
      }));
    } catch {
      return [];
    }
  }

  // ===== GET /api/version =====

  async getVersion(): Promise<string> {
    try {
      const response = await fetch(`${this.baseURL}/api/version`, {
        signal: AbortSignal.timeout(5_000),
      });
      if (!response.ok) return 'unknown';
      const data = await response.json() as { version?: string };
      return data.version ?? 'unknown';
    } catch {
      return 'unknown';
    }
  }

  // ========== 私有转换方法 ==========

  private toNativeRequest(request: MetonaRequest): Record<string, unknown> {
    const messages = [
      {
        role: 'system',
        content: [
          request.systemPrompt.roleDefinition,
          request.systemPrompt.outputConstraints,
          request.systemPrompt.safetyGuidelines,
          request.systemPrompt.dynamicReminders,
        ].filter(Boolean).join('\n\n'),
      },
      ...request.messages.filter((m) => m.role !== 'system').map((m) => {
        const msg: Record<string, unknown> = { role: m.role, content: m.content };
        // Ollama 图片使用 images 字段（纯 base64 数组，不含 data: 前缀）
        if (m.images?.length) {
          msg.images = m.images.map((img) => {
            const url = img.url;
            // data:image/png;base64,iVBOR... → iVBOR...
            if (url.startsWith('data:')) {
              const base64Part = url.split(',')[1];
              return base64Part ?? url;
            }
            return url;
          });
        }
        // 工具结果
        if (m.role === 'tool' && m.toolResult) {
          msg.tool_call_id = m.toolResult.toolCallId;
          msg.content = typeof m.toolResult.result === 'string'
            ? m.toolResult.result
            : JSON.stringify(m.toolResult.result);
        }
        // assistant 工具调用（Ollama REST API 要求 arguments 为 JSON 字符串）
        if (m.role === 'assistant' && m.toolCalls?.length) {
          msg.tool_calls = m.toolCalls.map((tc) => ({
            function: { name: tc.name, arguments: JSON.stringify(tc.args) },
          }));
        }
        // 推理内容回传（保持多轮推理链完整）
        if (m.role === 'assistant' && m.reasoningContent) {
          (msg as Record<string, unknown>).reasoning_content = m.reasoningContent;
        }
        return msg;
      }),
    ];

    const body: Record<string, unknown> = {
      model: this.config.defaultModel,
      messages,
      options: {
        temperature: request.params.temperature,
        num_predict: request.params.maxTokens,
        top_p: request.params.topP,
        stop: request.params.stopSequences,
        num_ctx: request.params.contextLength,
      },
    };

    // Tool Calling
    if (request.tools?.length) {
      body.tools = request.tools.map((t) => ({
        type: 'function',
        function: {
          name: t.name,
          description: t.description,
          parameters: t.parameters,
        },
      }));
    }

    // Thinking 模式
    if (request.params.thinkingEnabled) {
      const effortMap: Record<string, string | boolean> = { low: 'low', medium: 'medium', high: 'high', max: true };
      body.think = effortMap[request.params.thinkingEffort ?? 'high'] ?? true;
    }

    return body;
  }

  private toMetonaResponse(data: Record<string, unknown>, requestId: string): MetonaResponse {
    const message = data.message as Record<string, unknown> | undefined;
    const toolCalls = message?.tool_calls as Array<Record<string, unknown>> | undefined;
    return {
      meta: {
        requestId,
        provider: this.provider,
        model: (data.model as string) ?? this.config.defaultModel,
        latencyMs: 0,
        timestamp: Date.now(),
        perfStats: {
          loadDurationMs: data.load_duration ? (data.load_duration as number) / 1e6 : undefined,
          promptEvalDurationMs: data.prompt_eval_duration ? (data.prompt_eval_duration as number) / 1e6 : undefined,
          evalDurationMs: data.eval_duration ? (data.eval_duration as number) / 1e6 : undefined,
          tokensPerSecond: data.eval_count && data.eval_duration
            ? ((data.eval_count as number) / ((data.eval_duration as number) / 1e9))
            : undefined,
        },
      },
      content: (message?.content as string) ?? '',
      reasoningContent: message?.thinking as string | undefined,
      toolCalls: toolCalls?.map((tc, i) => {
        const fn = tc.function as Record<string, unknown>;
        return {
          id: `tc_${Date.now()}_${i}`,
          name: (fn?.name as string) ?? '',
          args: (fn?.arguments as Record<string, unknown>) ?? {},
          iteration: 0,
          timestamp: Date.now(),
        };
      }),
      usage: {
        inputTokens: (data.prompt_eval_count as number) ?? 0,
        outputTokens: (data.eval_count as number) ?? 0,
        totalTokens: ((data.prompt_eval_count as number) ?? 0) + ((data.eval_count as number) ?? 0),
      },
      finishReason: mapOllamaDoneReason(data.done_reason as string | undefined, !!message?.tool_calls),
    };
  }
}

/**
 * 映射 Ollama done_reason → MetonaFinishReason
 *
 * @see apis/ollama-api-docs-20260518.html — /api/chat 响应字段
 */
function mapOllamaDoneReason(
  reason: string | undefined,
  hasToolCalls: boolean,
): MetonaFinishReason {
  if (hasToolCalls) return MetonaFinishReason.TOOL_CALLS;
  switch (reason) {
    case 'stop': return MetonaFinishReason.STOP;
    case 'length': return MetonaFinishReason.LENGTH;
    case 'load': return MetonaFinishReason.STOP;   // 冷启动加载完成，非错误
    case 'unload': return MetonaFinishReason.STOP;
    default: return MetonaFinishReason.STOP;
  }
}
