/**
 * W7: BooCoder-hosted OpenAI-compatible local-model gateway.
 *
 * Accepts composite local model ids ("sam-desktop/qwen3.6-35b"), parses them
 * via the provider registry, and proxies the request to the correct provider's
 * baseUrl with the bare wire model id. Unknown provider → 400.
 *
 * Presented to opencode as ONE stable provider namespace "boocode-local".
 * The inner modelID carries the composite local identity so duplicate wire
 * names across providers remain unambiguous end-to-end (D-6).
 */
import { once } from 'node:events';
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { parseModelRef, getLlamaProviders } from './llama-providers.js';
import { fetchRegistryModels } from './provider-snapshot.js';
import type { ProviderModel } from './provider-types.js';

/**
 * Resolve a composite model id to the upstream provider's baseUrl + wire model id.
 */
export function resolveGatewayModel(
  model: string,
): { baseUrl: string; wireModelId: string } | { error: string } {
  const ref = parseModelRef(model);
  const providers = getLlamaProviders();
  const provider = providers.providers.find((p) => p.id === ref.providerId);
  if (!provider) {
    return { error: `unknown provider: ${ref.providerId} (model: ${model})` };
  }
  return { baseUrl: provider.baseUrl, wireModelId: ref.wireModelId };
}

/**
 * Handle POST /v1/chat/completions — proxy to the correct local provider.
 */
async function handleChatCompletions(
  req: FastifyRequest,
  reply: FastifyReply,
): Promise<void> {
  const body = req.body as Record<string, unknown> | undefined;
  if (!body || typeof body.model !== 'string') {
    return reply.code(400).send({ error: 'missing or invalid "model" field' });
  }

  const modelStr = body.model;
  const resolved = resolveGatewayModel(modelStr);
  if ('error' in resolved) {
    return reply.code(400).send({ error: resolved.error });
  }

  const { baseUrl, wireModelId } = resolved;

  const upstreamBody = { ...body, model: wireModelId };

  // Abort the upstream call if the client disconnects, so a cancelled turn
  // doesn't keep the GPU generating to completion.
  const clientGone = new AbortController();
  reply.raw.once('close', () => clientGone.abort());

  // Forward the client's Authorization header when present (future-proofing
  // for authed upstreams; llama-swap ignores it today).
  const auth = req.headers.authorization;

  // Forward inbound X-Boo-Source header for per-consumer attribution (P4).
  // Default to 'boocoder' when not present (opencode dispatch path).
  const booSource = (req.headers['x-boo-source'] as string | undefined) ?? 'boocoder';

  let upstreamRes: Response;
  try {
    upstreamRes = await fetch(`${baseUrl}/v1/chat/completions`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...(auth ? { Authorization: auth } : {}),
        'X-Boo-Source': booSource,
      },
      body: JSON.stringify(upstreamBody),
      signal: AbortSignal.any([AbortSignal.timeout(300_000), clientGone.signal]),
    });
  } catch (err) {
    if (clientGone.signal.aborted) return; // client went away; nothing to answer
    req.log.error({ err, baseUrl, model: modelStr }, 'local-gateway: upstream fetch failed');
    return reply.code(502).send({
      error: `upstream provider unreachable: ${err instanceof Error ? err.message : String(err)}`,
    });
  }

  // Pipe the upstream response status + headers + body to the client.
  const status = upstreamRes.status;
  const contentType = upstreamRes.headers.get('content-type') ?? 'application/json';

  if (body.stream) {
    // Streaming: pipe the response body with backpressure — pause reading the
    // upstream when the client socket's buffer is full.
    reply.raw.writeHead(status, { 'content-type': contentType });
    if (upstreamRes.body) {
      const reader = upstreamRes.body.getReader();
      try {
        while (!clientGone.signal.aborted) {
          const { done, value } = await reader.read();
          if (done) break;
          if (!reply.raw.write(value)) await once(reply.raw, 'drain');
        }
      } catch (err) {
        if (!clientGone.signal.aborted) {
          req.log.error({ err, baseUrl, model: modelStr }, 'local-gateway: stream relay failed');
        }
      } finally {
        reply.raw.end();
      }
    } else {
      reply.raw.end();
    }
  } else {
    // Non-streaming: relay the full JSON response.
    const data = await upstreamRes.json().catch(() => null);
    if (data === null) {
      return reply.code(status === 200 ? 502 : status).send({
        error: { message: 'upstream returned a non-JSON response', code: status },
      });
    }
    reply.code(status).header('content-type', contentType).send(data);
  }
}

/**
 * Handle GET /v1/models — live composite model list fetched from every
 * provider in the registry (same source as the provider snapshot).
 */
async function handleModels(_req: FastifyRequest, reply: FastifyReply): Promise<void> {
  const models: ProviderModel[] = await fetchRegistryModels();
  reply.send({
    object: 'list',
    data: models.map((m) => ({ id: m.id, object: 'model', owned_by: 'boocode-local' })),
  });
}

/**
 * Register the local-model gateway routes on the coder's Fastify instance.
 */
export function registerLocalGatewayRoutes(app: FastifyInstance): void {
  app.post('/v1/chat/completions', handleChatCompletions);
  app.get('/v1/models', handleModels);
}
