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

import { io, Socket } from "socket.io-client";
import { GadgetService } from "../lib/service.ts";
import env from "../config/env.ts";
import {
  type IChatSession,
  type IProject,
  type IDroneRegistration,
  type IProjectTask,
  type IUser,
  WorkspaceMode,
  type SubmitPromptCallbackData,
} from "@gadget/api";

/**
 * REST API response envelope
 */
interface ApiResponse<T = unknown> {
  success: boolean;
  message?: string;
  data?: T;
}

/**
 * Work order completion result
 */
interface WorkOrderResult {
  success: boolean;
  message?: string;
}

/**
 * Pending work order tracker — maps turnId to a Promise resolver
 */
interface PendingWorkOrder {
  resolve: (value: WorkOrderResult) => void;
  reject: (reason: Error) => void;
}

class PlatformService extends GadgetService {
  private jwt: string | null = null;
  private socket: Socket | null = null;
  private selectedDrone: IDroneRegistration | null = null;

  /**
   * Tracks in-flight work orders by turnId. When the server emits
   * "workOrderComplete", the corresponding Promise is resolved.
   */
  private pendingWorkOrders: Map<string, PendingWorkOrder> = new Map();

  /**
   * Interval timer for session heartbeats sent to the drone
   * via the platform. Prevents the drone's 120s heartbeat timeout.
   */
  private heartbeatInterval: ReturnType<typeof setInterval> | null = null;

  get name(): string {
    return "PlatformService";
  }
  get slug(): string {
    return "svc:platform";
  }

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

  async stop(): Promise<void> {
    this.stopHeartbeat();

    if (this.socket) {
      this.socket.disconnect();
      this.socket = null;
    }

    // Reject any pending work orders
    for (const [turnId, pending] of this.pendingWorkOrders) {
      pending.reject(new Error("PlatformService shutting down"));
    }
    this.pendingWorkOrders.clear();

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

  setSelectedDrone(drone: IDroneRegistration): void {
    this.selectedDrone = drone;
  }

  getSelectedDrone(): IDroneRegistration | null {
    return this.selectedDrone;
  }

  // ─── REST API Methods ───────────────────────────────────────────────

  /**
   * Authenticate with the gadget-code platform.
   * POST /api/v1/auth/sign-in
   * Returns: JWT token string
   */
  async authenticate(email: string, password: string): Promise<string> {
    const json = await this.apiRequest<{ token: string; user: IUser }>(
      "POST",
      "/auth/sign-in",
      { email, password },
    );

    if (!json.data?.token) {
      throw new Error("Authentication response did not contain a token");
    }

    this.jwt = json.data.token;
    this.log.info("authenticated with platform", { email });
    return this.jwt;
  }

  /**
   * Get all projects for the authenticated user.
   * GET /api/v1/projects
   */
  async getProjects(): Promise<IProject[]> {
    const json = await this.apiRequest<IProject[]>("GET", "/projects");
    return json.data ?? [];
  }

  /**
   * Create a new chat session.
   * POST /api/v1/chat-sessions
   */
  async createSession(opts: {
    projectId: string;
    providerId: string;
    selectedModel: string;
    mode: string;
    name?: string;
  }): Promise<IChatSession> {
    const json = await this.apiRequest<IChatSession>("POST", "/chat-sessions", {
      projectId: opts.projectId,
      providerId: opts.providerId,
      selectedModel: opts.selectedModel,
      mode: opts.mode,
      name: opts.name,
    });

    if (!json.data) {
      throw new Error("Create session response did not contain data");
    }

    return json.data;
  }

  /**
   * Get available drone registrations.
   * GET /api/v1/drone/registration
   */
  async getAvailableDrones(): Promise<IDroneRegistration[]> {
    const json = await this.apiRequest<IDroneRegistration[]>(
      "GET",
      "/drone/registration",
    );
    return json.data ?? [];
  }

  /**
   * Update a task's lastRun field.
   * PATCH /api/v1/projects/:projectId/tasks/:taskId/lastRun
   */
  async updateTaskLastRun(
    projectId: string,
    taskId: string,
    sessionId: string,
  ): Promise<void> {
    await this.apiRequest(
      "PATCH",
      `/projects/${projectId}/tasks/${taskId}/lastRun`,
      { sessionId },
    );
    this.log.info("task lastRun updated", { projectId, taskId, sessionId });
  }

  // ─── REST API Helper ────────────────────────────────────────────────

  private async apiRequest<T = unknown>(
    method: string,
    path: string,
    body?: unknown,
  ): Promise<ApiResponse<T>> {
    const url = `${env.platform.baseUrl}/api/v1${path}`;
    const headers: Record<string, string> = {
      Accept: "application/json",
      "Content-Type": "application/json",
    };
    if (this.jwt) {
      headers["Authorization"] = `Bearer ${this.jwt}`;
    }

    const response = await fetch(url, {
      method,
      headers,
      body: body ? JSON.stringify(body) : undefined,
    });

    const json = (await response.json()) as ApiResponse<T>;
    if (!json.success) {
      const error = new Error(json.message || "API request failed");
      (error as any).statusCode = response.status;
      throw error;
    }

    return json;
  }

  // ─── Socket.IO Methods ──────────────────────────────────────────────

  /**
   * Connect to the platform via Socket.IO using JWT auth.
   * Mirrors the browser IDE's socket connection behavior.
   */
  async connectSocket(): Promise<void> {
    return new Promise((resolve, reject) => {
      if (!this.jwt) {
        reject(new Error("Cannot connect socket: not authenticated"));
        return;
      }

      const options = {
        auth: { token: this.jwt },
        reconnection: true,
        reconnectionAttempts: 10,
        reconnectionDelay: 1000,
        reconnectionDelayMax: 5000,
        timeout: 5000,
        transports: ["websocket"],
      };

      this.socket = io(env.platform.baseUrl, options);

      this.socket.on("connect_error", (err: Error) => {
        this.log.error("socket connect error", { err: err.message });
        reject(err);
      });

      this.socket.on("connect", () => {
        this.log.info("connected to platform via Socket.IO");
        this.startHeartbeat();
        resolve();
      });

      // Listen for work order completion from the server
      this.socket.on(
        "workOrderComplete",
        (turnId: string, success: boolean, message?: string) => {
          this.onWorkOrderComplete(turnId, success, message);
        },
      );

      this.socket.on("disconnect", (reason: string) => {
        this.log.info("socket disconnected", { reason });
      });

      this.socket.io.on("reconnect", (attempt: number) => {
        this.log.info("socket reconnected", { attempt });
      });

      this.socket.io.on("reconnect_attempt", (attempt: number) => {
        this.log.info("socket reconnect attempt", { attempt });
      });

      this.socket.io.on("reconnect_failed", () => {
        this.log.error("socket reconnection failed after max attempts");
      });
    });
  }

  /**
   * Lock the selected drone to a session.
   * Socket emit: requestSessionLock(registration, project, session, callback)
   */
  async requestSessionLock(
    project: IProject,
    session: IChatSession,
  ): Promise<boolean> {
    if (!this.socket?.connected || !this.selectedDrone) {
      this.log.warn("cannot request session lock: socket not connected or no drone selected");
      return false;
    }

    return new Promise((resolve) => {
      this.socket!.emit(
        "requestSessionLock",
        this.selectedDrone,
        project,
        session,
        (success: boolean, _chatSessionId: string) => {
          if (success) {
            this.log.info("session lock acquired", {
              droneId: this.selectedDrone!._id,
              sessionId: session._id,
            });
          } else {
            this.log.warn("session lock denied", {
              droneId: this.selectedDrone!._id,
            });
          }
          resolve(success);
        },
      );
    });
  }

  /**
   * Set the drone's workspace mode.
   * Socket emit: requestWorkspaceMode(registration, project, session, mode, callback)
   */
  async requestWorkspaceMode(
    project: IProject,
    session: IChatSession,
    mode: WorkspaceMode,
  ): Promise<boolean> {
    if (!this.socket?.connected || !this.selectedDrone) {
      this.log.warn("cannot request workspace mode: socket not connected or no drone selected");
      return false;
    }

    return new Promise((resolve) => {
      this.socket!.emit(
        "requestWorkspaceMode",
        this.selectedDrone,
        project,
        session,
        mode,
        (success: boolean, currentMode: WorkspaceMode, reason?: string) => {
          if (success) {
            this.log.info("workspace mode set", {
              mode: currentMode,
            });
          } else {
            this.log.warn("workspace mode request failed", {
              mode,
              currentMode,
              reason,
            });
          }
          resolve(success);
        },
      );
    });
  }

  /**
   * Submit a prompt and wait for the work order to complete.
   * Socket emit: submitPrompt(content, callback)
   * Then waits for "workOrderComplete" server event.
   */
  async submitPromptAndWait(
    content: string,
  ): Promise<WorkOrderResult> {
    if (!this.socket?.connected) {
      throw new Error("Socket not connected");
    }

    return new Promise((resolve, reject) => {
      this.socket!.emit(
        "submitPrompt",
        content,
        (success: boolean, data: SubmitPromptCallbackData) => {
          if (!success) {
            reject(
              new Error(data.message || "Prompt submission failed"),
            );
            return;
          }

          const turnId = data.turnId;
          if (!turnId) {
            reject(new Error("No turnId returned from submitPrompt"));
            return;
          }

          // Store the resolver — it will be called when workOrderComplete fires
          this.pendingWorkOrders.set(turnId, { resolve, reject });
          this.log.info("prompt submitted, waiting for work order completion", {
            turnId,
          });
        },
      );
    });
  }

  /**
   * Release the session lock on the drone.
   * Socket emit: releaseSessionLock(registration, project, session, callback)
   */
  async releaseSessionLock(
    project: IProject,
    session: IChatSession,
  ): Promise<void> {
    if (!this.socket?.connected || !this.selectedDrone) {
      this.log.warn("cannot release session lock: socket not connected or no drone selected");
      return;
    }

    return new Promise((resolve) => {
      this.socket!.emit(
        "releaseSessionLock",
        this.selectedDrone,
        project,
        session,
        (success: boolean) => {
          if (success) {
            this.log.info("session lock released", {
              droneId: this.selectedDrone!._id,
              sessionId: session._id,
            });
          } else {
            this.log.warn("session lock release failed", {
              droneId: this.selectedDrone!._id,
              sessionId: session._id,
            });
          }
          resolve();
        },
      );
    });
  }

  // ─── Task Execution ─────────────────────────────────────────────────

  /**
   * Execute a task through the full headless IDE pipeline:
   * 1. Create ChatSession via REST API
   * 2. Lock the drone to this session
   * 3. Set workspace mode to Agent
   * 4. Submit prompt and wait for completion
   * 5. Release session lock
   * 6. Update task.lastRun via dedicated PATCH endpoint
   */
  async executeTask(task: IProjectTask, project: IProject): Promise<void> {
    this.log.info("task execution starting", {
      taskId: task._id,
      taskName: task.name,
      projectSlug: project.slug,
      mode: task.mode,
    });

    let session: IChatSession | null = null;

    try {
      // 1. Create ChatSession via REST API
      session = await this.createSession({
        projectId: project._id as string,
        providerId: task.provider as string,
        selectedModel: task.selectedModel as string,
        mode: task.mode,
        name: `Task: ${task.name}`,
      });

      // 2. Lock the drone to this session
      const locked = await this.requestSessionLock(project, session);
      if (!locked) {
        throw new Error("Failed to lock drone for task execution");
      }

      // 3. Set workspace mode to Agent
      const modeSet = await this.requestWorkspaceMode(
        project,
        session,
        WorkspaceMode.Agent,
      );
      if (!modeSet) {
        throw new Error("Failed to set drone workspace mode");
      }

      // 4. Submit prompt and wait for completion
      const result = await this.submitPromptAndWait(task.content);

      // 5. Release session lock
      await this.releaseSessionLock(project, session);

      // 6. Update task.lastRun via dedicated PATCH endpoint
      await this.updateTaskLastRun(
        project._id as string,
        task._id as string,
        session._id as string,
      );

      this.log.info("task execution completed", {
        taskId: task._id,
        taskName: task.name,
        sessionId: session._id,
        success: result.success,
      });
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error);
      this.log.error("task execution failed", {
        taskId: task._id,
        taskName: task.name,
        error: msg,
      });

      // Try to release the session lock on failure
      if (session && this.socket?.connected && this.selectedDrone) {
        try {
          await this.releaseSessionLock(project, session);
        } catch {
          // Best-effort release
        }
      }

      throw error;
    }
  }

  // ─── Work Order Completion ──────────────────────────────────────────

  /**
   * Called when the server emits "workOrderComplete".
   * Resolves the corresponding pending Promise.
   */
  private onWorkOrderComplete(
    turnId: string,
    success: boolean,
    message?: string,
  ): void {
    const pending = this.pendingWorkOrders.get(turnId);
    if (pending) {
      this.pendingWorkOrders.delete(turnId);
      pending.resolve({ success, message });
      this.log.info("work order completed", { turnId, success, message });
    } else {
      this.log.warn("received workOrderComplete for unknown turnId", {
        turnId,
        success,
      });
    }
  }

  // ─── Heartbeat ──────────────────────────────────────────────────────

  /**
   * Start sending periodic session heartbeats to the drone
   * via the platform. This prevents the drone's 120-second
   * heartbeat timeout from firing while a task is active.
   */
  private startHeartbeat(): void {
    if (this.heartbeatInterval) return; // already running

    this.heartbeatInterval = setInterval(() => {
      if (this.socket?.connected) {
        this.socket.emit("sessionHeartbeat", (ack: boolean) => {
          if (!ack) {
            this.log.warn("sessionHeartbeat: drone did not acknowledge");
          }
        });
      }
    }, 19_000); // every 19 seconds, same as browser IDE
  }

  /**
   * Stop the heartbeat interval.
   */
  private stopHeartbeat(): void {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
      this.heartbeatInterval = null;
    }
  }
}

export default new PlatformService();
