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

import assert from "node:assert";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";

import { simpleGit, SimpleGit, SimpleGitOptions } from "simple-git";

import { IChatTurn, IProject } from "@gadget/api";

import { GadgetService } from "../lib/service.ts";

export interface WorkspaceChatSession {
  _id: string; // MongoDB ChatSession._id
  name: string; // Session name for display
  lockedAt: string; // ISO 8601 timestamp
}

export interface WorkspaceLockedProject {
  _id: string; // MongoDB Project._id
  slug: string; // Project slug (directory name)
  gitUrl?: string; // Remote git URL
  lockedAt: string; // ISO 8601 timestamp
}

export interface WorkspaceProject {
  _id: string;
  slug: string;
  gitUrl?: string;
  clonedAt: string | null;
  lastSyncAt: string | null;
}

export interface WorkspaceRegistration {
  _id: string; // MongoDB DroneRegistration._id
  status: string; // Current drone status
  registeredAt: string; // ISO 8601 timestamp
}

export interface WorkspaceData {
  workspaceId: string; // UUID v4, immutable once created
  createdAt: string; // ISO 8601 timestamp
  hostname: string; // Machine hostname where drone runs
  workspaceDir: string; // Absolute path to workspace directory

  // Active session state (null when idle)
  chatSession: WorkspaceChatSession | null;

  // Project currently being worked on (null when idle)
  lockedProject: WorkspaceLockedProject | null;

  // All projects cloned into this workspace
  projects: Array<WorkspaceProject>;

  // Drone registration (updated each startup)
  registration: WorkspaceRegistration | null;
}

export interface WorkOrderCache {
  id: string;
  receivedAt: string; // ISO 8601 timestamp
  turn: IChatTurn;
  status: "processing" | "completed" | "error";
  error?: string; // Error message if status === 'error'
}

class WorkspaceService extends GadgetService {
  private _gadgetDir: string = "";
  private cacheDir: string = "";

  private workspaceFile: string = "";
  private _workspaceData: WorkspaceData | null = null;

  private workspaceGit?: SimpleGit;
  private repos = new Map<string, SimpleGit>();

  get name(): string {
    return "WorkspaceService";
  }
  get slug(): string {
    return "svc:workspace";
  }

  get gadgetDir(): string | undefined {
    return this._gadgetDir || undefined;
  }

  get workspaceData(): WorkspaceData | null {
    return this._workspaceData;
  }

  get workspaceId(): string | null {
    return this._workspaceData?.workspaceId ?? null;
  }

  get workspaceDir(): string | undefined {
    return this._workspaceData?.workspaceDir;
  }

  get workspaceCacheDir(): string | undefined {
    return this.cacheDir || undefined;
  }

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

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

  /**
   * Initializes the workspace directory structure.
   * Creates .gadget/ directory if it doesn't exist.
   * Validates or creates workspace.json with persistent identity.
   */
  async initialize(workspaceDir: string): Promise<void> {
    this._gadgetDir = path.join(workspaceDir, ".gadget");
    this.cacheDir = path.join(this._gadgetDir, "cache");
    this.workspaceFile = path.join(this._gadgetDir, "workspace.json");

    // Create directory structure
    await fs.promises.mkdir(this._gadgetDir, { recursive: true });
    await fs.promises.mkdir(this.cacheDir, { recursive: true });

    // Load or create workspace data
    await this.loadOrCreateWorkspaceData(workspaceDir);

    const options: Partial<SimpleGitOptions> = { baseDir: workspaceDir };
    this.workspaceGit = simpleGit(options);

    this.log.info("workspace initialized", {
      workspaceId: this._workspaceData?.workspaceId,
      workspaceDir,
    });
  }

  /**
   * Loads existing workspace data or creates new workspace.
   */
  private async loadOrCreateWorkspaceData(workspaceDir: string): Promise<void> {
    try {
      if (await this.exists(this.workspaceFile)) {
        // Load existing workspace
        const content = await fs.promises.readFile(this.workspaceFile, "utf-8");
        this._workspaceData = JSON.parse(content) as WorkspaceData;
        this.log.info("loaded existing workspace", {
          workspaceId: this._workspaceData.workspaceId,
        });
      } else {
        // Create new workspace
        const crypto = await import("node:crypto");
        this._workspaceData = {
          workspaceId: crypto.randomUUID(),
          createdAt: new Date().toISOString(),
          hostname: os.hostname(),
          workspaceDir: workspaceDir,
          chatSession: null,
          lockedProject: null,
          projects: [],
          registration: null,
        };
        await this.writeWorkspaceData();
        this.log.info("created new workspace", {
          workspaceId: this._workspaceData.workspaceId,
        });
      }
    } catch (error) {
      const err = error as Error;
      this.log.error("failed to load workspace data", { error: err.message });
      throw new Error(`Failed to initialize workspace: ${err.message}`);
    }
  }

  /**
   * Writes current workspace data to disk.
   */
  async writeWorkspaceData(): Promise<void> {
    if (!this._workspaceData) {
      throw new Error("Cannot write workspace data: not initialized");
    }
    await fs.promises.writeFile(
      this.workspaceFile,
      JSON.stringify(this._workspaceData, null, 2),
      "utf-8",
    );
  }

  /**
   * Updates the chat session in workspace data.
   */
  updateChatSession(chatSession: { _id: string; name: string } | null): void {
    if (!this._workspaceData) return;

    this._workspaceData.chatSession = chatSession
      ? {
          ...chatSession,
          lockedAt: new Date().toISOString(),
        }
      : null;
  }

  /**
   * Updates the locked project in workspace data.
   */
  updateLockedProject(
    project: {
      _id: string; // MongoDB Project._id
      slug: string; // Project slug (directory name)
      gitUrl?: string; // Remote git URL
    } | null,
  ): void {
    if (!this._workspaceData) return;
    if (!project) {
      this._workspaceData.lockedProject = null;
      return;
    }
    this._workspaceData.lockedProject = Object.assign(
      {
        lockedAt: new Date().toISOString(),
      },
      project,
    );
  }

  /**
   * Adds a project to the workspace projects list.
   */
  addProject(project: { _id: string; slug: string; gitUrl?: string }): void {
    if (!this._workspaceData) return;

    const existing = this._workspaceData.projects.find(
      (p) => p.slug === project.slug,
    );
    if (existing) {
      existing._id = project._id;
      if (project.gitUrl) {
        existing.gitUrl = project.gitUrl;
      } else {
        delete existing.gitUrl;
      }
      return;
    }

    this._workspaceData.projects.push({
      ...project,
      clonedAt: null,
      lastSyncAt: null,
    });
  }

  async deployProject(project: IProject): Promise<void> {
    assert(this.workspaceData, "workspace is uninitialized");

    const projectDir = this.getProjectDirectory(project.slug);
    const dirExists = await this.exists(projectDir);

    if (project.gitUrl) {
      assert(this.workspaceGit, "workspace git interface is uninitialized");

      if (dirExists) {
        /*
         * If .git exists, it's already cloned — nothing to do.
         * If .git is missing, the directory is stale (partial clone, mkdir
         * from a previous non-git deploy, etc.). Remove it so we can clone
         * fresh.
         */
        const gitDir = path.join(projectDir, ".git");
        if (await this.exists(gitDir)) {
          this.log.info("project already cloned, skipping deploy", {
            projectDir,
          });
          return;
        }

        this.log.warn("removing stale project directory for fresh clone", {
          projectDir,
        });
        await fs.promises.rm(projectDir, { recursive: true, force: true });
      }

      this.log.info("cloning into git repo", {
        url: project.gitUrl,
        projectDir,
      });
      await this.workspaceGit.clone(project.gitUrl, projectDir);
      this.log.info("project deployed via git clone", { projectDir });
      return;
    }

    /*
     * Non-git project: only create the directory if it doesn't exist.
     */
    if (!dirExists) {
      this.log.info("creating project directory", { projectDir });
      await fs.promises.mkdir(projectDir, { recursive: true });
    } else {
      this.log.info("project directory already exists", { projectDir });
    }
  }

  /**
   * Checks if a project ID is already tracked in the workspace.
   */
  hasProjectById(id: string): boolean {
    if (!this._workspaceData) return false;
    return this._workspaceData.projects.some((p) => p._id === id);
  }

  /**
   * Marks a project as deployed by updating its clonedAt and lastSyncAt timestamps.
   */
  markProjectDeployed(slug: string): void {
    if (!this._workspaceData) return;
    const project = this._workspaceData.projects.find((p) => p.slug === slug);
    if (project) {
      const now = new Date().toISOString();
      project.clonedAt = now;
      project.lastSyncAt = now;
    }
  }

  /**
   * Updates the registration in workspace data.
   */
  updateRegistration(
    registration: {
      _id: string;
      status: string;
    } | null,
  ): void {
    if (!this._workspaceData) return;

    this._workspaceData.registration = registration
      ? {
          ...registration,
          registeredAt: new Date().toISOString(),
        }
      : null;
  }

  /**
   * Writes a work order cache file before processing.
   */
  async writeWorkOrderCache(turn: IChatTurn): Promise<string> {
    const crypto = await import("node:crypto");
    const workOrderId = crypto.randomUUID();
    const cacheFile = path.join(this.cacheDir, "work-order.json");

    const cache: WorkOrderCache = {
      id: workOrderId,
      receivedAt: new Date().toISOString(),
      turn,
      status: "processing",
    };

    await fs.promises.writeFile(
      cacheFile,
      JSON.stringify(cache, null, 2),
      "utf-8",
    );
    this.log.info("work order cache written", {
      id: workOrderId,
      turnId: turn._id,
    });

    return workOrderId;
  }

  /**
   * Removes the work order cache file after completion.
   */
  async removeWorkOrderCache(): Promise<void> {
    const cacheFile = path.join(this.cacheDir, "work-order.json");
    try {
      if (await this.exists(cacheFile)) {
        await fs.promises.unlink(cacheFile);
        this.log.info("work order cache removed");
      }
    } catch (error) {
      const err = error as Error;
      this.log.warn("failed to remove work order cache", {
        error: err.message,
      });
    }
  }

  /**
   * Reads the work order cache for crash recovery.
   */
  async readWorkOrderCache(): Promise<WorkOrderCache | null> {
    const cacheFile = path.join(this.cacheDir, "work-order.json");
    try {
      if (await this.exists(cacheFile)) {
        const content = await fs.promises.readFile(cacheFile, "utf-8");
        return JSON.parse(content) as WorkOrderCache;
      }
    } catch (error) {
      const err = error as Error;
      this.log.warn("failed to read work order cache", {
        error: err.message,
      });
    }
    return null;
  }

  /**
   * Checks if a file or directory exists.
   */
  async exists(filePath: string): Promise<boolean> {
    try {
      await fs.promises.access(filePath);
      return true;
    } catch {
      return false;
    }
  }

  getProjectDirectory(slug: string): string {
    assert(this.workspaceData, "workspace uninitialized");
    return path.join(this.workspaceData?.workspaceDir, slug);
  }

  getGitRepo(slug: string): SimpleGit {
    assert(this.workspaceData, "workspace uninitialized");

    /*
     * If we have a SimpleGit instance for the project slug, return it.
     */
    let git: SimpleGit | undefined = this.repos.get(slug);
    if (git) {
      return git;
    }

    /*
     * Create a new SimpleGit instance rooted in the project's workspace
     * directory, add it to the repo cache, and return it.
     */
    const options: Partial<SimpleGitOptions> = {
      baseDir: this.getProjectDirectory(slug),
    };
    git = simpleGit(options);
    this.repos.set(slug, git);

    return git;
  }
}

export default new WorkspaceService();
