import React from "react";
import { render } from "ink-testing-library";
import { writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { TuiApp } from "../src/adapters/channel/tui/app/TuiApp.js";
import { createGateway } from "../src/gateway/index.js";
import { createModelRuntime } from "../src/model/index.js";
import { createDefaultPermissionContext, PermissionRuntime } from "../src/permission/index.js";
import { loadPilotConfig } from "../src/pilot/index.js";
import { createRouterRuntime } from "../src/router/index.js";
import {
  SequentialToolScheduler,
  ToolRegistry,
  ToolRuntime,
  type PilotDeckToolDefinition,
} from "../src/tool/index.js";
import type { AgentRuntimeConfig } from "../src/agent/index.js";
import { createAgentSession } from "../src/agent/index.js";

const PROVIDER = process.env.PILOTDECK_E2E_PROVIDER ?? "edgeclaw";
const MODEL = process.env.PILOTDECK_E2E_MODEL ?? "moonshotai/kimi-k2.6";
const PROMPT = process.env.PILOTDECK_E2E_PROMPT ?? "Use add_numbers to compute 17 + 25, then tell me the result.";

const addNumbersTool: PilotDeckToolDefinition = {
  name: "add_numbers",
  description: "Add two numbers and return the result.",
  kind: "custom",
  inputSchema: {
    type: "object",
    required: ["a", "b"],
    additionalProperties: false,
    properties: {
      a: { type: "number" },
      b: { type: "number" },
    },
  },
  isReadOnly: () => true,
  isConcurrencySafe: () => true,
  execute: async (input) => {
    const { a, b } = input as { a: number; b: number };
    return { content: [{ type: "text", text: String(a + b) }], data: { sum: a + b } };
  },
};

async function main(): Promise<void> {
  const snapshot = loadPilotConfig();
  const provider = snapshot.config.model.providers[PROVIDER];
  if (!provider?.models[MODEL]) {
    throw new Error(`Provider ${PROVIDER} or model ${MODEL} is not configured.`);
  }

  const cwd = process.cwd();
  const registry = new ToolRegistry();
  registry.register(addNumbersTool);
  const permissionRuntime = new PermissionRuntime();
  const toolRuntime = new ToolRuntime(registry, permissionRuntime);
  const scheduler = new SequentialToolScheduler(toolRuntime);
  const modelRuntime = createModelRuntime(snapshot.config.model);

  const config: AgentRuntimeConfig = {
    provider: PROVIDER,
    model: MODEL,
    cwd,
    systemPrompt:
      "You are PilotDeck running an end-to-end TUI test. When asked for arithmetic, you MUST call the provided add_numbers tool exactly once instead of computing it yourself, then report the answer in plain text.",
    maxOutputTokens: 1024,
    temperature: 0,
    permissionMode: "default",
    permissionContext: createDefaultPermissionContext({
      cwd,
      mode: "default",
      canPrompt: false,
      bypassAvailable: true,
    }),
    metadata: { test: "tui-e2e-record" },
  };

  const router = createRouterRuntime(
    snapshot.config.router ?? {
      scenarios: {
        default: { id: `${PROVIDER}/${MODEL}`, provider: PROVIDER, model: MODEL },
      },
      zeroUsageRetry: { enabled: true, maxAttempts: 5 },
    },
    { modelRuntime },
  );

  const baseGateway = createGateway({
    session: {
      create: async ({ sessionKey }) =>
        createAgentSession({
          sessionId: sessionKey,
          config,
          dependencies: {
            router,
            tools: { registry, scheduler },
          },
        }),
    },
    serverInfo: { mode: "in_process", projectKey: cwd },
  });

  const gateway = process.env.PILOTDECK_E2E_TRACE === "1" ? wrapWithTrace(baseGateway) : baseGateway;

  const tree = (
    <TuiApp
      gateway={gateway}
      connection="in_process"
      projectKey={cwd}
      cwd={cwd}
      model={`${PROVIDER} · ${MODEL}`}
    />
  );

  const instance = render(tree);

  const writeFrame = (label: string) => {
    process.stdout.write(`\n--- ${label} (frame #${instance.frames.length}) ---\n`);
    process.stdout.write(`${instance.lastFrame() ?? ""}\n`);
  };

  await wait(120);
  writeFrame("cold start");

  for (const ch of PROMPT) {
    instance.stdin.write(ch);
    await wait(8);
  }
  await wait(120);
  writeFrame("after typing prompt");

  instance.stdin.write("\r");
  writeFrame("submit");

  const finalFrame = await waitForCompletedFrame(instance, 120_000);
  writeFrame("final");

  const logPath = resolve(process.cwd(), "artifacts/tui-e2e-frames.log");
  writeFileSync(logPath, instance.frames.map((frame, index) => `--- frame ${index} ---\n${frame}\n`).join("\n"));
  process.stdout.write(`\nSaved ${instance.frames.length} frames to ${logPath}\n`);

  instance.unmount();
  if (!finalFrame) {
    throw new Error("Timed out waiting for the assistant final frame.");
  }
}

function wrapWithTrace(gateway: ReturnType<typeof createGateway>): ReturnType<typeof createGateway> {
  return new Proxy(gateway, {
    get(target, prop, receiver) {
      const original = Reflect.get(target, prop, receiver);
      if (prop !== "submitTurn" || typeof original !== "function") {
        return original;
      }
      return (...args: Parameters<typeof gateway.submitTurn>) => {
        const startedAt = Date.now();
        let lastAt = startedAt;
        let textChars = 0;
        const iterable = original.apply(target, args) as AsyncIterable<unknown>;
        const ms = (now: number) => `${(now - startedAt).toString().padStart(5)} ms`;
        process.stdout.write(`\n[trace] submitTurn() called\n`);
        return (async function* () {
          for await (const event of iterable) {
            const now = Date.now();
            const delta = now - lastAt;
            lastAt = now;
            const ev = event as { type: string; text?: string; name?: string; toolCallId?: string };
            const type = ev.type;
            let detail = "";
            if (type === "assistant_text_delta") {
              textChars += ev.text?.length ?? 0;
              detail = ` text+=${ev.text?.length ?? 0} total=${textChars}`;
            } else if (type === "tool_call_started" || type === "tool_call_finished") {
              detail = ` ${ev.name ?? ev.toolCallId ?? ""}`;
            } else if (type === "error") {
              detail = ` "${(event as { message?: string }).message ?? ""}"`;
            }
            process.stdout.write(`[trace ${ms(now)} +${delta.toString().padStart(4)}ms] ${type}${detail}\n`);
            yield event;
          }
          const total = Date.now() - startedAt;
          process.stdout.write(`[trace] turn finished after ${total} ms (${textChars} assistant chars)\n`);
        })();
      };
    },
  });
}

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

async function waitForFrame(
  instance: ReturnType<typeof render>,
  pattern: RegExp,
  timeoutMs: number,
): Promise<string | undefined> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const frame = instance.lastFrame();
    if (frame && pattern.test(frame)) {
      return frame;
    }
    await wait(120);
  }
  return undefined;
}

async function waitForCompletedFrame(
  instance: ReturnType<typeof render>,
  timeoutMs: number,
): Promise<string | undefined> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const frame = instance.lastFrame() ?? "";
    const hasResult = /\b42\b/.test(frame);
    const stillThinking = /✦ thinking/.test(frame);
    if (hasResult && !stillThinking) {
      return frame;
    }
    await wait(150);
  }
  return undefined;
}

async function waitForStableFrame(
  instance: ReturnType<typeof render>,
  timeoutMs: number,
  stableMs = 600,
): Promise<string | undefined> {
  const deadline = Date.now() + timeoutMs;
  let last = instance.lastFrame();
  let lastChange = Date.now();
  while (Date.now() < deadline) {
    const current = instance.lastFrame();
    if (current !== last) {
      last = current;
      lastChange = Date.now();
    } else if (Date.now() - lastChange >= stableMs) {
      return current;
    }
    await wait(120);
  }
  return last;
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});