import type { IoDomain, IoFlowInstruction, IoReference, IoWriteInstruction, PulseInstruction, WaitInstruction } from "../grl/ir/index.js";
import type { MotionDiagnostic } from "../kdl/types.js";

export type IoWriter = "program" | "user" | "script";

export interface IoPermissionRule {
  writer: IoWriter;
  domains: IoDomain[];
  access: "read" | "write" | "readwrite";
}

export interface IoEvent {
  id: number;
  time: number;
  writer: IoWriter;
  target: IoReference;
  previous: boolean | number | string | undefined;
  value: boolean | number | string;
  kind: "write" | "pulse_set" | "pulse_reset" | "script";
}

export interface IoScriptStep {
  delay: number;
  target: IoReference;
  value: boolean | number | string;
}

export interface IoScriptExecution {
  name: string;
  nextIndex: number;
  elapsed: number;
  steps: IoScriptStep[];
  done: boolean;
}

export type WaitStatus = "satisfied" | "waiting" | "timeout" | "hold-stop";

export interface WaitEvaluation {
  status: WaitStatus;
  elapsed: number;
  instruction: WaitInstruction;
  diagnostic?: MotionDiagnostic;
  onTimeout?: WaitInstruction["onTimeout"];
}

export interface IoRuntimeSnapshot {
  time: number;
  image: Record<string, boolean | number | string>;
  events: IoEvent[];
  diagnostics: MotionDiagnostic[];
  scripts: IoScriptExecution[];
}

export interface IoRuntimeOptions {
  permissions?: IoPermissionRule[];
}

interface PulseSchedule {
  dueTime: number;
  target: IoReference;
  writer: IoWriter;
}

export class IoImageRuntime {
  private readonly image = new Map<string, boolean | number | string>();
  private readonly previousImage = new Map<string, boolean | number | string>();
  private readonly eventsValue: IoEvent[] = [];
  private readonly diagnosticsValue: MotionDiagnostic[] = [];
  private readonly pulses: PulseSchedule[] = [];
  private readonly scriptsValue: IoScriptExecution[] = [];
  private nextEventId = 1;
  private timeValue = 0;

  constructor(private readonly options: IoRuntimeOptions = {}) {}

  get time(): number {
    return this.timeValue;
  }

  get events(): IoEvent[] {
    return this.eventsValue.map((event) => ({ ...event, target: { ...event.target } }));
  }

  get diagnostics(): MotionDiagnostic[] {
    return [...this.diagnosticsValue];
  }

  read(reference: IoReference): boolean | number | string | undefined {
    return this.image.get(ioKey(reference));
  }

  write(
    reference: IoReference,
    value: boolean | number | string,
    writer: IoWriter = "program",
    kind: IoEvent["kind"] = "write"
  ): IoEvent | undefined {
    if (!this.canWrite(reference, writer)) {
      this.diagnosticsValue.push({
        severity: "error",
        code: "VC_IO_PERMISSION_DENIED",
        message: `${writer} cannot write ${reference.raw}`,
        data: { writer, target: reference }
      });
      return undefined;
    }
    const key = ioKey(reference);
    const previous = this.image.get(key);
    if (previous === undefined) {
      this.previousImage.delete(key);
    } else {
      this.previousImage.set(key, previous);
    }
    this.image.set(key, value);
    const event: IoEvent = {
      id: this.nextEventId,
      time: this.timeValue,
      writer,
      target: { ...reference },
      previous,
      value,
      kind
    };
    this.nextEventId += 1;
    this.eventsValue.push(event);
    return event;
  }

  executeInstruction(instruction: IoFlowInstruction, writer: IoWriter = "program"): WaitEvaluation | IoEvent[] | undefined {
    if (instruction.kind === "IO_WRITE") {
      const event = this.executeWrite(instruction, writer);
      return event ? [event] : [];
    }
    if (instruction.kind === "PULSE") {
      return this.executePulse(instruction, writer);
    }
    return this.evaluateWait(instruction, 0);
  }

  executeWrite(instruction: IoWriteInstruction, writer: IoWriter = "program"): IoEvent | undefined {
    return this.write(instruction.target, instruction.value, writer);
  }

  executePulse(instruction: PulseInstruction, writer: IoWriter = "program"): IoEvent[] {
    const events: IoEvent[] = [];
    const setEvent = this.write(instruction.target, true, writer, "pulse_set");
    if (setEvent) {
      events.push(setEvent);
      this.pulses.push({
        dueTime: this.timeValue + instruction.duration,
        target: instruction.target,
        writer
      });
    }
    return events;
  }

  evaluateWait(instruction: WaitInstruction, elapsed: number): WaitEvaluation {
    if (this.matchesCondition(instruction.condition)) {
      return {
        status: "satisfied",
        elapsed,
        instruction
      };
    }
    if (instruction.timeout !== undefined && elapsed >= instruction.timeout) {
      const diagnostic: MotionDiagnostic = {
        severity: instruction.onTimeout ? "warning" : "error",
        code: "VC_WAIT_TIMEOUT",
        message: `Wait timed out after ${instruction.timeout}s`,
        ...(instruction.sourceMap ? { sourceMap: instruction.sourceMap } : {}),
        data: { condition: instruction.condition }
      };
      this.diagnosticsValue.push(diagnostic);
      return {
        status: instruction.onTimeout?.kind === "call" ? "hold-stop" : "timeout",
        elapsed,
        instruction,
        diagnostic,
        ...(instruction.onTimeout ? { onTimeout: instruction.onTimeout } : {})
      };
    }
    return {
      status: "waiting",
      elapsed,
      instruction
    };
  }

  addScript(name: string, steps: IoScriptStep[]): IoScriptExecution {
    const script: IoScriptExecution = {
      name,
      nextIndex: 0,
      elapsed: 0,
      steps: steps.map((step) => ({ ...step, target: { ...step.target } })),
      done: steps.length === 0
    };
    this.scriptsValue.push(script);
    return { ...script, steps: script.steps.map((step) => ({ ...step, target: { ...step.target } })) };
  }

  advance(deltaTime: number): void {
    if (!Number.isFinite(deltaTime) || deltaTime < 0) {
      this.diagnosticsValue.push({
        severity: "error",
        code: "VC_IO_TIME_INVALID",
        message: "IO runtime delta time must be a finite non-negative number"
      });
      return;
    }
    this.timeValue += deltaTime;
    this.flushPulses();
    this.advanceScripts(deltaTime);
  }

  matchesCondition(condition: string): boolean {
    const normalized = normalizeCondition(condition);
    const allMatch = unwrapCall(normalized, "all");
    if (allMatch) {
      return splitTopLevel(allMatch).every((part) => this.matchesCondition(part));
    }
    const anyMatch = unwrapCall(normalized, "any");
    if (anyMatch) {
      return splitTopLevel(anyMatch).some((part) => this.matchesCondition(part));
    }
    const rising = unwrapCall(normalized, "rising");
    if (rising) {
      const reference = parseIoReferenceText(rising);
      return Boolean(reference && this.previousValue(reference) !== true && this.read(reference) === true);
    }
    const falling = unwrapCall(normalized, "falling");
    if (falling) {
      const reference = parseIoReferenceText(falling);
      return Boolean(reference && this.previousValue(reference) === true && this.read(reference) !== true);
    }
    const changed = unwrapCall(normalized, "changed");
    if (changed) {
      const reference = parseIoReferenceText(changed);
      return Boolean(reference && this.previousValue(reference) !== this.read(reference));
    }

    const comparison = normalized.match(/^(io\.[A-Za-z]+\[\d+\]|io\.alias\.[A-Za-z_][A-Za-z0-9_]*)\s*(==|!=|>=|<=|>|<)\s*(.+)$/);
    if (!comparison) {
      return false;
    }
    const reference = parseIoReferenceText(comparison[1]!);
    if (!reference) {
      return false;
    }
    const left = this.read(reference);
    const right = parseConditionValue(comparison[3]!);
    switch (comparison[2]) {
      case "==":
        return left === right;
      case "!=":
        return left !== right;
      case ">=":
        return Number(left) >= Number(right);
      case "<=":
        return Number(left) <= Number(right);
      case ">":
        return Number(left) > Number(right);
      case "<":
        return Number(left) < Number(right);
      default:
        return false;
    }
  }

  snapshot(): IoRuntimeSnapshot {
    const image: Record<string, boolean | number | string> = {};
    for (const [key, value] of this.image.entries()) {
      image[key] = value;
    }
    return {
      time: this.timeValue,
      image,
      events: this.events,
      diagnostics: this.diagnostics,
      scripts: this.scriptsValue.map((script) => ({
        ...script,
        steps: script.steps.map((step) => ({ ...step, target: { ...step.target } }))
      }))
    };
  }

  private canWrite(reference: IoReference, writer: IoWriter): boolean {
    const rules = this.options.permissions;
    if (!rules || rules.length === 0) {
      return true;
    }
    return rules.some((rule) =>
      rule.writer === writer &&
      rule.domains.includes(reference.domain) &&
      (rule.access === "write" || rule.access === "readwrite")
    );
  }

  private previousValue(reference: IoReference): boolean | number | string | undefined {
    return this.previousImage.get(ioKey(reference));
  }

  private flushPulses(): void {
    const due = this.pulses.filter((pulse) => pulse.dueTime <= this.timeValue);
    for (const pulse of due) {
      this.write(pulse.target, false, pulse.writer, "pulse_reset");
    }
    for (const pulse of due) {
      const index = this.pulses.indexOf(pulse);
      if (index >= 0) {
        this.pulses.splice(index, 1);
      }
    }
  }

  private advanceScripts(deltaTime: number): void {
    for (const script of this.scriptsValue) {
      if (script.done) {
        continue;
      }
      script.elapsed += deltaTime;
      while (script.nextIndex < script.steps.length) {
        const step = script.steps[script.nextIndex]!;
        if (script.elapsed + 1e-12 < step.delay) {
          break;
        }
        this.write(step.target, step.value, "script", "script");
        script.nextIndex += 1;
      }
      script.done = script.nextIndex >= script.steps.length;
    }
  }
}

export function ioKey(reference: IoReference): string {
  if (reference.domain === "alias") {
    return `io.alias.${reference.alias ?? reference.raw}`;
  }
  return `io.${reference.domain}[${reference.index ?? 0}]`;
}

function normalizeCondition(condition: string): string {
  return condition
    .replace(/\s*\.\s*/g, ".")
    .replace(/\s*\[\s*/g, "[")
    .replace(/\s*\]\s*/g, "]")
    .replace(/\s*\(\s*/g, "(")
    .replace(/\s*\)\s*/g, ")")
    .replace(/\s*,\s*/g, ",")
    .replace(/\s+/g, " ")
    .trim();
}

function unwrapCall(text: string, name: string): string | undefined {
  const prefix = `${name}(`;
  return text.startsWith(prefix) && text.endsWith(")") ? text.slice(prefix.length, -1) : undefined;
}

function splitTopLevel(text: string): string[] {
  const parts: string[] = [];
  let depth = 0;
  let start = 0;
  for (let index = 0; index < text.length; index += 1) {
    const char = text[index];
    if (char === "(") {
      depth += 1;
    } else if (char === ")") {
      depth = Math.max(0, depth - 1);
    } else if (char === "," && depth === 0) {
      parts.push(text.slice(start, index));
      start = index + 1;
    }
  }
  parts.push(text.slice(start));
  return parts.map((part) => part.trim()).filter((part) => part.length > 0);
}

export function parseIoReferenceText(text: string): IoReference | undefined {
  const alias = text.match(/^io\.alias\.([A-Za-z_][A-Za-z0-9_]*)$/);
  if (alias) {
    return {
      domain: "alias",
      alias: alias[1]!,
      raw: text
    };
  }
  const indexed = text.match(/^io\.([A-Za-z]+)\[(\d+)\]$/);
  if (!indexed) {
    return undefined;
  }
  return {
    domain: indexed[1] as IoDomain,
    index: Number(indexed[2]),
    raw: text
  };
}

function parseConditionValue(text: string): boolean | number | string {
  const trimmed = text.trim();
  if (trimmed === "true") {
    return true;
  }
  if (trimmed === "false") {
    return false;
  }
  if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) {
    return Number(trimmed);
  }
  if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
    return trimmed.slice(1, -1);
  }
  return trimmed;
}
