// SystemIO backend — shell, files, build
import type { EffectBackend } from "../backend";
import type { State } from "../../eval/machine";
import type { OpCall } from "../opcall";
import type { Val } from "../../eval/values";
import { capRequire, type CapSet } from "../../governance/caps";
import type { Profile } from "../../governance/profile";
import type { Runtime } from "../../eval/runtime";
import { compileTextToExpr } from "../../pipeline/compileText";
import { runToCompletionWithState } from "../../eval/run";

function mapGet(payload: Val | undefined, key: string): Val | undefined {
  if (!payload || payload.tag !== "Map") return undefined;
  for (const [k, v] of payload.entries) {
    if (k.tag === "Sym" && k.name === key) return v;
    if (k.tag === "Str" && k.s === key) return v;
  }
  return undefined;
}

function getFirstNumber(vals: Array<Val | undefined>): number | undefined {
  for (const v of vals) {
    if (v?.tag === "Num") return v.n;
  }
  return undefined;
}

export class SystemIOBackend implements EffectBackend {
  readonly ops = [
    "shell.op",
    "file.read.op",
    "file.write.op",
    "build.run.op",
    "eval.run.op",
    "callback.eval_lisp",
  ] as const;

  constructor(
    private readonly profile: Profile,
    private readonly runtime: Runtime,
  ) {}

  async handle(state: State, opcall: OpCall): Promise<State> {
    if (opcall.op === "shell.op") {
      if (process.env.OMEGA_ALLOW_UNSAFE_SHELL !== "1") {
        throw new Error(
          "E-SEC-SHELL-001 shell.op is disabled by default. " +
          "Re-run with --allow-unsafe-shell or set OMEGA_ALLOW_UNSAFE_SHELL=1.",
        );
      }
      capRequire(this.profile.caps, "shell", "shell.op");

      const cmdArg = opcall.args[0];
      if (!cmdArg || cmdArg.tag !== "Str") {
        throw new Error("shell.op: expected string command");
      }

      const command = cmdArg.s;
      const { execSync } = await import("child_process");
      try {
        // Use bash on Windows (Git Bash) so cat/grep/head/etc. work
        const shellOpts = process.platform === "win32"
          ? { shell: "bash" as const }
          : {};
        const stdout = execSync(command, {
          encoding: "utf-8",
          maxBuffer: 10 * 1024 * 1024,
          timeout: 300000,
          ...shellOpts,
        });
        return opcall.resumption.invoke({ tag: "Str", s: stdout.trim() } as Val) as State;
      } catch (e: any) {
        const stderr = e.stderr?.toString?.() ?? "";
        const message = e.message ?? "command failed";
        throw new Error(`shell.op failed: ${message}\n${stderr}`);
      }
    }

    if (opcall.op === "file.read.op") {
      capRequire(this.profile.caps, "file.read", "file.read.op");

      const pathArg = opcall.args[0];
      if (!pathArg || pathArg.tag !== "Str") {
        throw new Error("file.read.op: expected string path");
      }

      const { readFileSync } = await import("fs");
      try {
        const content = readFileSync(pathArg.s, "utf-8");
        return opcall.resumption.invoke({ tag: "Str", s: content } as Val) as State;
      } catch (e: any) {
        throw new Error(`file.read.op failed: ${e.message}`);
      }
    }

    if (opcall.op === "file.write.op") {
      capRequire(this.profile.caps, "file.write", "file.write.op");

      const pathArg = opcall.args[0];
      const contentArg = opcall.args[1];
      if (!pathArg || pathArg.tag !== "Str") {
        throw new Error("file.write.op: expected string path as first arg");
      }
      if (!contentArg || contentArg.tag !== "Str") {
        throw new Error("file.write.op: expected string content as second arg");
      }

      const { writeFileSync } = await import("fs");
      try {
        writeFileSync(pathArg.s, contentArg.s, "utf-8");
        return opcall.resumption.invoke({ tag: "Unit" } as Val) as State;
      } catch (e: any) {
        throw new Error(`file.write.op failed: ${e.message}`);
      }
    }

    if (opcall.op === "build.run.op") {
      capRequire(this.profile.caps, "shell", "build.run.op");

      const cwdArg = opcall.args[0];
      const cwd = cwdArg?.tag === "Str" ? cwdArg.s : process.cwd();

      const { runBuild, buildResultToVal } = await import("../../../package-bridge/runtime-backends");
      try {
        const result = await runBuild(cwd);
        const valResult = buildResultToVal(result);
        return opcall.resumption.invoke(valResult as Val) as State;
      } catch (e: any) {
        throw new Error(`build.run.op failed: ${e.message}`);
      }
    }

    if (opcall.op === "eval.run.op" || opcall.op === "callback.eval_lisp") {
      let code: string | undefined;
      let maxSteps: number | undefined;

      if (opcall.op === "eval.run.op") {
        const codeArg = opcall.args[0];
        if (!codeArg || codeArg.tag !== "Str") {
          throw new Error("eval.run.op: expected string code as first arg");
        }
        code = codeArg.s;
        const maxStepsArg = opcall.args[1];
        maxSteps = maxStepsArg?.tag === "Num" ? maxStepsArg.n : undefined;
      } else {
        const payload = opcall.args[0];
        if (payload?.tag === "Str") {
          code = payload.s;
        } else {
          const codeVal = mapGet(payload, ":code") ?? mapGet(payload, "code");
          if (codeVal?.tag === "Str") code = codeVal.s;
          maxSteps = getFirstNumber([
            mapGet(payload, ":max-steps"),
            mapGet(payload, "max-steps"),
            mapGet(payload, ":maxSteps"),
            mapGet(payload, "maxSteps"),
            mapGet(payload, ":depth-limit"),
            mapGet(payload, "depth-limit"),
            mapGet(payload, ":depth"),
            mapGet(payload, "depth"),
          ]);
        }
        if (!code) {
          throw new Error("callback.eval_lisp: expected payload with string :code");
        }
      }

      try {
        const expr = compileTextToExpr(code);
        const inner: State = {
          control: { tag: "Expr", e: expr },
          env: state.env,
          store: state.store,
          kont: [],
          handlers: state.handlers,
          profile: state.profile,
          budget: state.budget,
          sec: state.sec,
          provenanceGraph: state.provenanceGraph,
          provenanceStore: state.provenanceStore,
          provenanceSourceChecker: state.provenanceSourceChecker,
        };

        const { value, state: innerFinal } = await runToCompletionWithState(
          this.runtime,
          inner,
          maxSteps ? { maxSteps } : {},
        );

        const resumed = opcall.resumption.invoke(value) as State;
        return {
          ...resumed,
          env: innerFinal.env,
          store: innerFinal.store,
          handlers: innerFinal.handlers,
          profile: innerFinal.profile,
          budget: innerFinal.budget,
          sec: innerFinal.sec,
          provenanceGraph: innerFinal.provenanceGraph,
          provenanceStore: innerFinal.provenanceStore,
          provenanceSourceChecker: innerFinal.provenanceSourceChecker,
        };
      } catch (e: any) {
        if (opcall.op === "callback.eval_lisp") {
          throw new Error(`callback.eval_lisp failed: ${e.message}`);
        }
        throw new Error(`eval.run.op failed: ${e.message}`);
      }
    }

    throw new Error(`SystemIOBackend: unhandled op ${opcall.op}`);
  }
}
