import { spawn, type ChildProcess } from "node:child_process";
import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { logger } from "./logger";
import type { FinalizeLogJob, FinalizeLogResult } from "./logFinalizer";
import { isPlainRecord, safeGetOwnProperty } from "../lib/objectUtils";

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;
};

function errorMessage(err: unknown): string {
  return err instanceof Error ? err.message : String(err);
}

function resolveSessionWorkerPath(): string {
  return fileURLToPath(new URL("./sessionWorkerEntry.ts", import.meta.url));
}

export function isSessionProcessAvailable(): boolean {
  return existsSync(resolveSessionWorkerPath());
}

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 resolvedPath = resolveSessionWorkerPath();

    this.child = spawn(process.execPath, [...process.execArgv, resolvedPath], {
      stdio: ["pipe", "pipe", "pipe", "ipc"],
      windowsHide: true,
    });

    this.restartCount += 1;

    this.child.on("message", (raw: unknown) => {
      if (!isPlainRecord(raw)) {
        logger.error("[sessionProcess] Received malformed IPC message from worker, discarding");
        return;
      }
      const id = safeGetOwnProperty(raw, "id");
      const result = safeGetOwnProperty(raw, "result");
      if (typeof id !== "string" || !isPlainRecord(result)) {
        logger.error("[sessionProcess] Received malformed IPC message from worker, discarding");
        return;
      }
      // Runtime shape validated above; the cast bridges unknown → FinalizeLogResult.
      // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
      const msg = { id, result: result as 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 });
      try {
        child.send({ id, job }, (err) => {
          if (err === null || err === undefined) return;
          this.pending.delete(id);
          reject(
            new Error(
              `Session process send failed for session=${this.sessionId}: ${errorMessage(err)}`,
            ),
          );
        });
      } catch (err) {
        this.pending.delete(id);
        reject(
          new Error(
            `Session process send failed for session=${this.sessionId}: ${errorMessage(err)}`,
          ),
        );
      }
    });
  }

  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();
  }
}
