/**
 * A local stand-in for Prime Intellect's OpenAI-compatible inference endpoint.
 *
 * Piggy is the only part of PIG that costs money to exercise, which meant the
 * only way to see the chat UI move was to spend the credit. This speaks the
 * same wire protocol `apps/piggy/src/chat.ts` and `provider.ts` parse — SSE
 * deltas, `reasoning_content`, incrementally assembled `tool_calls`, and a
 * trailing `usage` chunk — so the whole loop, including a real tool round trip,
 * runs offline and deterministically.
 *
 * It is a development tool. It is never imported by the worker or the chat
 * server; it is started on its own with `pnpm -F @pig/piggy run dev:mock`.
 *
 * Steering it: a user message containing one of these directives makes the mock
 * take a specific branch, so the failure states of the UI can be seen on demand
 * rather than only when production breaks.
 *
 *   /mock error       — respond 500, the upstream-failure path
 *   /mock ratelimit   — respond 429
 *   /mock cut         — stream a few tokens, then drop the connection mid-answer
 *   /mock slow        — stream at roughly a tenth of the usual rate
 *   /mock badtool     — emit a tool call with unparseable JSON arguments
 *   /mock notool      — answer directly, calling nothing
 *   /mock long        — stream a long, markdown-heavy answer (tables, code, lists)
 */
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';

interface ChatMessage {
  role: string;
  content?: string | null;
  name?: string;
  tool_calls?: { id: string; function: { name: string; arguments: string } }[];
}

interface ChatRequest {
  model?: string;
  messages?: ChatMessage[];
  tools?: { function: { name: string; description?: string } }[];
  stream?: boolean;
}

const DIRECTIVES = ['error', 'ratelimit', 'cut', 'slow', 'badtool', 'notool', 'long'] as const;
type Directive = (typeof DIRECTIVES)[number];

/**
 * The directive comes from the question being asked, which is the LAST user
 * message — never from the whole conversation.
 *
 * Joining every user turn meant a `/mock cut` earlier in the transcript steered
 * every question after it, and `DIRECTIVES.find` resolves in list order rather
 * than in the order they were typed, so the hijack was silent: asking for
 * `/mock badtool` after a `/mock cut` quietly replayed the cut. Anyone walking
 * the failure states in one sitting saw the wrong one and had no way to tell.
 */
function directiveFor(messages: ChatMessage[]): Directive | null {
  const asked = messages.filter((message) => message.role === 'user').at(-1);
  const text = (asked?.content ?? '').toLowerCase();
  return DIRECTIVES.find((name) => text.includes(`/mock ${name}`)) ?? null;
}

/**
 * Chunking on word boundaries rather than characters, because that is what the
 * real endpoint does and a UI that only looks smooth under character-by-character
 * delivery is a UI that will look wrong in production.
 */
function tokenise(text: string): string[] {
  return text.match(/\s*\S+/g) ?? [];
}

const LONG_ANSWER = `Here is the supply picture for the accounts you asked about.

| Supplier | Available | Blended cost | Committed through |
| --- | ---: | ---: | --- |
| Northwind Compute | 512× H100 | $1.86/GPU-hr | 2026-11-30 |
| Halden Systems | 128× H200 | $2.94/GPU-hr | 2027-02-28 |
| Kestrel Labs | 64× A100 | $0.91/GPU-hr | 2026-09-15 |

Two things stand out:

1. **Northwind is the only supplier with headroom above 256 GPUs**, so any demand
   above that has to be split across two contracts.
2. Kestrel's commitment expires inside 45 days and is only 38% sold. Unsold hours
   are charged against the full commitment, so that block is currently losing money.

To pull the margin figure yourself:

\`\`\`sql
select supplier_id, sum(sold_hours) / nullif(sum(committed_hours), 0) as utilisation
from allocations
group by supplier_id
order by utilisation asc;
\`\`\`

I would open the Kestrel renewal before the Northwind expansion.`;

const SHORT_ANSWER = `Based on the record I just read, this account has 512 H100s committed
through the end of November at a blended $1.86/GPU-hr, and 38% of those hours are
still unsold. That is the number worth acting on — unsold hours are charged against
the full commitment, so utilisation below about 70% turns the block negative.`;

const REASONING = `The user is asking about capacity, so I should read the record
rather than answer from the page title. I will call the PIG tool first and quote
its figures.`;

function sse(response: ServerResponse, payload: unknown): void {
  response.write(`data: ${JSON.stringify(payload)}\n\n`);
}

/** `[DONE]` is a raw sentinel, not JSON — quoting it is what a naive mock gets wrong. */
function sseDone(response: ServerResponse): void {
  response.write('data: [DONE]\n\n');
}

function deltaChunk(delta: Record<string, unknown>, model: string): unknown {
  return {
    id: 'mock-completion',
    object: 'chat.completion.chunk',
    model,
    choices: [{ index: 0, delta, finish_reason: null }],
  };
}

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

/**
 * Prefers a read-only tool that takes no required arguments when one is on
 * offer, so the mock exercises a real tool round trip against whatever tool set
 * the caller happens to have registered.
 */
function pickTool(request: ChatRequest): { name: string; arguments: string } | null {
  const names = (request.tools ?? []).map((tool) => tool.function.name);
  const first = names[0];
  if (first === undefined) return null;
  const preferred =
    names.find((name) => name.includes('page') || name.includes('overview')) ?? first;
  return { name: preferred, arguments: '{}' };
}

async function streamCompletion(
  response: ServerResponse,
  request: ChatRequest,
  directive: Directive | null,
): Promise<void> {
  const model = request.model ?? 'nvidia/nemotron-3-nano-30b-a3b';
  const messages = request.messages ?? [];
  const alreadyCalledATool = messages.some((message) => message.role === 'tool');
  const pace = directive === 'slow' ? 120 : 18;

  response.writeHead(200, {
    'content-type': 'text/event-stream',
    'cache-control': 'no-cache',
    connection: 'keep-alive',
  });

  for (const token of tokenise(REASONING)) {
    sse(response, deltaChunk({ reasoning_content: token }, model));
    await sleep(pace / 2);
  }

  const tool = pickTool(request);
  const shouldCallTool = !alreadyCalledATool && directive !== 'notool' && tool !== null;

  if (shouldCallTool) {
    const args = directive === 'badtool' ? '{"unclosed": ' : tool.arguments;
    // Split across chunks the way the real endpoint does, so the assembly logic
    // in chat.ts is genuinely exercised rather than handed a finished object.
    sse(response, deltaChunk({ tool_calls: [{ index: 0, id: 'call_mock_1', function: { name: tool.name } }] }, model));
    for (const piece of args.match(/.{1,6}/g) ?? []) {
      sse(response, deltaChunk({ tool_calls: [{ index: 0, function: { arguments: piece } }] }, model));
      await sleep(pace / 3);
    }
    sse(response, { id: 'mock-completion', object: 'chat.completion.chunk', model, choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }], usage: { prompt_tokens: 820, completion_tokens: 36 } });
    sseDone(response);
    response.end();
    return;
  }

  const answer = directive === 'long' ? LONG_ANSWER : SHORT_ANSWER;
  const tokens = tokenise(answer);
  for (const [index, token] of tokens.entries()) {
    if (directive === 'cut' && index === 12) {
      response.destroy();
      return;
    }
    sse(response, deltaChunk({ content: token }, model));
    await sleep(pace);
  }

  sse(response, {
    id: 'mock-completion',
    object: 'chat.completion.chunk',
    model,
    choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
    usage: { prompt_tokens: 1_240, completion_tokens: tokens.length },
  });
  sseDone(response);
  response.end();
}

function nonStreamingCompletion(request: ChatRequest, directive: Directive | null): unknown {
  const messages = request.messages ?? [];
  const alreadyCalledATool = messages.some((message) => message.role === 'tool');
  const tool = pickTool(request);
  const shouldCallTool = !alreadyCalledATool && directive !== 'notool' && tool !== null;

  return {
    id: 'mock-completion',
    object: 'chat.completion',
    model: request.model ?? 'nvidia/nemotron-3-nano-30b-a3b',
    choices: [
      {
        index: 0,
        message: shouldCallTool
          ? {
              role: 'assistant',
              content: null,
              tool_calls: [
                {
                  id: 'call_mock_1',
                  type: 'function',
                  function: {
                    name: tool.name,
                    arguments: directive === 'badtool' ? '{"unclosed": ' : tool.arguments,
                  },
                },
              ],
            }
          : { role: 'assistant', content: SHORT_ANSWER },
        finish_reason: shouldCallTool ? 'tool_calls' : 'stop',
      },
    ],
    usage: { prompt_tokens: 1_240, completion_tokens: 180 },
  };
}

async function readBody(request: IncomingMessage): Promise<string> {
  const chunks: Buffer[] = [];
  for await (const chunk of request) chunks.push(chunk as Buffer);
  return Buffer.concat(chunks).toString('utf8');
}

export function createMockInferenceServer() {
  return createServer((request, response) => {
    void (async () => {
      if (!request.url?.endsWith('/chat/completions') || request.method !== 'POST') {
        response.writeHead(404, { 'content-type': 'application/json' });
        response.end(JSON.stringify({ error: { message: 'Not found.' } }));
        return;
      }

      let parsed: ChatRequest;
      try {
        parsed = JSON.parse(await readBody(request)) as ChatRequest;
      } catch {
        response.writeHead(400, { 'content-type': 'application/json' });
        response.end(JSON.stringify({ error: { message: 'Invalid JSON.' } }));
        return;
      }

      const directive = directiveFor(parsed.messages ?? []);
      if (directive === 'error' || directive === 'ratelimit') {
        const status = directive === 'ratelimit' ? 429 : 500;
        response.writeHead(status, { 'content-type': 'application/json' });
        response.end(JSON.stringify({ error: { message: `Mock inference returned ${status}.` } }));
        return;
      }

      if (parsed.stream) {
        await streamCompletion(response, parsed, directive);
        return;
      }

      response.writeHead(200, { 'content-type': 'application/json' });
      response.end(JSON.stringify(nonStreamingCompletion(parsed, directive)));
    })();
  });
}

const port = Number(process.env.MOCK_INFERENCE_PORT ?? 8_945);
createMockInferenceServer().listen(port, '127.0.0.1', () => {
  console.log(`Mock Prime Intellect inference listening on http://127.0.0.1:${port}/v1`);
  console.log(`Directives: ${DIRECTIVES.map((name) => `/mock ${name}`).join(', ')}`);
});
