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

import env from "../config/env.ts";
const aiEnv: IAiEnvironment = {
  NODE_ENV: env.NODE_ENV,
  services: {
    google: {
      cse: {
        apiKey: env.google.cse.apiKey,
        engineId: env.google.cse.engineId,
      },
    },
  },
};

import { IAiProvider as DbAiProvider, GadgetId, type IDroneModelConfig } from "@gadget/api";
import { GadgetService } from "../lib/service.js";
import {
  AiApi,
  type IAiChatOptions,
  type IAiChatResponse,
  type IAiGenerateOptions,
  type IAiGenerateResponse,
  type IAiModelConfig,
  type IAiProvider as AiProviderConfig,
  type IAiResponseStreamFn,
  createAiApi,
  IAiEnvironment,
} from "@gadget/ai";

/**
 * An abstraction of the backend AI APIs (Ollama, OpenAI) that provides one
 * common interface and contract for working with different AI APIs.
 *
 * We are Ollama-first and prefer the Ollama API because it offers more actual
 * control over the AI models, their performance, and their results at runtime.
 *
 * OpenAI, however, is the most ubiquitous API, and expands the range of service
 * providers developers can use with the Gadget ecosystem.
 */
class AiService extends GadgetService {
  get name(): string {
    return "AiService";
  }
  get slug(): string {
    return "svc:ai";
  }

  private activeApi: AiApi | null = null;

  async start(): Promise<void> {
    this.log.info("started");
  }

  async stop(): Promise<void> {
    this.log.info("stopped");
  }

  /**
   * Converts a database IAiProvider document to a runtime IAiProvider config.
   * The DB model uses `apiType` and extends Mongoose Document, while the runtime
   * config uses `sdk` and is a plain object.
   */
  mapDbProviderToConfig(provider: DbAiProvider | GadgetId): AiProviderConfig {
    if (typeof provider === "string") {
      throw new Error("Provider must be populated, not a GadgetId reference");
    }
    return {
      _id: provider._id,
      name: provider.name,
      sdk: provider.apiType, // map apiType → sdk
      baseUrl: provider.baseUrl,
      apiKey: provider.apiKey,
    };
  }

  /**
   * Query the list of models available from the provider, then queries the
   * models for their individual capabilities. The results are cached in the Gadget
   */
  async discovery(provider: DbAiProvider | GadgetId): Promise<void> {
    const config = this.mapDbProviderToConfig(provider);
    this.log.info("discovering provider model list", {
      name: config.name,
      sdk: config.sdk,
    });
    const api = this.getApi(config);
    const response = await api.listModels();
    this.log.debug("listModels response", { response });
  }

  async generate(
    provider: DbAiProvider | GadgetId,
    model: Omit<IAiModelConfig, "provider">,
    options: IAiGenerateOptions,
    streamCallback?: IAiResponseStreamFn,
  ): Promise<IAiGenerateResponse> {
    const config = this.mapDbProviderToConfig(provider);
    this.log.info("calling provider to generate a response", {
      name: config.name,
      sdk: config.sdk,
      haveStreamCallback: !!streamCallback,
      options,
    });
    const api = this.getApi(config);
    const modelConfig: IAiModelConfig = { ...model, provider: config };
    try {
      return await api.generate(modelConfig, options, streamCallback);
    } finally {
      this.activeApi = null;
    }
  }

  async chat(
    provider: DbAiProvider | GadgetId,
    model: Omit<IAiModelConfig, "provider">,
    options: IAiChatOptions,
    streamCallback?: IAiResponseStreamFn,
  ): Promise<IAiChatResponse> {
    const config = this.mapDbProviderToConfig(provider);
    this.log.info("calling provider to process chat", {
      provider: config.name,
      sdk: config.sdk,
      model,
      haveStreamCallback: !!streamCallback,
    });
    const api = this.getApi(config);
    const modelConfig: IAiModelConfig = { ...model, provider: config };
    try {
      return await api.chat(modelConfig, options, streamCallback);
    } finally {
      this.activeApi = null;
    }
  }

  getApi(provider: AiProviderConfig) {
    const api = createAiApi(aiEnv, provider, this.log);
    this.activeApi = api;
    return api;
  }

  /** Forcefully abort any in-progress request on the active API instance. */
  abort(): void {
    this.activeApi?.abort();
    this.activeApi = null;
  }
}

export default new AiService();
