import type { Env } from "../../../src/core/eval/env";
import { envEmpty, envSet } from "../../../src/core/eval/env";
import type { Store } from "../../../src/core/eval/store";
import type { State, Frame, StepOutcome } from "../../../src/core/eval/machine";
import type { Val } from "../../../src/core/eval/values";
import { VUnit, VTrue, VFalse } from "../../../src/core/eval/values";
import { registerConditionPrims } from "../../../src/core/conditions/prims";

function deepEqual(a: Val, b: Val): boolean {
  if (a.tag !== b.tag) return false;
  switch (a.tag) {
    case "Unit":
      return true;
    case "Num":
      return (b as { n: number }).n === a.n;
    case "Bool":
      return (b as { b: boolean }).b === a.b;
    case "Str":
      return (b as { s: string }).s === a.s;
    case "Sym":
      return (b as { name: string }).name === a.name;
    case "Vector": {
      const other = b as { items: Val[] };
      if (other.items.length !== a.items.length) return false;
      for (let i = 0; i < a.items.length; i++) {
        if (!deepEqual(a.items[i], other.items[i])) return false;
      }
      return true;
    }
    default:
      return JSON.stringify(a) === JSON.stringify(b);
  }
}

function listToArray(value: Val): Val[] {
  const items: Val[] = [];
  let current = value;
  while (current.tag === "Vector" && current.items.length >= 2) {
    items.push(current.items[0]);
    current = current.items[1];
  }
  return items;
}

function arrayToList(items: Val[]): Val {
  let result: Val = VUnit;
  for (let i = items.length - 1; i >= 0; i--) {
    result = { tag: "Vector", items: [items[i], result] };
  }
  return result;
}

export function installMembranePrims(store: Store): { env: Env; store: Store } {
  let env: Env = envEmpty();
  let st: Store = store;

  function def(name: string, v: Val) {
    const [nextStore, addr] = st.alloc(v);
    st = nextStore;
    env = envSet(env, name, addr);
  }

  function isCallable(proc: Val): proc is Val {
    return proc.tag === "Native" || proc.tag === "Closure";
  }

  function ensureArity(proc: Val, expected: number, name: string): void {
    if (proc.tag === "Native") {
      if (proc.arity !== "variadic" && proc.arity !== expected) {
        throw new Error(`${name}: expected procedure of arity ${expected}`);
      }
      return;
    }
    if (proc.tag === "Closure") {
      if (proc.params.length !== expected) {
        throw new Error(`${name}: expected procedure of arity ${expected}`);
      }
      return;
    }
    throw new Error(`${name}: expected a procedure`);
  }

  function applyProcedure(proc: Val, procArgs: Val[], state: State): State | StepOutcome {
    if (proc.tag === "Native") {
      return proc.fn(procArgs, state);
    }
    if (proc.tag === "Closure") {
      if (proc.params.length !== procArgs.length) {
        throw new Error(`procedure arity mismatch: expected ${proc.params.length}, got ${procArgs.length}`);
      }
      let nextStore = state.store;
      let nextEnv = proc.env;
      for (let i = 0; i < proc.params.length; i++) {
        const [allocatedStore, addr] = nextStore.alloc(procArgs[i]);
        nextStore = allocatedStore;
        nextEnv = envSet(nextEnv, proc.params[i], addr);
      }
      const kont = state.kont.concat([{ tag: "KCall", savedEnv: state.env } as Frame]);
      return { ...state, control: { tag: "Expr", e: proc.body }, env: nextEnv, store: nextStore, kont };
    }
    throw new Error("expected procedure");
  }

  function applyNative(proc: Val, procArgs: Val[], state: State): Val | null {
    if (proc.tag !== "Native") {
      return null;
    }
    const result = proc.fn(procArgs, state);
    if ("control" in result && result.control.tag === "Val") {
      return result.control.v;
    }
    return null;
  }

  registerConditionPrims(def, { applyProcedure, ensureArity, isCallable });

  def("*uninit*", { tag: "Native", name: "*uninit*", arity: 0, fn: (_args, s) => {
    return { ...s, control: { tag: "Val", v: VUnit } };
  }});

  def("+", { tag: "Native", name: "+", arity: "variadic", fn: (args, s) => {
    return { ...s, control: { tag: "Val", v: { tag: "Num", n: args.reduce((acc, value) => acc + (value as { n: number }).n, 0) } } };
  }});

  def("-", { tag: "Native", name: "-", arity: "variadic", fn: (args, s) => {
    const nums = args.map((value) => (value as { n: number }).n);
    const n = nums.length === 1 ? -nums[0] : nums.slice(1).reduce((acc, value) => acc - value, nums[0]);
    return { ...s, control: { tag: "Val", v: { tag: "Num", n } } };
  }});

  def("*", { tag: "Native", name: "*", arity: "variadic", fn: (args, s) => {
    return { ...s, control: { tag: "Val", v: { tag: "Num", n: args.reduce((acc, value) => acc * (value as { n: number }).n, 1) } } };
  }});

  def("=", { tag: "Native", name: "=", arity: 2, fn: (args, s) => {
    return { ...s, control: { tag: "Val", v: (args[0] as { n: number }).n === (args[1] as { n: number }).n ? VTrue : VFalse } };
  }});

  def("<", { tag: "Native", name: "<", arity: 2, fn: (args, s) => {
    return { ...s, control: { tag: "Val", v: (args[0] as { n: number }).n < (args[1] as { n: number }).n ? VTrue : VFalse } };
  }});

  def(">", { tag: "Native", name: ">", arity: 2, fn: (args, s) => {
    return { ...s, control: { tag: "Val", v: (args[0] as { n: number }).n > (args[1] as { n: number }).n ? VTrue : VFalse } };
  }});

  def("<=", { tag: "Native", name: "<=", arity: 2, fn: (args, s) => {
    return { ...s, control: { tag: "Val", v: (args[0] as { n: number }).n <= (args[1] as { n: number }).n ? VTrue : VFalse } };
  }});

  def(">=", { tag: "Native", name: ">=", arity: 2, fn: (args, s) => {
    return { ...s, control: { tag: "Val", v: (args[0] as { n: number }).n >= (args[1] as { n: number }).n ? VTrue : VFalse } };
  }});

  def("/", { tag: "Native", name: "/", arity: 2, fn: (args, s) => {
    return { ...s, control: { tag: "Val", v: { tag: "Num", n: (args[0] as { n: number }).n / (args[1] as { n: number }).n } } };
  }});

  def("modulo", { tag: "Native", name: "modulo", arity: 2, fn: (args, s) => {
    return { ...s, control: { tag: "Val", v: { tag: "Num", n: (args[0] as { n: number }).n % (args[1] as { n: number }).n } } };
  }});

  def("not", { tag: "Native", name: "not", arity: 1, fn: (args, s) => {
    const value = args[0];
    const isFalsey = (value.tag === "Bool" && !value.b) || value.tag === "Unit";
    return { ...s, control: { tag: "Val", v: isFalsey ? VTrue : VFalse } };
  }});

  def("or", { tag: "Native", name: "or", arity: "variadic", fn: (args, s) => {
    for (const arg of args) {
      const truthy = !(arg.tag === "Bool" && !arg.b) && arg.tag !== "Unit";
      if (truthy) {
        return { ...s, control: { tag: "Val", v: arg } };
      }
    }
    return { ...s, control: { tag: "Val", v: VFalse } };
  }});

  def("and", { tag: "Native", name: "and", arity: "variadic", fn: (args, s) => {
    let last: Val = VTrue;
    for (const arg of args) {
      const truthy = !(arg.tag === "Bool" && !arg.b) && arg.tag !== "Unit";
      if (!truthy) {
        return { ...s, control: { tag: "Val", v: arg } };
      }
      last = arg;
    }
    return { ...s, control: { tag: "Val", v: last } };
  }});

  def("cons", { tag: "Native", name: "cons", arity: 2, fn: (args, s) => {
    return { ...s, control: { tag: "Val", v: { tag: "Vector", items: [args[0], args[1]] } } };
  }});

  def("car", { tag: "Native", name: "car", arity: 1, fn: (args, s) => {
    const value = args[0] as { tag: string; items?: Val[] };
    if (value.tag === "Vector" && value.items && value.items.length >= 1) {
      return { ...s, control: { tag: "Val", v: value.items[0] } };
    }
    throw new Error("car: expected pair");
  }});

  def("cdr", { tag: "Native", name: "cdr", arity: 1, fn: (args, s) => {
    const value = args[0] as { tag: string; items?: Val[] };
    if (value.tag === "Vector" && value.items && value.items.length >= 2) {
      return { ...s, control: { tag: "Val", v: value.items[1] } };
    }
    throw new Error("cdr: expected pair");
  }});

  def("null?", { tag: "Native", name: "null?", arity: 1, fn: (args, s) => {
    const value = args[0] as { tag: string; items?: Val[] };
    const isNull = value.tag === "Unit" || (value.tag === "Vector" && value.items?.length === 0);
    return { ...s, control: { tag: "Val", v: isNull ? VTrue : VFalse } };
  }});

  def("pair?", { tag: "Native", name: "pair?", arity: 1, fn: (args, s) => {
    const value = args[0] as { tag: string; items?: Val[] };
    const isPair = value.tag === "Vector" && (value.items?.length ?? 0) >= 2;
    return { ...s, control: { tag: "Val", v: isPair ? VTrue : VFalse } };
  }});

  def("list?", { tag: "Native", name: "list?", arity: 1, fn: (args, s) => {
    const value = args[0] as { tag: string; items?: Val[] };
    const isNull = value.tag === "Unit" || (value.tag === "Vector" && value.items?.length === 0);
    const isPair = value.tag === "Vector" && (value.items?.length ?? 0) >= 2;
    return { ...s, control: { tag: "Val", v: isNull || isPair ? VTrue : VFalse } };
  }});

  def("list", { tag: "Native", name: "list", arity: "variadic", fn: (args, s) => {
    return { ...s, control: { tag: "Val", v: arrayToList(args) } };
  }});

  def("reverse", { tag: "Native", name: "reverse", arity: 1, fn: (args, s) => {
    const items = listToArray(args[0]);
    let result: Val = VUnit;
    for (const item of items) {
      result = { tag: "Vector", items: [item, result] };
    }
    return { ...s, control: { tag: "Val", v: result } };
  }});

  def("string-length", { tag: "Native", name: "string-length", arity: 1, fn: (args, s) => {
    const value = args[0] as { tag: string; s?: string };
    if (value.tag !== "Str") throw new Error("string-length: expected string");
    return { ...s, control: { tag: "Val", v: { tag: "Num", n: value.s!.length } } };
  }});

  def("string-append", { tag: "Native", name: "string-append", arity: "variadic", fn: (args, s) => {
    let result = "";
    for (const arg of args) {
      if (arg.tag !== "Str") throw new Error("string-append: expected strings");
      result += arg.s;
    }
    return { ...s, control: { tag: "Val", v: { tag: "Str", s: result } } };
  }});

  def("substring", { tag: "Native", name: "substring", arity: 3, fn: (args, s) => {
    const str = args[0];
    const start = args[1];
    const end = args[2];
    if (str.tag !== "Str") throw new Error("substring: expected string");
    if (start.tag !== "Num" || end.tag !== "Num") throw new Error("substring: expected numbers for indices");
    return { ...s, control: { tag: "Val", v: { tag: "Str", s: str.s.substring(start.n, end.n) } } };
  }});

  def("get", { tag: "Native", name: "get", arity: "variadic", fn: (args, s) => {
    if (args.length < 2 || args.length > 3) throw new Error("get: expected (get coll key) or (get coll key default)");
    const coll = args[0];
    const key = args[1];
    const defaultVal: Val = args.length === 3 ? args[2] : VUnit;

    if (coll.tag === "Map") {
      for (const [entryKey, entryValue] of coll.entries) {
        if (deepEqual(entryKey, key)) {
          return { ...s, control: { tag: "Val", v: entryValue } };
        }
      }
      return { ...s, control: { tag: "Val", v: defaultVal } };
    }

    if (coll.tag === "Vector" && key.tag === "Num") {
      if (key.n >= 0 && key.n < coll.items.length) {
        return { ...s, control: { tag: "Val", v: coll.items[key.n] } };
      }
      return { ...s, control: { tag: "Val", v: defaultVal } };
    }

    return { ...s, control: { tag: "Val", v: defaultVal } };
  }});

  def("equal?", { tag: "Native", name: "equal?", arity: 2, fn: (args, s) => {
    return { ...s, control: { tag: "Val", v: deepEqual(args[0], args[1]) ? VTrue : VFalse } };
  }});

  def("map", { tag: "Native", name: "map", arity: 2, fn: (args, s) => {
    const proc = args[0];
    const items = listToArray(args[1]);

    if (items.length === 0) {
      return { ...s, control: { tag: "Val", v: VUnit } };
    }

    if (proc.tag === "Native") {
      const results: Val[] = [];
      for (const item of items) {
        const result = applyNative(proc, [item], s);
        if (result === null) throw new Error("map: Native function did not return a value");
        results.push(result);
      }
      return { ...s, control: { tag: "Val", v: arrayToList(results) } };
    }

    if (proc.tag === "Closure") {
      const [first, ...rest] = items;
      const frame: Frame = { tag: "KMapRest", fn: proc, remaining: rest, acc: [], env: s.env };
      return applyProcedure(proc, [first], { ...s, kont: [...s.kont, frame] });
    }

    throw new Error("map: expected procedure");
  }});

  def("filter", { tag: "Native", name: "filter", arity: 2, fn: (args, s) => {
    const proc = args[0];
    const items = listToArray(args[1]);

    if (items.length === 0) {
      return { ...s, control: { tag: "Val", v: VUnit } };
    }

    if (proc.tag === "Native") {
      const results: Val[] = [];
      for (const item of items) {
        const result = applyNative(proc, [item], s);
        if (result === null) throw new Error("filter: Native function did not return a value");
        if ((result.tag === "Bool" && result.b) || (result.tag !== "Bool" && result.tag !== "Unit")) {
          results.push(item);
        }
      }
      return { ...s, control: { tag: "Val", v: arrayToList(results) } };
    }

    if (proc.tag === "Closure") {
      const [first, ...rest] = items;
      const frame: Frame = { tag: "KFilterRest", fn: proc, remaining: rest, currentItem: first, acc: [], env: s.env };
      return applyProcedure(proc, [first], { ...s, kont: [...s.kont, frame] });
    }

    throw new Error("filter: expected procedure");
  }});

  return { env, store: st };
}
