import { join, isAbsolute } from 'node:path';
import { mkdirSync, writeFileSync, chmodSync, rmSync, createWriteStream } from 'node:fs';
import { tmpdir } from 'node:os';
import { getTaskByDirName } from './task-files';
import { getOwnerHomeDir, DATA_PATH } from '../../data-path';
import { killTree } from './process-tree';
import { buildTaskApiEnv } from './task-api-env';

// Script-job event stream. `stdout`/`stderr` are high-frequency (live-only + persisted to the log
// file); `started`/`exit`/`error` are structural and get buffered for viewer replay.
export type ScriptEvent =
  | { type: 'started'; taskName: string }
  | { type: 'stdout'; data: string }
  | { type: 'stderr'; data: string }
  | { type: 'progress'; progress: unknown }
  | { type: 'exit'; code: number }
  | { type: 'error'; message: string };

// A script can emit machine-readable progress on stdout: a line `@@officer:progress@@ {json}`. Those
// lines are plucked out (parsed into a `progress` event, kept OUT of the human log); everything else
// streams verbatim.
const PROGRESS_SENTINEL = '@@officer:progress@@';

export type ExecuteScriptParams = {
  jobId: string;
  userId: number;
  email: string;
  username?: string | null;
  taskDirName: string;
  inputs: Record<string, string>;
  cwd?: string;
  abortSignal: { aborted: boolean };
  emit: (event: ScriptEvent) => void;
};

const getRunner = (language: string): string[] =>
  language === 'python'
    ? ['python3']
    : language === 'typescript'
      ? ['bun', 'run']
      : language === 'javascript'
        ? ['node']
        : ['bash'];
const getFileName = (language: string): string =>
  language === 'python'
    ? 'run.py'
    : language === 'typescript'
      ? 'index.ts'
      : language === 'javascript'
        ? 'index.js'
        : 'run.sh';

function materializeScript(language: string, implementation: string): string {
  const dir = join(tmpdir(), `officer-task-${Date.now()}-${Math.random().toString(36).slice(2)}`);
  mkdirSync(dir, { recursive: true });
  const filePath = join(dir, getFileName(language));
  writeFileSync(filePath, implementation);
  chmodSync(filePath, 0o755);
  return filePath;
}

const buildInputEnv = (inputs: Record<string, string>): Record<string, string> => {
  const env: Record<string, string> = {};
  for (const [key, value] of Object.entries(inputs)) env[`INPUT_${key.toUpperCase()}`] = value;
  return env;
};

const buildArgs = (inputs: Record<string, string>, argsOrder?: string[] | null): string[] =>
  !argsOrder || argsOrder.length === 0 ? [] : argsOrder.map((name) => inputs[name] ?? '');

// The persisted per-job output log — served by GET /jobs/:id/log for backfill / unattended viewing.
export const jobLogPath = (jobId: string) => join(DATA_PATH, 'jobs', `${jobId}.log`);

// Run a script-mode task as a background job. Emits ScriptEvents (for live viewers) AND appends all
// output to a durable log file. Resolves with the process exit code; throws only on spawn failure or
// when aborted (the manager maps those to failed/stopped).
export async function executeScript(params: ExecuteScriptParams): Promise<{ exitCode: number }> {
  const { jobId, userId, email, username, inputs, abortSignal, emit } = params;

  const task = await getTaskByDirName(params.taskDirName);
  if (!task) throw new Error(`Task not found: ${params.taskDirName}`);
  if (task.mode !== 'script') throw new Error('Task is not a script-mode task');
  if (!task.implementation) throw new Error(`Task ${params.taskDirName} has no implementation`);

  const language = task.language ?? 'bash';
  const scriptPath = materializeScript(language, task.implementation);
  const inputEnv = buildInputEnv(inputs);
  const positionalArgs = buildArgs(inputs, task.args);
  const cmd = [...getRunner(language), scriptPath, ...positionalArgs];

  const homeDir = getOwnerHomeDir(email);
  const cwd = params.cwd ? (isAbsolute(params.cwd) ? params.cwd : join(homeDir, params.cwd)) : homeDir;

  const spawnCmd = cmd;
  const apiEnv = await buildTaskApiEnv({ id: userId, email, username });
  const spawnEnv = { ...(process.env as Record<string, string>), ...apiEnv, ...inputEnv };
  const spawnCwd = cwd;

  mkdirSync(join(DATA_PATH, 'jobs'), { recursive: true });
  const log = createWriteStream(jobLogPath(jobId), { flags: 'w' });
  // The pumps must keep draining the child's stdout no matter what: a throw from a viewer broadcast or
  // a log-stream error must not stop consumption, or the child's stdout pipe backs up and the whole job
  // silently wedges (log + progress freeze while it keeps running). So both side-effects are isolated.
  const safeEmit = (event: ScriptEvent) => {
    try {
      emit(event);
    } catch {
      /* a dead viewer / broadcast error must never kill the pump */
    }
  };
  const safeLog = (data: string) => {
    try {
      log.write(data);
    } catch {
      /* a log-stream error must never kill the pump */
    }
  };
  const cleanup = () => {
    try {
      rmSync(join(scriptPath, '..'), { recursive: true, force: true });
    } catch {
      /* best effort */
    }
  };

  emit({ type: 'started', taskName: task.name });

  const proc = Bun.spawn(spawnCmd, { cwd: spawnCwd, env: spawnEnv, stdout: 'pipe', stderr: 'pipe' });

  // Cooperative cancellation: poll the shared abort flag and tear down the whole process tree.
  const abortPoll = setInterval(() => {
    if (abortSignal.aborted) {
      clearInterval(abortPoll);
      try {
        killTree(proc.pid);
      } catch {
        /* already dead */
      }
    }
  }, 500);

  // stderr: raw passthrough. Each pump owns its decoder (stateful across chunks; must not be shared).
  const pumpRaw = async (reader: ReadableStreamDefaultReader<Uint8Array>, type: 'stdout' | 'stderr') => {
    const decoder = new TextDecoder();
    try {
      for (;;) {
        const { done, value } = await reader.read();
        if (done) break;
        const data = decoder.decode(value, { stream: true });
        if (!data) continue;
        safeEmit({ type, data });
        safeLog(data);
      }
    } catch {
      // stream closed
    }
  };

  // stdout: line-buffered so progress-sentinel lines can be extracted from the human log + stream.
  const pumpStdout = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
    const decoder = new TextDecoder();
    let buf = '';
    const handleLine = (line: string, hadNewline: boolean) => {
      if (line.startsWith(PROGRESS_SENTINEL)) {
        let parsed: unknown;
        try {
          parsed = JSON.parse(line.slice(PROGRESS_SENTINEL.length).trim());
        } catch {
          return; // malformed progress payload — drop it, don't leak the sentinel into the log
        }
        safeEmit({ type: 'progress', progress: parsed });
        return;
      }
      const out = hadNewline ? `${line}\n` : line;
      if (!out) return;
      safeEmit({ type: 'stdout', data: out });
      safeLog(out);
    };
    try {
      for (;;) {
        const { done, value } = await reader.read();
        if (done) break;
        buf += decoder.decode(value, { stream: true });
        let nl: number;
        while ((nl = buf.indexOf('\n')) !== -1) {
          handleLine(buf.slice(0, nl), true);
          buf = buf.slice(nl + 1);
        }
      }
    } catch {
      // stream closed
    }
    if (buf) handleLine(buf, false); // trailing partial line (no newline)
  };

  const [, , exitCode] = await Promise.all([
    pumpStdout(proc.stdout.getReader()),
    pumpRaw(proc.stderr.getReader(), 'stderr'),
    proc.exited,
  ]);

  clearInterval(abortPoll);
  await new Promise<void>((resolve) => log.end(resolve));
  cleanup();

  if (abortSignal.aborted) throw new Error('Job was stopped');

  emit({ type: 'exit', code: exitCode });
  return { exitCode };
}
