import { emitArtifact } from "./artifacts.js";
import { getFailureCollectionSnapshot, group } from "./checks.js";

const DEFAULT_SCENARIO_SEED = "default";

export function createScenarioRuntime(options = {}) {
  const suiteSeed = normalizeSeed(options.seed);
  const state = {
    seed: suiteSeed,
    scenarioName: null,
    choices: {},
    notes: {},
    resources: [],
    resourceState: new Map(),
    steps: [],
  };

  return {
    get seed() {
      return state.seed;
    },
    get scenarioName() {
      return state.scenarioName;
    },
    resource(name, factory, options = {}) {
      return createScenarioResource(state, name, factory, options);
    },
    pick(name, choices) {
      return pickChoice(state, name, choices);
    },
    maybe(name, probability = 0.5) {
      return maybeChoice(state, name, probability);
    },
    choose(name, shapeOrChoices) {
      return chooseScenario(state, name, shapeOrChoices);
    },
    note(name, value) {
      const normalizedName = normalizeLabel(name, "note");
      state.notes[normalizedName] = cloneForArtifact(value);
      return value;
    },
    step(name, fn) {
      return runScenarioStep(state, name, fn);
    },
    emitArtifact() {
      emitScenarioArtifact(state);
    },
    snapshot() {
      return buildScenarioArtifactPayload(state);
    },
  };
}

function createScenarioResource(state, name, factory, options = {}) {
  const normalizedName = normalizeLabel(name, "resource");
  if (typeof factory !== "function") {
    throw new Error(`scenario.resource("${normalizedName}") requires a factory function`);
  }

  const scope = normalizeResourceScope(options.scope);
  if (!state.resources.some((entry) => entry.name === normalizedName)) {
    state.resources.push({ name: normalizedName, scope });
  }

  return {
    get() {
      if (scope === "step") {
        return factory();
      }

      if (state.resourceState.has(normalizedName)) {
        return state.resourceState.get(normalizedName);
      }

      const value = factory();
      state.resourceState.set(normalizedName, value);
      return value;
    },
  };
}

function pickChoice(state, name, choices) {
  const normalizedName = normalizeLabel(name, "choice");
  const values = Array.isArray(choices) ? choices : [];
  if (values.length === 0) {
    throw new Error(`scenario.pick("${normalizedName}") requires at least one choice`);
  }

  const index = chooseIndex(state.seed, state.scenarioName || "scenario", normalizedName, values.length);
  const value = values[index];
  state.choices[normalizedName] = cloneForArtifact(value);
  return value;
}

function maybeChoice(state, name, probability = 0.5) {
  const normalizedName = normalizeLabel(name, "choice");
  const numericProbability = Number(probability);
  if (!Number.isFinite(numericProbability) || numericProbability < 0 || numericProbability > 1) {
    throw new Error(
      `scenario.maybe("${normalizedName}") probability must be between 0 and 1`
    );
  }

  const ratio = chooseRatio(state.seed, state.scenarioName || "scenario", normalizedName);
  const value = ratio < numericProbability;
  state.choices[normalizedName] = value;
  return value;
}

function chooseScenario(state, name, shapeOrChoices) {
  const normalizedName = normalizeLabel(name, "scenario");
  state.scenarioName = normalizedName;

  if (Array.isArray(shapeOrChoices)) {
    const selected = pickChoice(state, `${normalizedName}:variant`, shapeOrChoices);
    return selected;
  }

  if (!shapeOrChoices || typeof shapeOrChoices !== "object") {
    throw new Error(`scenario.choose("${normalizedName}") requires an object or array`);
  }

  return shapeOrChoices;
}

function runScenarioStep(state, name, fn) {
  const stepName = normalizeLabel(name, "unnamed step");
  if (typeof fn !== "function") {
    throw new Error(`scenario.step("${stepName}") requires a function`);
  }

  const entry = {
    name: stepName,
    startedAt: new Date().toISOString(),
    durationMs: 0,
    status: "passed",
  };
  state.steps.push(entry);

  const startedAt = Date.now();
  const before = getFailureCollectionSnapshot();

  try {
    return group(stepName, () => fn());
  } catch (error) {
    entry.status = "failed";
    entry.error = error instanceof Error ? error.message : String(error);
    throw error;
  } finally {
    const after = getFailureCollectionSnapshot();
    entry.durationMs = Date.now() - startedAt;
    entry.finishedAt = new Date().toISOString();
    if (entry.status !== "failed" && after.failureCount > before.failureCount) {
      entry.status = "failed";
      entry.failureCount = after.failureCount - before.failureCount;
    } else if (after.failureCount > before.failureCount) {
      entry.failureCount = after.failureCount - before.failureCount;
    }
  }
}

function emitScenarioArtifact(state) {
  const payload = buildScenarioArtifactPayload(state);
  emitArtifact("scenario", payload, {
    kind: "testkit.scenario",
    summary: buildScenarioSummary(payload),
  });
}

function buildScenarioArtifactPayload(state) {
  return {
    schemaVersion: 1,
    seed: state.seed,
    scenarioName: state.scenarioName,
    choices: cloneForArtifact(state.choices),
    notes: cloneForArtifact(state.notes),
    resources: state.resources.map((entry) => ({ ...entry })),
    steps: state.steps.map((entry) => ({ ...entry })),
  };
}

function buildScenarioSummary(payload) {
  const failedStep = payload.steps.find((entry) => entry.status === "failed");
  if (failedStep) {
    return `${payload.scenarioName || "scenario"} seed=${payload.seed} failed at ${failedStep.name}`;
  }
  return `${payload.scenarioName || "scenario"} seed=${payload.seed} (${payload.steps.length} steps)`;
}

function chooseIndex(seed, scenarioName, choiceName, length) {
  if (length <= 1) return 0;
  const value = hashString(`${seed}:${scenarioName}:${choiceName}`);
  return value % length;
}

function chooseRatio(seed, scenarioName, choiceName) {
  const value = hashString(`${seed}:${scenarioName}:${choiceName}`);
  return value / 0xffffffff;
}

function hashString(input) {
  let hash = 2166136261;
  const source = String(input);
  for (let index = 0; index < source.length; index += 1) {
    hash ^= source.charCodeAt(index);
    hash = Math.imul(hash, 16777619);
  }
  return hash >>> 0;
}

function normalizeSeed(value) {
  if (value === undefined || value === null) return DEFAULT_SCENARIO_SEED;
  const normalized = String(value).trim();
  return normalized.length > 0 ? normalized : DEFAULT_SCENARIO_SEED;
}

function normalizeLabel(value, fallback) {
  if (typeof value !== "string") return fallback;
  const normalized = value.trim();
  return normalized.length > 0 ? normalized : fallback;
}

function normalizeResourceScope(value) {
  const normalized = normalizeLabel(value, "scenario");
  if (normalized === "file") return "file";
  if (normalized === "scenario") return "scenario";
  if (normalized === "step") return "step";
  throw new Error(`Unsupported scenario resource scope "${value}"`);
}

function cloneForArtifact(value) {
  if (value === undefined) return null;
  return JSON.parse(JSON.stringify(value));
}
