import { fork, type ChildProcess } from "node:child_process";
import { logger } from "./logger";
import type { FinalizeLogJob, FinalizeLogResult } from "./logFinalizer";

const IDLE_TIMEOUT_MS = Number(process.env["SESSION_PROCESS_IDLE_MS"]) || 5 * 60 * 1000; // 5 min default
const MAX_RESTARTS = 3;

const _processes = new Map<string, SessionProcess>();

type PendingJob = {
  resolve: (result: FinalizeLogResult) => void;
  reject: (err: Error) => void;
};

export class SessionProcess {
  private child: ChildProcess | null = null;
  private pending = new Map<string, PendingJob>();
  private nextId = 0;
  private idleTimer: ReturnType<typeof setTimeout> | null = null;
  private restartCount = 0;
  private destroyed = false;

  constructor(private sessionId: string) {}

  /** Number of outstanding jobs sent to this process. */
  get pendingCount(): number {
    return this.pending.size;
  }

  private ensureRunning(): ChildProcess {
    if (this.child !== null && this.child.connected) return this.child;

    const entryPath = new URL("./sessionWorkerEntry.ts", import.meta.url).pathname;
    // On Windows, the path from URL.pathname starts with "/" which needs to
    // be stripped when it's a drive letter path (e.g. "/C:/..." → "C:/...")
    const resolvedPath =
      process.platform === "win32" && entryPath.startsWith("/") ? entryPath.slice(1) : entryPath;

    this.child = fork(resolvedPath, [], {
      stdio: ["pipe", "pipe", "pipe", "ipc"],
    });

    this.restartCount += 1;

    this.child.on("message", (msg: { id: string; result: FinalizeLogResult }) => {
      const pending = this.pending.get(msg.id);
      if (pending !== undefined) {
        this.pending.delete(msg.id);
        pending.resolve(msg.result);
      }
      this.resetIdleTimer();
    });

    this.child.on("error", (err) => {
      logger.error(`[sessionProcess] Session ${this.sessionId} process error:`, err.message);
    });

    this.child.on("exit", (code, signal) => {
      const wasConnected = this.child !== null;
      this.child = null;

      if (this.destroyed) return;

      // Reject all pending jobs on unexpected exit
      for (const [, pending] of this.pending) {
        pending.reject(
          new Error(
            `Session process exited with code ${code ?? signal}, session=${this.sessionId}`,
          ),
        );
      }
      this.pending.clear();

      if (wasConnected && this.restartCount <= MAX_RESTARTS) {
        logger.warn(
          `[sessionProcess] Session ${this.sessionId} worker exited (code=${code ?? signal}), ` +
            `restart ${this.restartCount}/${MAX_RESTARTS}`,
        );
      }
    });

    this.resetIdleTimer();
    return this.child;
  }

  enqueue(job: FinalizeLogJob): Promise<FinalizeLogResult> {
    return new Promise((resolve, reject) => {
      const child = this.ensureRunning();
      const id = String(++this.nextId);
      this.pending.set(id, { resolve, reject });
      child.send({ id, job });
    });
  }

  private resetIdleTimer(): void {
    if (this.idleTimer !== null) clearTimeout(this.idleTimer);
    this.idleTimer = setTimeout(() => {
      if (this.pending.size === 0) {
        this.destroy();
      }
    }, IDLE_TIMEOUT_MS);
  }

  destroy(): void {
    this.destroyed = true;
    if (this.idleTimer !== null) {
      clearTimeout(this.idleTimer);
      this.idleTimer = null;
    }
    if (this.child !== null) {
      this.child.kill();
      this.child = null;
    }
    _processes.delete(this.sessionId);
  }
}

/** Get or create a dedicated child process for a session. */
export function getSessionProcess(sessionId: string): SessionProcess {
  const existing = _processes.get(sessionId);
  if (existing !== undefined) return existing;

  const sp = new SessionProcess(sessionId);
  _processes.set(sessionId, sp);
  return sp;
}

/** Forcefully tear down all session processes (e.g. on server shutdown). */
export function destroyAllSessionProcesses(): void {
  for (const [, sp] of _processes) {
    sp.destroy();
  }
}
