// SPEC: issue-1190 fake Responses provider P0/P1.
// Responsibility: deterministic OpenAI-compatible Responses HTTP provider for HWLAB sentinel ECHO runs.
import { createHash, randomUUID } from "node:crypto";

export interface FakeResponsesProviderConfig {
  readonly serviceName: string;
  readonly version: string;
  readonly modelId: string;
  readonly mode: "echo";
  readonly host: string;
  readonly port: number;
  readonly responseDelayMs: number;
  readonly nonEchoPolicy: "error";
}

export interface FakeResponsesProviderService {
  readonly config: FakeResponsesProviderConfig;
  fetch(request: Request): Promise<Response>;
  health(): Record<string, unknown>;
}

interface ResponseRequestContext {
  readonly requestId: string;
  readonly traceId: string | null;
  readonly bodyBytes: number;
  readonly bodySha256: string;
  readonly model: string;
  readonly stream: boolean;
  readonly prompt: string | null;
}

const DEFAULT_VERSION = "issue-1190-p1";

export function loadFakeResponsesProviderConfig(env: NodeJS.ProcessEnv = process.env): FakeResponsesProviderConfig {
  const modelId = env.FAKE_RESPONSES_MODEL_ID || env.MODEL_ID || "fake-echo";
  const host = env.LISTEN_HOST || env.HOST || "0.0.0.0";
  const port = boundedPort(env.PORT, 8080);
  const responseDelayMs = boundedInteger(env.FAKE_RESPONSES_DELAY_MS || env.RESPONSE_DELAY_MS, 0, 0, 30_000, "response delay");
  return {
    serviceName: env.FAKE_RESPONSES_SERVICE_NAME || "hwlab-fake-responses-provider",
    version: env.FAKE_RESPONSES_VERSION || DEFAULT_VERSION,
    modelId,
    mode: "echo",
    host,
    port,
    responseDelayMs,
    nonEchoPolicy: "error",
  };
}

export function createFakeResponsesProviderService(config: FakeResponsesProviderConfig): FakeResponsesProviderService {
  const service: FakeResponsesProviderService = {
    config,
    async fetch(request: Request): Promise<Response> {
      const url = new URL(request.url);
      if (request.method === "GET" && url.pathname === "/healthz") return jsonResponse(service.health());
      if (request.method === "GET" && url.pathname === "/v1/models") return jsonResponse(modelsPayload(config));
      if (request.method === "POST" && url.pathname === "/v1/responses") return await handleResponses(config, request);
      return jsonResponse({ error: { type: "not_found", message: "not found" }, valuesPrinted: false }, 404);
    },
    health(): Record<string, unknown> {
      return {
        ok: true,
        service: config.serviceName,
        version: config.version,
        mode: config.mode,
        model: config.modelId,
        valuesPrinted: false,
      };
    },
  };
  return service;
}

async function handleResponses(config: FakeResponsesProviderConfig, request: Request): Promise<Response> {
  const requestId = request.headers.get("x-request-id") || `fake_${randomUUID()}`;
  const traceId = request.headers.get("traceparent") || request.headers.get("x-trace-id");
  const rawBody = await request.text();
  const bodySha256 = sha256(rawBody);
  let body: Record<string, unknown>;
  try {
    const parsed = JSON.parse(rawBody) as unknown;
    if (!isRecord(parsed)) throw new Error("body must be a JSON object");
    body = parsed;
  } catch (error) {
    logRequest({ requestId, traceId, bodyBytes: Buffer.byteLength(rawBody), bodySha256, model: config.modelId, stream: false, prompt: null }, 400, "invalid-json");
    return providerError("invalid_json", error instanceof Error ? error.message : String(error), requestId, 400);
  }

  const model = typeof body.model === "string" && body.model.length > 0 ? body.model : config.modelId;
  const stream = body.stream !== false;
  const prompt = latestUserText(body);
  const context: ResponseRequestContext = {
    requestId,
    traceId,
    bodyBytes: Buffer.byteLength(rawBody),
    bodySha256,
    model,
    stream,
    prompt,
  };
  const echo = parseEcho(prompt);
  if (echo === null) {
    logRequest(context, 400, "non-echo-prompt");
    return providerError("non_echo_prompt", "fake-echo only accepts latest user input matching /^ECHO\\s+([\\s\\S]*)$/", requestId, 400);
  }
  logRequest(context, 200, "echo");
  return stream ? streamResponse(config, context, echo) : jsonResponse(completedResponsePayload(context, echo));
}

function streamResponse(config: FakeResponsesProviderConfig, context: ResponseRequestContext, text: string): Response {
  const responseId = responseIdFor(context);
  const encoder = new TextEncoder();
  const body = new ReadableStream<Uint8Array>({
    async start(controller) {
      const enqueue = (chunk: string) => controller.enqueue(encoder.encode(chunk));
      enqueue(sse("response.created", responseCreatedPayload(context, responseId)));
      enqueue(sse("response.output_item.added", outputItemAddedPayload(responseId)));
      enqueue(sse("response.content_part.added", contentPartAddedPayload(responseId)));
      if (config.responseDelayMs > 0) await sleep(config.responseDelayMs);
      enqueue(sse("response.output_text.delta", outputTextDeltaPayload(responseId, text)));
      enqueue(sse("response.output_text.done", outputTextDonePayload(responseId, text)));
      enqueue(sse("response.content_part.done", contentPartDonePayload(responseId, text)));
      enqueue(sse("response.output_item.done", outputItemDonePayload(responseId, text)));
      enqueue(sse("response.completed", { type: "response.completed", response: completedResponsePayload(context, text, responseId) }));
      enqueue("data: [DONE]\n\n");
      controller.close();
    },
  });
  return new Response(body, {
    status: 200,
    headers: {
      "content-type": "text/event-stream; charset=utf-8",
      "cache-control": "no-cache, no-transform",
      "x-request-id": context.requestId,
    },
  });
}

function modelsPayload(config: FakeResponsesProviderConfig): Record<string, unknown> {
  return {
    object: "list",
    data: [{
      id: config.modelId,
      object: "model",
      created: 0,
      owned_by: "unidesk-fake-provider",
    }],
    valuesPrinted: false,
  };
}

function responseCreatedPayload(context: ResponseRequestContext, responseId: string): Record<string, unknown> {
  return {
    type: "response.created",
    response: {
      id: responseId,
      object: "response",
      created_at: Math.floor(Date.now() / 1000),
      status: "in_progress",
      model: context.model,
      output: [],
      parallel_tool_calls: false,
    },
  };
}

function outputItemAddedPayload(responseId: string): Record<string, unknown> {
  return {
    type: "response.output_item.added",
    response_id: responseId,
    output_index: 0,
    item: { id: "msg_echo_0", type: "message", status: "in_progress", role: "assistant", content: [] },
  };
}

function contentPartAddedPayload(responseId: string): Record<string, unknown> {
  return {
    type: "response.content_part.added",
    response_id: responseId,
    item_id: "msg_echo_0",
    output_index: 0,
    content_index: 0,
    part: { type: "output_text", text: "" },
  };
}

function outputTextDeltaPayload(responseId: string, text: string): Record<string, unknown> {
  return {
    type: "response.output_text.delta",
    response_id: responseId,
    item_id: "msg_echo_0",
    output_index: 0,
    content_index: 0,
    delta: text,
  };
}

function outputTextDonePayload(responseId: string, text: string): Record<string, unknown> {
  return {
    type: "response.output_text.done",
    response_id: responseId,
    item_id: "msg_echo_0",
    output_index: 0,
    content_index: 0,
    text,
  };
}

function contentPartDonePayload(responseId: string, text: string): Record<string, unknown> {
  return {
    type: "response.content_part.done",
    response_id: responseId,
    item_id: "msg_echo_0",
    output_index: 0,
    content_index: 0,
    part: { type: "output_text", text },
  };
}

function outputItemDonePayload(responseId: string, text: string): Record<string, unknown> {
  return {
    type: "response.output_item.done",
    response_id: responseId,
    output_index: 0,
    item: assistantMessage(text, "completed"),
  };
}

function completedResponsePayload(context: ResponseRequestContext, text: string, explicitId?: string): Record<string, unknown> {
  const id = explicitId ?? responseIdFor(context);
  return {
    id,
    object: "response",
    created_at: Math.floor(Date.now() / 1000),
    status: "completed",
    model: context.model,
    output: [assistantMessage(text, "completed")],
    output_text: text,
    usage: usageFor(context.prompt ?? "", text),
    parallel_tool_calls: false,
    valuesPrinted: false,
  };
}

function assistantMessage(text: string, status: "in_progress" | "completed"): Record<string, unknown> {
  return {
    id: "msg_echo_0",
    type: "message",
    status,
    role: "assistant",
    content: [{ type: "output_text", text }],
  };
}

function usageFor(prompt: string, output: string): Record<string, unknown> {
  return {
    input_tokens: Math.max(1, Math.ceil(prompt.length / 4)),
    output_tokens: Math.max(1, Math.ceil(output.length / 4)),
    total_tokens: Math.max(2, Math.ceil((prompt.length + output.length) / 4)),
  };
}

function providerError(code: string, message: string, requestId: string, status: number): Response {
  return jsonResponse({
    error: {
      type: "invalid_request_error",
      code,
      message,
    },
    requestId,
    valuesPrinted: false,
  }, status, { "x-request-id": requestId });
}

function latestUserText(body: Record<string, unknown>): string | null {
  const inputText = latestUserTextFromInput(body.input);
  if (inputText !== null) return inputText;
  return latestUserTextFromInput(body.messages);
}

function latestUserTextFromInput(input: unknown): string | null {
  if (typeof input === "string") return input;
  if (!Array.isArray(input)) return null;
  for (let index = input.length - 1; index >= 0; index -= 1) {
    const item = input[index];
    if (typeof item === "string") return item;
    if (!isRecord(item)) continue;
    const role = typeof item.role === "string" ? item.role : null;
    if (role !== null && role !== "user") continue;
    const direct = contentText(item.content);
    if (direct !== null) return direct;
    const text = contentText(item);
    if (text !== null) return text;
  }
  return null;
}

function contentText(value: unknown): string | null {
  if (typeof value === "string") return value;
  if (Array.isArray(value)) {
    const parts = value.map(contentText).filter((item): item is string => item !== null);
    return parts.length === 0 ? null : parts.join("");
  }
  if (!isRecord(value)) return null;
  for (const key of ["text", "input_text"]) {
    const found = value[key];
    if (typeof found === "string") return found;
  }
  return null;
}

function parseEcho(prompt: string | null): string | null {
  if (prompt === null) return null;
  const match = /^ECHO\s+([\s\S]*)$/u.exec(prompt);
  return match?.[1] ?? null;
}

function responseIdFor(context: ResponseRequestContext): string {
  return `resp_fake_${sha256(`${context.requestId}\0${context.bodySha256}`).slice(0, 24)}`;
}

function logRequest(context: ResponseRequestContext, status: number, outcome: string): void {
  const payload = {
    event: "fake-responses-provider.request",
    requestId: context.requestId,
    traceIdPresent: context.traceId !== null,
    status,
    outcome,
    model: context.model,
    stream: context.stream,
    bodyBytes: context.bodyBytes,
    bodySha256: `sha256:${context.bodySha256}`,
    promptSha256: context.prompt === null ? null : `sha256:${sha256(context.prompt)}`,
    valuesPrinted: false,
  };
  console.error(JSON.stringify(payload));
}

function sse(event: string, data: Record<string, unknown>): string {
  return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
}

function jsonResponse(payload: Record<string, unknown>, status = 200, extraHeaders: Record<string, string> = {}): Response {
  return new Response(`${JSON.stringify(payload)}\n`, {
    status,
    headers: {
      "content-type": "application/json; charset=utf-8",
      ...extraHeaders,
    },
  });
}

function sha256(value: string): string {
  return createHash("sha256").update(value).digest("hex");
}

function boundedPort(raw: string | undefined, fallback: number): number {
  return boundedInteger(raw, fallback, 1, 65_535, "port");
}

function boundedInteger(raw: string | undefined, fallback: number, min: number, max: number, label: string): number {
  if (raw === undefined || raw.length === 0) return fallback;
  const value = Number(raw);
  if (!Number.isInteger(value) || value < min || value > max) throw new Error(`${label} must be an integer between ${min} and ${max}`);
  return value;
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}
