/**
 * MembraneEffectBackend — handles membrane.* effects from CESK programs.
 *
 * Effects: membrane.allow, membrane.deny, membrane.mutate, membrane.trace,
 *          membrane.log, membrane.rate-check
 *
 * Uses a sink/collector pattern: effects set the result on a shared collector,
 * which the MembraneStepper reads after the program completes.
 */
import type { EffectBackend } from '@open-matrix/omega-core';
import type { State, Val, OpCall } from '@open-matrix/omega-core';

// ─── Result types ────────────────────────────────────────────────────────────

export type MembraneAction = 'allow' | 'deny' | 'mutate';

export interface MembraneResult {
  action: MembraneAction;
  payload?: unknown;       // mutated payload (action === 'mutate')
  error?: string;          // error message (action === 'deny')
  traceData?: unknown;     // optional trace data
  logs?: string[];         // log entries from membrane.log effects
}

// ─── Sink interface ──────────────────────────────────────────────────────────

export interface MembraneEffectSink {
  onAllow(): void;
  onDeny(error: string): void;
  onMutate(newPayload: unknown): void;
  onTrace(data?: unknown): void;
  onLog(message: string): void;
}

// ─── Default sink (result collector) ─────────────────────────────────────────

export class MembraneResultCollector implements MembraneEffectSink {
  result: MembraneResult = { action: 'allow' };
  logs: string[] = [];

  onAllow(): void {
    this.result = { ...this.result, action: 'allow' };
  }

  onDeny(error: string): void {
    this.result = { ...this.result, action: 'deny', error };
  }

  onMutate(newPayload: unknown): void {
    this.result = { ...this.result, action: 'mutate', payload: newPayload };
  }

  onTrace(data?: unknown): void {
    this.result = { ...this.result, traceData: data };
  }

  onLog(message: string): void {
    this.logs.push(message);
  }

  reset(): void {
    this.result = { action: 'allow' };
    this.logs = [];
  }
}

// ─── Rate limiter state ──────────────────────────────────────────────────────

interface RateEntry {
  count: number;
  windowStart: number;
  windowMs: number;
}

// ─── Backend implementation ──────────────────────────────────────────────────

/** Delegate for capability checks — injected by the actor or test harness. */
export interface CapabilityChecker {
  checkCapability(capability: string, context?: string): Promise<{ allowed: boolean; error?: string }>;
}

export class MembraneEffectBackend implements EffectBackend {
  readonly ops = [
    'membrane.allow',
    'membrane.deny',
    'membrane.trace',
    'membrane.mutate',
    'membrane.log',
    'membrane.rate-check',
    'membrane.check-capability',
  ] as const;

  private sink: MembraneEffectSink;
  private rateLimits = new Map<string, RateEntry>();
  private capabilityChecker: CapabilityChecker | null;

  /** Max rate-limit entries before eviction sweep */
  private static MAX_RATE_ENTRIES = 10_000;

  constructor(sink: MembraneEffectSink, capabilityChecker?: CapabilityChecker) {
    this.sink = sink;
    this.capabilityChecker = capabilityChecker ?? null;
  }

  /** Swap the sink (collector) for per-run isolation. */
  setSink(sink: MembraneEffectSink): void {
    this.sink = sink;
  }

  async handle(_state: State, opcall: OpCall): Promise<State> {
    switch (opcall.op) {
      case 'membrane.allow': {
        this.sink.onAllow();
        return opcall.resumption.invoke({ tag: 'Unit' } as Val);
      }

      case 'membrane.deny': {
        const error = this.strArg(opcall.args[0], 'Denied by membrane');
        this.sink.onDeny(error);
        return opcall.resumption.invoke({ tag: 'Unit' } as Val);
      }

      case 'membrane.mutate': {
        const payloadStr = this.strArg(opcall.args[0]);
        let newPayload: unknown;
        try {
          newPayload = JSON.parse(payloadStr);
        } catch {
          newPayload = payloadStr;
        }
        this.sink.onMutate(newPayload);
        return opcall.resumption.invoke({ tag: 'Unit' } as Val);
      }

      case 'membrane.trace': {
        const traceOp = this.strArg(opcall.args[0]);
        const traceSender = this.strArg(opcall.args[1]);
        const traceExtra = opcall.args[2] ? this.strArg(opcall.args[2]) : undefined;
        this.sink.onTrace({ op: traceOp, sender: traceSender, extra: traceExtra });
        return opcall.resumption.invoke({ tag: 'Unit' } as Val);
      }

      case 'membrane.log': {
        const message = this.strArg(opcall.args[0]);
        this.sink.onLog(message);
        return opcall.resumption.invoke({ tag: 'Unit' } as Val);
      }

      case 'membrane.rate-check': {
        const key = this.strArg(opcall.args[0], 'default');
        const limit = this.numArg(opcall.args[1], 100);
        const windowMs = this.numArg(opcall.args[2], 60000);
        const allowed = this.checkRate(key, limit, windowMs);
        return opcall.resumption.invoke({ tag: 'Bool', b: allowed } as Val);
      }

      case 'membrane.check-capability': {
        const capability = this.strArg(opcall.args[0], '');
        const context = this.strArg(opcall.args[1], '');
        if (!this.capabilityChecker) {
          // No checker installed — deny by default (secure default)
          return opcall.resumption.invoke({ tag: 'Bool', b: false } as Val);
        }
        const result = await this.capabilityChecker.checkCapability(capability, context);
        return opcall.resumption.invoke({ tag: 'Bool', b: result.allowed } as Val);
      }

      default: {
        // Unknown membrane effect — deny for safety
        this.sink.onDeny(`Unknown membrane effect: ${opcall.op}`);
        return opcall.resumption.invoke({ tag: 'Unit' } as Val);
      }
    }
  }

  // ─── Helpers ───────────────────────────────────────────────────────────────

  private strArg(v: Val | undefined, fallback = ''): string {
    if (!v) return fallback;
    if (v.tag === 'Str') return (v as { tag: 'Str'; s: string }).s;
    if (v.tag === 'Num') return String((v as { tag: 'Num'; n: number }).n);
    if (v.tag === 'Bool') return String((v as { tag: 'Bool'; b: boolean }).b);
    return fallback;
  }

  private numArg(v: Val | undefined, fallback: number): number {
    if (!v) return fallback;
    if (v.tag === 'Num') return (v as { tag: 'Num'; n: number }).n;
    if (v.tag === 'Str') {
      const n = Number((v as { tag: 'Str'; s: string }).s);
      return isNaN(n) ? fallback : n;
    }
    return fallback;
  }

  private checkRate(key: string, limit: number, windowMs: number): boolean {
    const now = Date.now();

    // Evict stale entries when map exceeds threshold
    if (this.rateLimits.size > MembraneEffectBackend.MAX_RATE_ENTRIES) {
      for (const [k, e] of this.rateLimits) {
        if (now - e.windowStart > e.windowMs * 2) this.rateLimits.delete(k);
      }
    }

    let entry = this.rateLimits.get(key);
    if (!entry || (now - entry.windowStart) > windowMs) {
      entry = { count: 0, windowStart: now, windowMs };
      this.rateLimits.set(key, entry);
    }
    entry.count++;
    return entry.count <= limit;
  }
}
