// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// Licensed under the Apache License, Version 2.0

import { IAiEnvironment } from "./config/env.ts";
import { IAiTool } from "./tools/tool.ts";

export type AiSdkType = "ollama" | "openai";

export interface IAiProvider {
  _id: string;
  name: string;
  sdk: AiSdkType;
  baseUrl: string;
  apiKey: string;
}

export interface IAiModelConfig {
  provider: IAiProvider;
  modelId: string;
  params: {
    reasoning: boolean | "high" | "medium" | "low";
    temperature: number;
    topP: number;
    topK: number;
    /** Ollama: -1 = unlimited (generate until natural stop or context limit) */
    numPredict: number;
    /** Context window size (input + output tokens); Ollama passes as num_ctx */
    numCtx: number;
    /** OpenAI-compatible: maximum completion tokens the model can generate */
    maxCompletionTokens: number;
  };
}

export interface IAiInferenceStats {
  tokenCounts: {
    input: number;
    thinking: number;
    response: number;
  };
  duration: {
    seconds: number;
    text: string;
  };
}

export interface IAiGenerateOptions {
  prompt: string;
  systemPrompt?: string;
  signal?: AbortSignal;
}

export interface IAiGenerateResponse {
  response: string;
  thinking?: string;
  stats: IAiInferenceStats;
  done: boolean;
  doneReason?: string;
}

export interface IContextChatMessage {
  createdAt: Date;
  role: string;
  callId?: string;
  toolCallId?: string;
  toolName?: string;
  toolCalls?: Array<{
    id: string;
    type: "function";
    function: {
      name: string;
      arguments: string;
    };
  }>;
  content: string;
  user?: {
    _id: string;
    username: string;
    displayName: string;
  };
}

export interface IToolCall {
  callId: string;
  function: {
    name: string;
    arguments: string;
  };
}

export interface IToolCallResult {
  callId: string;
  functionName: string;
  result: string;
  error?: string;
}

export interface IAiChatOptions {
  systemPrompt?: string;
  userPrompt?: string;
  context?: IContextChatMessage[];
  tools?: IAiTool[];
  signal?: AbortSignal;
}

export interface IAiChatResponse {
  response: string;
  thinking?: string;
  stats: IAiInferenceStats;
  done: boolean;
  doneReason?: string;
  toolCalls?: IToolCall[];
  toolCallResults?: IToolCallResult[];
}

export interface IAiStreamChunk {
  type: "thinking" | "response" | "toolCall";
  data: string;
  toolCallId?: string;
  toolName?: string;
  params?: string;
}

export type IAiResponseStreamFn = (chunk: IAiStreamChunk) => Promise<void>;

export interface IAiLogger {
  debug(message: string, metadata?: unknown): Promise<void> | void;
  info(message: string, metadata?: unknown): Promise<void> | void;
  warn(message: string, metadata?: unknown): Promise<void> | void;
  error(message: string, metadata?: unknown): Promise<void> | void;
}

const noOpLogger: IAiLogger = {
  debug: () => {},
  info: () => {},
  warn: () => {},
  error: () => {},
};

export function defaultLogger(): IAiLogger {
  return noOpLogger;
}

export interface IAiModelListResult {
  models: Array<{
    id: string;
    name: string;
    parameterLabel?: string;
    parameterCount?: number;
    contextWindow?: number;
  }>;
}

export interface IAiModelProbeResult {
  capabilities: {
    canCallTools: boolean;
    hasVision: boolean;
    hasEmbedding: boolean;
    hasThinking: boolean;
    isInstructTuned: boolean;
  };
  settings?: {
    temperature?: number;
    topP?: number;
    topK?: number;
    numCtx?: number;
    /** Ollama: discovered num_predict from model parameters (informational; overridden to -1 at inference time) */
    numPredict?: number;
    /** OpenAI-compatible: discovered from model info */
    maxCompletionTokens?: number;
  };
}

export abstract class AiApi {
  protected env: IAiEnvironment;
  protected provider: IAiProvider;
  protected log: IAiLogger;

  constructor(env: IAiEnvironment, provider: IAiProvider, logger?: IAiLogger) {
    this.env = env;
    this.provider = provider;
    this.log = logger ?? defaultLogger();
  }

  abstract listModels(): Promise<IAiModelListResult>;
  abstract probeModel(modelId: string): Promise<IAiModelProbeResult>;

  abstract generate(
    model: IAiModelConfig,
    options: IAiGenerateOptions,
    streamCallback?: IAiResponseStreamFn,
  ): Promise<IAiGenerateResponse>;

  abstract chat(
    model: IAiModelConfig,
    options: IAiChatOptions,
    streamCallback?: IAiResponseStreamFn,
  ): Promise<IAiChatResponse>;

  /**
   * Forcefully abort any in-progress API request.
   * Provider-specific implementations (e.g., Ollama) should terminate
   * the underlying HTTP request immediately. Default is no-op.
   */
  abort(): void {
    // Override in provider-specific implementations
  }

  protected assertNonEmptyChatResponse(response: IAiChatResponse): void {
    const hasResponse = response.response.trim().length > 0;
    const hasThinking = !!response.thinking?.trim();
    const hasToolCalls = !!response.toolCalls?.length;
    const hasToolResults = !!response.toolCallResults?.length;

    if (!hasResponse && !hasThinking && !hasToolCalls && !hasToolResults) {
      throw new Error(
        "Provider returned an empty chat response: no text, thinking, tool calls, or tool results.",
      );
    }
  }

}
