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

import env from "../config/env.ts";
import assert from "node:assert";

import { Socket } from "socket.io-client";
import {
  GoogleSearchTool,
  IAiChatOptions,
  IAiEnvironment,
  IAiStreamChunk,
  type IContextChatMessage,
} from "@gadget/ai";
import {
  IChatSession,
  IChatTurn,
  IUser,
  ServerToClientEvents,
  ClientToServerEvents,
  ChatSessionMode,
} from "@gadget/api";

import AiService from "./ai.ts";

import { GadgetService } from "../lib/service.ts";
import { AiToolbox } from "@gadget/ai";

export interface IAgentWorkOrder {
  createdAt: Date;
  turn: IChatTurn;
  context: IChatTurn[];
}

interface IAgentWorkflow {
  chatOptions: IAiChatOptions;
  context: IContextChatMessage[];
}

type DroneSocket = Socket<ServerToClientEvents, ClientToServerEvents>;

const aiEnv: IAiEnvironment = {
  NODE_ENV: env.NODE_ENV || "develop",
  services: {
    google: {
      cse: {
        apiKey: env.google.cse.apiKey,
        engineId: env.google.cse.engineId,
      },
    },
  },
};

class AgentService extends GadgetService {
  private toolbox = new AiToolbox(aiEnv);

  get name(): string {
    return "AgentService";
  }
  get slug(): string {
    return "svc:agent";
  }

  async start(): Promise<void> {
    const googleSearchTool = new GoogleSearchTool(this.toolbox);
    this.toolbox.register(googleSearchTool, [
      ChatSessionMode.Plan,
      ChatSessionMode.Build,
      ChatSessionMode.Test,
      ChatSessionMode.Ship,
      ChatSessionMode.Develop,
    ]);

    this.log.info("started");
  }

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

  async process(
    workOrder: IAgentWorkOrder,
    socket: DroneSocket,
  ): Promise<void> {
    const { turn } = workOrder;
    const task: IAgentWorkflow = {
      chatOptions: {},
      context: [],
    };

    const onStreamChunk = async (chunk: IAiStreamChunk): Promise<void> => {
      this.log.debug("stream chunk received", { chunk });

      switch (chunk.type) {
        case "thinking":
          socket.emit("thinking", chunk.data);
          break;
        case "response":
          socket.emit("response", chunk.data);
          break;
        case "toolCall":
          socket.emit(
            "toolCall",
            chunk.toolCallId!,
            chunk.toolName!,
            chunk.params || "{}",
            chunk.data,
          );
          break;
      }
    };

    try {
      task.context = this.buildSessionContext(workOrder);
      task.chatOptions = {
        systemPrompt: turn.prompts.system,
        context: task.context,
        userPrompt: turn.prompts.user,
        tools: this.getToolsForMode(turn.mode),
      };
    } catch (cause) {
      socket.emit(
        "workOrderComplete",
        turn._id,
        false,
        `failed to build session context: ${(cause as Error).message}`,
      );
      const error = new Error("failed to build session context", { cause });
      throw error;
    }

    try {
      const reasoningEffort = turn.reasoningEffort || "off";
      const reasoning: boolean | "low" | "medium" | "high" =
        reasoningEffort === "off" ? false : reasoningEffort;

      const response = await AiService.chat(
        turn.provider,
        {
          modelId: turn.llm,
          params: {
            reasoning,
            temperature: 0.8,
            topP: 0.9,
            topK: 40,
          },
        },
        task.chatOptions,
        onStreamChunk,
      );

      // Check for model loading failure
      if (
        response.doneReason === "load" &&
        !response.response &&
        !response.thinking &&
        (!response.toolCalls || response.toolCalls.length === 0)
      ) {
        throw new Error("Model failed to respond (still loading or error)");
      }

      // Emit thinking content if present
      if (response.thinking) {
        socket.emit("thinking", response.thinking);
      }

      // Emit response content if present
      if (response.response) {
        socket.emit("response", response.response);
      }

      // Emit tool calls if present
      if (response.toolCalls && response.toolCalls.length > 0) {
        for (const toolCall of response.toolCalls) {
          socket.emit(
            "toolCall",
            toolCall.callId,
            toolCall.function.name,
            toolCall.function.arguments,
            response.toolCallResults?.find((r) => r.callId === toolCall.callId)
              ?.result || "",
          );
        }
      }
    } catch (cause) {
      socket.emit(
        "workOrderComplete",
        turn._id,
        false,
        `failed to process agentic workflow loop: ${(cause as Error).message}`,
      );
      const error = new Error("failed to process agentic workflow loop", {
        cause,
      });
      throw error;
    }

    // Emit work order complete
    socket.emit("workOrderComplete", turn._id, true);
  }

  buildSessionContext(workOrder: IAgentWorkOrder): IContextChatMessage[] {
    const session = workOrder.turn.session as IChatSession;
    if (!session.user) {
      throw new Error("ChatSession must be populated with user data");
    }
    const user: IUser = session.user as IUser;
    const messages: IContextChatMessage[] = [];

    for (const turn of workOrder.context) {
      /*
       * add the User message
       */
      messages.push({
        createdAt: turn.createdAt,
        role: "user",
        content: turn.prompts.user,
        user: {
          _id: user._id,
          username: user.email,
          displayName: user.displayName,
        },
      });

      /*
       * Add the agent's responses (thinking, respone text, tool calls)
       */
      if (turn.toolCalls?.length > 0) {
        for (const toolCall of turn.toolCalls) {
          messages.push({
            createdAt: turn.createdAt,
            role: "tool",
            callId: toolCall.callId,
            toolName: toolCall.name,
            content: toolCall.response,
          });
        }
      }

      /*
       * Add the assistant's output (if any), to include the thinking
       * (reasoning) output (if any).
       */
      let content = "";

      // Extract thinking and response from blocks
      for (const block of turn.blocks) {
        if (block.mode === "thinking" && typeof block.content === "string") {
          content += `<thinking>${block.content}</thinking>`;
        } else if (
          block.mode === "responding" &&
          typeof block.content === "string"
        ) {
          if (content && content.length) {
            content += "\n";
          }
          content += block.content;
        }
      }

      messages.push({
        createdAt: turn.createdAt,
        role: "assistant",
        content:
          content && content.length
            ? content
            : "(you didn't say anything this turn)",
      });
    }

    return messages;
  }

  /**
   * To optimize context, reduce clutter, and help the agent focus, full outputs
   * of older file reads and edits are summarized once a newer version is available.
   */
  pruneSessionContext(messages: IContextChatMessage[]): void {
    // TODO
  }

  private getToolsForMode(mode: ChatSessionMode): any[] {
    return Array.from(this.toolbox.getModeSet(mode) || []);
  }
}

export default new AgentService();
