/**
 * 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 log from 'electron-log';
import { nanoid } from 'nanoid';
import type { MetonaRequest, MetonaResponse, MetonaStreamEvent } from '../types';
import { MetonaFinishReason, MetonaStreamEventType } from '../types';
import type { MetonaModelInfo } from '../types/metona-adapter';

export class OllamaAdapter extends BaseAdapter {
  // H-2 修复: provider → providerId（规范要求）
  override readonly providerId: string = 'ollama';
  readonly supportedModels = ['qwen3:latest', 'gemma3:latest', 'deepseek-r1:latest'];
  readonly supportsToolCalling = true;
  readonly supportsThinking = true;

  // H-2 修复: Ollama 本地模型默认上下文窗口（可由 options.num_ctx 覆盖）
  private static readonly DEFAULT_CONTEXT_WINDOW = 4096;

  private baseURL: string;

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

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

  // H-2 修复: chat → send（规范要求）
  async send(request: MetonaRequest): Promise<MetonaResponse> {
    // #2 修复: toNativeRequest 改为 async（需下载 URL 图片转 base64）
    const nativeRequest = await this.toNativeRequest(request);

    // #24 修复: 使用 fetchWithTimeout 替代 getFetchSignal + fetch，确保 timer 清理
    const response = await this.fetchWithTimeout(`${this.baseURL}/api/chat`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ...nativeRequest, stream: false }),
    }, this.config.timeoutMs ?? 300_000);

    if (!response.ok) {
      await this.throwHttpError(response, 'Ollama API error');
    }

    const data = await response.json() as Record<string, unknown>;
    return this.toMetonaResponse(data, request.meta.requestId, request.meta.iteration);
  }

  // H-2 修复: chatStream → sendStream（规范要求）
  async *sendStream(request: MetonaRequest): AsyncIterable<MetonaStreamEvent> {
    // #2 修复: toNativeRequest 改为 async（需下载 URL 图片转 base64）
    const nativeRequest = await this.toNativeRequest(request);

    // #24 修复: 使用 fetchWithTimeout 替代 getFetchSignal + fetch，确保 timer 清理
    const response = await this.fetchWithTimeout(`${this.baseURL}/api/chat`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ...nativeRequest, stream: true }),
    }, this.config.timeoutMs ?? 300_000);

    if (!response.ok || !response.body) {
      await this.throwHttpError(response, 'Ollama stream error');
    }

    // 非空断言：上方 if 已确保 response.body 不为 null
    const reader = response.body!.getReader();
    const decoder = new TextDecoder();
    let seq = 0;
    let buffer = '';
    let streamEndedNormally = false;

    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) {
              const args = tc.function?.arguments;
              const parsedArgs = typeof args === 'string' ? JSON.parse(args) : (args ?? {});
              yield {
                type: MetonaStreamEventType.TOOL_CALL_COMPLETE,
                requestId: request.meta.requestId,
                sessionId: request.meta.sessionId,
                iteration: request.meta.iteration,
                seq: seq++,
                timestamp: Date.now(),
                toolCall: {
                  // L-9 修复: 统一使用 nanoid 生成工具调用 ID（与 sse-stream.ts 一致）
                  id: `tc_${nanoid(8)}`,
                  name: tc.function?.name ?? '',
                  args: parsedArgs,
                  iteration: request.meta.iteration,
                  timestamp: Date.now(),
                },
              };
            }
          }

          // 流结束
          if (chunk.done) {
            streamEndedNormally = true;
            // 发送 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 (parseErr) {
          // P2-8 修复: 与 sse-stream.ts 一致，记录解析失败行便于诊断
          log.warn(`[Ollama] Failed to parse NDJSON line: ${(parseErr as Error).message}`, trimmed.slice(0, 200));
        }
      }
    }

    // 流未正常结束（连接断开等），补发 DONE 事件防止 Agent Loop 挂起
    if (!streamEndedNormally) {
      yield {
        type: MetonaStreamEventType.DONE,
        requestId: request.meta.requestId,
        sessionId: request.meta.sessionId,
        iteration: request.meta.iteration,
        seq: seq++,
        timestamp: Date.now(),
      };
    }
  }

  // ===== 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 =====

  /**
   * H-2 修复: 返回 MetonaModelInfo[]（规范要求）
   *
   * Ollama /api/tags 返回模型列表含详细信息（name, size, details），
   * 转换为 MetonaModelInfo 并补充默认元数据。
   */
  async listModels(): Promise<MetonaModelInfo[]> {
    try {
      const response = await fetch(`${this.baseURL}/api/tags`, {
        signal: AbortSignal.timeout(10_000),
      });
      if (response.ok) {
        const data = await response.json() as {
          models?: Array<{
            name: string;
            size?: number;
            details?: { parameter_size?: string; quantization_level?: string; family?: string };
          }>;
        };
        if (data.models?.length) {
          return data.models.map((m) => ({
            id: m.name,
            name: m.name,
            // Ollama 模型上下文窗口由 options.num_ctx 决定，此处给保守值
            contextWindow: OllamaAdapter.DEFAULT_CONTEXT_WINDOW,
            supportsToolCalling: true,  // Ollama 多数模型支持，具体能力需通过 /api/show 查询
            supportsThinking: true,
            description: m.details
              ? `${m.details.family ?? 'unknown'} / ${m.details.parameter_size ?? '?'} / ${m.details.quantization_level ?? '?'}`
              : undefined,
          }));
        }
      }
    } catch {
      // API 不可用时降级
    }
    // 回退到 supportedModels
    return this.supportedModels.map((id) => ({ id }));
  }

  /**
   * H-2 修复: 获取上下文窗口大小（规范要求）
   *
   * Ollama 上下文窗口由 options.num_ctx 决定（默认 4096），
   * Engine 应通过 MetonaRequest.params.contextLength 显式设置。
   * 此处返回默认值，供 Engine 在未指定时参考。
   */
  override getContextWindow(): number {
    return OllamaAdapter.DEFAULT_CONTEXT_WINDOW;
  }

  // ===== 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 }),
      signal: AbortSignal.timeout(600_000), // 模型下载可能较慢，10 分钟超时
    });

    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 {
          // L-3 修复: 添加日志便于诊断非标准行（如进度通知、空行等）
          log.debug('[Ollama] skipped non-JSON line during pull:', line.slice(0, 100));
        }
      }
    }
  }

  // ===== 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';
    }
  }

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

  /**
   * #2 修复: 下载 http(s) URL 图片并转为纯 base64 字符串（不含 data: 前缀）
   *
   * Ollama API 的 images 字段要求纯 base64 字符串数组。
   * 当 MetonaMessage.images 中存储的是 URL 时，需先下载转为 base64。
   * 下载失败时返回空字符串（Ollama 会忽略空图片），不阻断整个请求。
   */
  private async resolveImageToBase64(url: string): Promise<string> {
    try {
      // 审查修复: 使用基类 fetchWithTimeout 合并 externalAbortSignal 和 30s 超时，
      // 避免用户中断时图片下载最多阻塞 30s×N（externalAbortSignal 是 BaseAdapter 的
      // private 属性，子类无法直接访问，故复用已合并 signal 的 fetchWithTimeout，
      // 该方法同时处理了 listener 泄漏问题）
      const res = await this.fetchWithTimeout(url, {}, 30_000);
      if (!res.ok) {
        throw new Error(`HTTP ${res.status}`);
      }
      const buf = Buffer.from(await res.arrayBuffer());
      return buf.toString('base64');
    } catch (error) {
      log.warn(`[Ollama] Failed to download image ${url.slice(0, 100)}: ${(error as Error).message}`);
      return '';
    }
  }

  private async toNativeRequest(request: MetonaRequest): Promise<Record<string, unknown>> {
    const messages: Record<string, unknown>[] = [
      {
        role: 'system',
        content: [
          request.systemPrompt.roleDefinition,
          request.systemPrompt.outputConstraints,
          request.systemPrompt.safetyGuidelines,
          request.systemPrompt.dynamicReminders,
        ].filter(Boolean).join('\n\n'),
      },
    ];

    // #2 修复: 改为 for 循环以支持 async 图片下载（map 回调无法 await）
    for (const m of request.messages) {
      if (m.role === 'system') continue;
        // C-6 修复: Ollama API 不支持 null content，assistant 仅有 tool_calls 时转为空字符串
        const msg: Record<string, unknown> = { role: m.role, content: m.content ?? '' };
        // Ollama 图片使用 images 字段（纯 base64 数组，不含 data: 前缀）
        if (m.images?.length) {
          // #2 修复: 支持公网 URL 图片，下载后转为纯 base64
          // 之前直接将 URL 字符串传给 Ollama，导致 base64 解码错误
          const resolvedImages: string[] = [];
          for (const img of m.images) {
            const url = img.url;
            if (url.startsWith('data:')) {
              // data:image/png;base64,iVBOR... → iVBOR...
              const base64Part = url.split(',')[1];
              resolvedImages.push(base64Part ?? url);
            } else if (url.startsWith('http://') || url.startsWith('https://')) {
              // #2 修复: 公网 URL → 下载 → 纯 base64
              const base64 = await this.resolveImageToBase64(url);
              if (base64) resolvedImages.push(base64);
            } else {
              // 已是纯 base64 字符串（无 data: 前缀）
              resolvedImages.push(url);
            }
          }
          msg.images = resolvedImages;
        }
        // 工具结果
        if (m.role === 'tool' && m.toolResult) {
          msg.tool_call_id = m.toolResult.toolCallId;
          // CE-2 修复: 工具失败时 result 为 null，优先用 error 字段作为 content
          msg.content = m.toolResult.error
            ? m.toolResult.error
            : (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;
        }
        messages.push(msg);
    }

    const body: Record<string, unknown> = {
      model: this.config.defaultModel,
      messages,
      options: {
        temperature: request.params.temperature,
        num_predict: request.params.maxTokens,
        ...(request.params.topP != null && { top_p: request.params.topP }),
        ...(request.params.stopSequences?.length && { stop: request.params.stopSequences }),
        ...(request.params.contextLength != null && { 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, iteration: number = 0): 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.providerId,
        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>;
        const rawArgs = fn?.arguments;
        let args: Record<string, unknown> = {};
        try {
          args = typeof rawArgs === 'string' ? JSON.parse(rawArgs) : (rawArgs as Record<string, unknown>) ?? {};
        } catch {
          args = {};
        }
        return {
          // L-9 修复（审计补充）: 非流式路径统一使用 nanoid，与流式路径（sendStream）保持一致
          id: `tc_${nanoid(8)}`,
          name: (fn?.name as string) ?? '',
          args,
          iteration,
          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;
  }
}
