/**
 * WorkspaceManager - Manages workspace lifecycle (open/close/list).
 */

import type { DomainEvent, Workspace } from "@coder-studio/core";
import type { WatcherLogger } from "../fs/watcher.js";
import { WorkspaceRepo } from "../storage/repositories/workspace-repo.js";
import { WorkspaceValidator } from "./validator.js";

export interface OpenWorkspaceRequest {
  path: string;
  wslDistro?: string;
}

export interface EventBus {
  emit(event: DomainEvent): void;
  on(type: DomainEvent["type"], handler: (event: DomainEvent) => void): () => void;
}

export interface AutoFetchRuntime {
  triggerOpenTimeFetch(workspaceId: string): void;
  recordSuccess(workspaceId: string): void;
  getLastFetchAt(workspaceId: string): number | undefined;
}

export interface WorkspaceManagerDeps {
  workspaceRepo: WorkspaceRepo;
  eventBus: EventBus;
  broadcaster?: Broadcaster;
  autoFetch?: AutoFetchRuntime;
  onDirty?: (workspaceId: string, reason: string) => void;
  teardown?: (workspaceId: string) => void | Promise<void>;
  onClose?: (workspaceId: string) => void | Promise<void>;
  logger?: WatcherLogger;
}

/**
 * Generates a unique workspace ID.
 */
function generateWorkspaceId(): string {
  return `ws_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
}

/**
 * WorkspaceManager handles workspace lifecycle operations.
 * It validates paths, checks runtime requirements, persists to DB,
 * and broadcasts metadata changes via EventBus.
 */
export class WorkspaceManager {
  private validator = new WorkspaceValidator();
  private watchers = new Map<string, WorkspaceWatcher>();
  private readonly repo: WorkspaceRepo;
  private logger: WatcherLogger | undefined;

  constructor(private deps: WorkspaceManagerDeps) {
    this.repo = deps.workspaceRepo;
    this.logger = deps.logger;
  }

  /**
   * Late-binds a logger after construction. Mirrors the pattern used by
   * `WSHub.setLogger`, so the manager can be created before Fastify's
   * `app.log` is available and still log watcher events with the proper
   * runtime logger.
   */
  setLogger(logger: WatcherLogger): void {
    this.logger = logger;
  }

  private startWatcher(workspaceId: string, rootPath: string): void {
    if (!this.deps.broadcaster || this.watchers.has(workspaceId)) {
      return;
    }

    this.watchers.set(
      workspaceId,
      new WorkspaceWatcher(
        workspaceId,
        rootPath,
        this.deps.broadcaster,
        this.logger,
        this.deps.onDirty
      )
    );
  }

  hydrateWatchers(): void {
    for (const workspace of this.list()) {
      this.startWatcher(workspace.id, workspace.path);
    }
  }

  updateUiState(workspaceId: string, uiState: Workspace["uiState"]): void {
    const workspace = this.get(workspaceId);
    if (!workspace) {
      throw new Error(`Workspace not found: ${workspaceId}`);
    }

    this.repo.updateUiState(workspaceId, uiState);

    this.deps.eventBus.emit({
      type: "workspace.meta.changed",
      workspaceId,
      patch: { uiState },
    });
  }

  /**
   * Opens a new workspace.
   *
   * 1. Validates path exists and is accessible
   * 2. Checks if workspace already exists for this path
   * 3. Persists workspace through the repository (or returns existing)
   * 4. Emits metadata change event
   *
   * @param req - Open workspace request
   * @returns Created or existing workspace
   */
  async open(req: OpenWorkspaceRequest): Promise<Workspace> {
    // 1. Validate path
    await this.validator.validate(req.path);

    // 2. Check if workspace already exists for this path
    const existing = this.getByPath(req.path);
    if (existing) {
      // Update last active timestamp and return existing
      this.touch(existing.id);
      const refreshed = this.get(existing.id) ?? existing;

      // Start watcher if not already watching (e.g., after server restart)
      this.startWatcher(refreshed.id, refreshed.path);

      this.deps.eventBus.emit({
        type: "workspace.meta.changed",
        workspaceId: refreshed.id,
        patch: { lastActiveAt: refreshed.lastActiveAt },
      });
      this.deps.autoFetch?.triggerOpenTimeFetch(refreshed.id);
      return refreshed;
    }

    // 3. Persist through the repository
    const workspace: Workspace = {
      id: generateWorkspaceId(),
      path: req.path,
      targetRuntime: "native",
      wslDistro: req.wslDistro,
      openedAt: Date.now(),
      lastActiveAt: Date.now(),
      uiState: {
        leftPanelWidth: 250,
        bottomPanelHeight: 200,
        focusMode: false,
        paneLayout: {
          id: "root",
          type: "leaf",
          leafKind: "draft",
        },
      },
    };

    this.repo.create(workspace);

    // 3. Emit event
    this.deps.eventBus.emit({
      type: "workspace.meta.changed",
      workspaceId: workspace.id,
      patch: workspace,
    });

    // Start file system watcher
    this.startWatcher(workspace.id, workspace.path);
    this.deps.autoFetch?.triggerOpenTimeFetch(workspace.id);

    return workspace;
  }

  /**
   * Closes a workspace.
   *
   * @param workspaceId - Workspace ID to close
   */
  async close(workspaceId: string): Promise<void> {
    const workspace = this.get(workspaceId);
    if (!workspace) {
      throw new Error(`Workspace not found: ${workspaceId}`);
    }

    // Stop file system watcher
    const watcher = this.watchers.get(workspaceId);
    if (watcher) {
      await watcher.close();
      this.watchers.delete(workspaceId);
    }

    if (this.deps.teardown) {
      await this.deps.teardown(workspaceId);
    }

    // Delete from the workspace repository
    this.repo.delete(workspaceId);

    if (this.deps.onClose) {
      try {
        await this.deps.onClose(workspaceId);
      } catch (err) {
        console.warn("[workspace] onClose hook failed:", err);
      }
    }

    // Emit event
    this.deps.eventBus.emit({
      type: "workspace.meta.changed",
      workspaceId: workspaceId,
      patch: { lastActiveAt: Date.now() },
    });
  }

  /**
   * Lists all open workspaces.
   *
   * @returns Array of workspaces
   */
  list(): Workspace[] {
    return this.repo.list();
  }

  /**
   * Gets a single workspace by ID.
   *
   * @param workspaceId - Workspace ID
   * @returns Workspace or undefined
   */
  get(workspaceId: string): Workspace | undefined {
    return this.repo.findById(workspaceId);
  }

  /**
   * Gets a workspace by path.
   *
   * @param path - Workspace path
   * @returns Workspace or undefined
   */
  getByPath(path: string): Workspace | undefined {
    return this.repo.findByPath(path);
  }

  /**
   * Updates workspace last active timestamp.
   *
   * @param workspaceId - Workspace ID
   */
  touch(workspaceId: string): void {
    const now = Date.now();
    this.repo.updateLastActive(workspaceId, now);
  }

  recordFetch(workspaceId: string): void {
    this.deps.autoFetch?.recordSuccess(workspaceId);
  }
}

import { WorkspaceWatcher } from "../fs/watcher.js";

export interface Broadcaster {
  broadcast(topic: string, data: unknown): void;
}
