// Deterministic strategy.
//
// This is the kernel default strategy:
// - no intercepted ops
// - step with stepOnce
// - dispatch all Op outcomes through runtime.dispatch

import type { State } from "../machine";
import type { Runtime } from "../runtime";
import type { Val } from "../values";
import { stepOnce } from "../machineStep";
import { envSet } from "../env";
import {
  type RunStrategy,
  type RunResult,
  type StepFn,
  type StrategyOptions,
  resolveStrategyOptions,
  runStatsFromSteps,
  valueResult,
  failResult,
} from "./contract";

export class DeterministicStrategy implements RunStrategy {
  readonly name = "deterministic";
  readonly description = "Kernel default: deterministic CESK step loop";
  readonly interceptedOps: readonly string[] = [];

  async run(
    runtime: Runtime,
    initial: State,
    stepFnOrOptions?: StepFn | StrategyOptions,
    maybeOptions?: StrategyOptions,
  ): Promise<RunResult> {
    const stepFn = typeof stepFnOrOptions === "function" ? stepFnOrOptions : stepOnce;
    const options = resolveStrategyOptions(stepFnOrOptions, maybeOptions);
    const maxSteps = options.maxSteps ?? 1_000_000;
    const timeoutMs = options.timeoutMs;
    const startedAt = Date.now();
    const collectTrace = options.trace ?? false;

    let effectCalls = 0;
    let st = initial;
    const trace: Array<{
      step: number;
      control: string;
      kontDepth: number;
      outcome: "state" | "done" | "op";
      op?: string;
      value?: string;
    }> = [];

    for (let i = 0; i < maxSteps; i++) {
      if (timeoutMs !== undefined && Date.now() - startedAt > timeoutMs) {
        return failResult(
          `strategy timeout after ${timeoutMs}ms`,
          st,
          runStatsFromSteps(i, {
            effectCalls,
            solutions: 0,
            elapsedMs: Date.now() - startedAt,
          }),
          { trace: collectTrace ? trace : undefined, meta: { deterministic: true } },
        );
      }

      options.budget?.consumeEvalStep();

      try {
        const out = stepFn(st);

        if (collectTrace) {
          trace.push({
            step: i,
            control: st.control.tag,
            kontDepth: st.kont.length,
            outcome: out.tag === "State" ? "state" : out.tag === "Done" ? "done" : "op",
            op: out.tag === "Op" ? out.opcall.op : undefined,
            value: out.tag === "Done" ? out.value.tag : undefined,
          });
        }

        if (out.tag === "State") {
          st = out.state;
          continue;
        }

        if (out.tag === "Done") {
          return valueResult(
            out.value,
            (out.state ?? st) as State,
            runStatsFromSteps(i + 1, {
              effectCalls,
              solutions: 1,
              elapsedMs: Date.now() - startedAt,
            }),
            {
              trace: collectTrace ? trace : undefined,
              meta: { deterministic: true },
            },
          );
        }

        if (out.tag === "Op") {
          effectCalls++;
          const handled = await runtime.dispatch(out.state, out.opcall);
          if (handled === "Uncaught") {
            const recovered = unwindToTryCatch(out.state, new Error(`Uncaught op: ${out.opcall.op}`));
            if (recovered) {
              st = recovered;
              continue;
            }
            return failResult(
              `Uncaught op: ${out.opcall.op}`,
              out.state,
              runStatsFromSteps(i + 1, {
                effectCalls,
                elapsedMs: Date.now() - startedAt,
              }),
              {
                trace: collectTrace ? trace : undefined,
                meta: { deterministic: true },
              },
            );
          }

          st = handled;
          continue;
        }
      } catch (err) {
        const recovered = unwindToTryCatch(st, err);
        if (recovered) {
          st = recovered;
          continue;
        }

        return failResult(
          err instanceof Error ? err.message : String(err),
          st,
          runStatsFromSteps(i + 1, {
            effectCalls,
            elapsedMs: Date.now() - startedAt,
          }),
          {
            trace: collectTrace ? trace : undefined,
            meta: { deterministic: true },
          },
        );
      }
    }

    return failResult(
      `step budget exceeded: ${maxSteps}`,
      st,
      runStatsFromSteps(maxSteps, {
        effectCalls,
        elapsedMs: Date.now() - startedAt,
      }),
      {
        trace: collectTrace ? trace : undefined,
        meta: { deterministic: true },
      },
    );
  }
}

function unwindToTryCatch(st: State, error: unknown): State | null {
  for (let i = st.kont.length - 1; i >= 0; i--) {
    const fr = st.kont[i] as any;
    if (fr.tag === "KTryCatch") {
      const errMsg = error instanceof Error ? error.message : String(error);
      const errVal: Val = { tag: "Str", s: errMsg };
      const [store2, addr] = st.store.alloc(errVal);
      const env2 = envSet(fr.env, fr.errVar, addr);
      return {
        ...st,
        control: { tag: "Expr", e: fr.handler },
        env: env2,
        store: store2,
        kont: fr.savedKont,
      };
    }
  }
  return null;
}
