/**
 * InMemoryHostAdapter — file-free, deterministic AgentHostAdapter (WP-03).
 * Used for multi-host registry tests; stays off production Active path.
 */

import { randomUUID } from "node:crypto";

import type { AgentHostAdapter } from "../contracts/AgentHostAdapter.ts";
import type { HostDescriptor } from "../contracts/HostDescriptor.ts";
import type { HostEventEnvelope } from "../contracts/HostEvent.ts";
import { toHostEventEnvelope } from "../contracts/HostEvent.ts";
import type { HostFailure } from "../contracts/HostFailure.ts";
import type {
  HostRunInspectionState,
  HostRunRef,
  StartRunInput,
} from "../contracts/HostRun.ts";
import type {
  HostSessionRef,
  HostSessionState,
  PrepareSessionInput,
  PreparedHostSession,
} from "../contracts/HostSession.ts";
import { IN_MEMORY_HOST_CAPABILITIES } from "./capabilityPresets.ts";

type SessionRec = {
  reference: HostSessionRef;
  state: HostSessionState;
};

type RunRec = {
  ref: HostRunRef;
  input: StartRunInput;
  cancelled: boolean;
  events: HostEventEnvelope[];
};

export interface InMemoryHostAdapterOptions {
  hostId?: string;
  displayName?: string;
  enabled?: boolean;
}

export class InMemoryHostAdapter implements AgentHostAdapter {
  readonly hostId: string;
  readonly #displayName: string;
  readonly #enabled: boolean;
  readonly #sessions = new Map<string, SessionRec>();
  readonly #runs = new Map<string, RunRec>();

  constructor(opts: InMemoryHostAdapterOptions = {}) {
    this.hostId = opts.hostId?.trim() || "host-in-memory";
    this.#displayName = opts.displayName?.trim() || "In-Memory Host";
    this.#enabled = opts.enabled !== false;
  }

  async describe(): Promise<HostDescriptor> {
    return {
      hostId: this.hostId,
      adapterType: "in-memory",
      displayName: this.#displayName,
      enabled: this.#enabled,
      capabilities: { ...IN_MEMORY_HOST_CAPABILITIES },
      version: "wp-03",
      metadata: { mode: "off" },
    };
  }

  async prepareSession(input: PrepareSessionInput): Promise<PreparedHostSession> {
    if (input.existing) {
      const key = sessionKey(input.existing);
      const existing = this.#sessions.get(key);
      if (existing) {
        existing.state = "ready";
        return { reference: existing.reference, state: "ready", created: false };
      }
      const rec: SessionRec = { reference: input.existing, state: "ready" };
      this.#sessions.set(key, rec);
      return { reference: input.existing, state: "ready", created: false };
    }
    const reference: HostSessionRef = {
      hostId: this.hostId,
      externalSessionId: `mem-${randomUUID()}`,
      mode: "local",
      metadata: {
        workerId: input.worker.workerId,
        roleId: input.worker.roleId,
      },
    };
    this.#sessions.set(sessionKey(reference), {
      reference,
      state: "ready",
    });
    return { reference, state: "ready", created: true };
  }

  async inspectSession(reference: HostSessionRef): Promise<HostSessionState> {
    return this.#sessions.get(sessionKey(reference))?.state ?? "unknown";
  }

  async startRun(session: HostSessionRef, input: StartRunInput): Promise<HostRunRef> {
    const sess = this.#sessions.get(sessionKey(session));
    if (!sess) {
      throw new Error(`unknown session: ${session.externalSessionId}`);
    }
    sess.state = "running";
    const ref: HostRunRef = {
      hostId: this.hostId,
      externalRunId: `mem-run-${input.runId}`,
      session,
    };
    const started = toHostEventEnvelope({
      eventId: `evt-${randomUUID()}`,
      at: new Date().toISOString(),
      hostId: this.hostId,
      workerId: input.workerId,
      runId: input.runId,
      type: "host.run.started",
      payload: { prompt: input.prompt, deliveryAttemptId: input.deliveryAttemptId },
    });
    const completed = toHostEventEnvelope({
      eventId: `evt-${randomUUID()}`,
      at: new Date().toISOString(),
      hostId: this.hostId,
      workerId: input.workerId,
      runId: input.runId,
      type: "host.run.completed",
      payload: { text: `echo:${input.prompt}` },
    });
    this.#runs.set(input.runId, {
      ref,
      input,
      cancelled: false,
      events: [started, completed],
    });
    return ref;
  }

  async *streamEvents(run: HostRunRef): AsyncIterable<HostEventEnvelope> {
    const runId = [...this.#runs.entries()].find(
      ([, rec]) => rec.ref.externalRunId === run.externalRunId,
    )?.[0];
    const rec = runId ? this.#runs.get(runId) : undefined;
    if (!rec) {
      yield toHostEventEnvelope({
        eventId: `evt-${randomUUID()}`,
        at: new Date().toISOString(),
        hostId: this.hostId,
        workerId: "unknown",
        runId: run.externalRunId ?? "unknown",
        type: "host.event.opaque",
        raw: { reason: "run_not_found" },
      });
      return;
    }
    for (const evt of rec.events) {
      if (rec.cancelled && evt.type === "host.run.completed") {
        yield toHostEventEnvelope({
          ...evt,
          eventId: `evt-${randomUUID()}`,
          type: "host.run.cancelled",
          payload: { reason: "cancelled" },
        });
        return;
      }
      yield evt;
    }
    const sess = this.#sessions.get(sessionKey(run.session));
    if (sess) sess.state = "idle";
  }

  async cancelRun(run: HostRunRef, _reason: string): Promise<void> {
    for (const rec of this.#runs.values()) {
      if (rec.ref.externalRunId === run.externalRunId) {
        rec.cancelled = true;
      }
    }
  }

  /** WP-08 delivery_unknown recovery — RunInspectionCapableHost. */
  async inspectRun(run: HostRunRef): Promise<HostRunInspectionState> {
    const entry = [...this.#runs.entries()].find(
      ([, rec]) => rec.ref.externalRunId === run.externalRunId,
    );
    if (!entry) return "unknown";
    const [, rec] = entry;
    if (rec.cancelled) return "cancelled";
    if (rec.events.some((e) => e.type === "host.run.completed")) {
      return "completed";
    }
    if (rec.events.some((e) => e.type === "host.run.started")) {
      return "running";
    }
    return "queued";
  }

  async closeSession(reference: HostSessionRef): Promise<void> {
    const sess = this.#sessions.get(sessionKey(reference));
    if (sess) sess.state = "closed";
  }

  classifyError(error: unknown): HostFailure {
    const message = error instanceof Error ? error.message : String(error);
    return {
      kind: "unknown",
      message,
      retryable: false,
      raw: error,
    };
  }
}

function sessionKey(ref: HostSessionRef): string {
  return `${ref.hostId}::${ref.externalSessionId}`;
}
