import type { FastifyInstance, FastifyRequest } from 'fastify';
import type { LlmService, LlmView, Viewer } from '../services/llm.service.js';
import { LlmAuthVerificationError, effectivePoolName } from '../services/llm.service.js';
import { NotFoundError, ConflictError } from '../services/mcp-server.service.js';

/**
 * v7: build a Viewer from the request's RBAC scope so the service
 * applies the visibility filter consistently. The list endpoint relies
 * on the preSerialization hook for the same logic; for get-by-name/id
 * the service does the filter itself and 404s on a hidden row.
 */
function viewerFromRequest(request: FastifyRequest): Viewer | null {
  if (request.userId === undefined || request.rbacScope === undefined) return null;
  return {
    userId: request.userId,
    wildcard: request.rbacScope.isAdmin,  // only admins skip the visibility filter
    allowedNames: request.rbacScope.names,
  };
}

export function registerLlmRoutes(
  app: FastifyInstance,
  service: LlmService,
): void {
  app.get('/api/v1/llms', async () => {
    // List goes through the preSerialization hook which applies both
    // RBAC name-scope and v7 visibility filter — service stays
    // viewer-blind here so internal callers (audit, sweeps) keep
    // working without a request context.
    return service.list();
  });

  // Accepts either CUID or human name. Used both by the CLI (which usually
  // resolves to CUID first) and by FailoverRouter's RBAC pre-check (which
  // hands over the user-facing name to avoid an extra round-trip).
  app.get<{ Params: { id: string } }>('/api/v1/llms/:id', async (request, reply) => {
    try {
      return await getByIdOrName(service, request.params.id, viewerFromRequest(request));
    } catch (err) {
      if (err instanceof NotFoundError) {
        reply.code(404);
        return { error: err.message };
      }
      throw err;
    }
  });

  // No explicit HEAD handler: Fastify auto-derives HEAD from GET, which runs
  // the same RBAC hook + lookup and drops the body. That's exactly what
  // FailoverRouter wants for its "can the caller still view this Llm?" probe.

  app.post('/api/v1/llms', async (request, reply) => {
    try {
      // Body field `_skipAuthCheck`: opt-out for offline registration (e.g.
      // wiring config before the upstream is reachable). Stripped from the
      // body before validation.
      const body = (request.body ?? {}) as Record<string, unknown>;
      const skipAuthCheck = body['_skipAuthCheck'] === true;
      delete body['_skipAuthCheck'];
      const row = await service.create(body, { skipAuthCheck });
      reply.code(201);
      return row;
    } catch (err) {
      if (err instanceof ConflictError) {
        reply.code(409);
        return { error: err.message };
      }
      if (err instanceof LlmAuthVerificationError) {
        reply.code(422);
        return { error: err.message, status: err.status };
      }
      throw err;
    }
  });

  app.put<{ Params: { id: string } }>('/api/v1/llms/:id', async (request, reply) => {
    try {
      const body = (request.body ?? {}) as Record<string, unknown>;
      const skipAuthCheck = body['_skipAuthCheck'] === true;
      delete body['_skipAuthCheck'];
      return await service.update(request.params.id, body, { skipAuthCheck });
    } catch (err) {
      if (err instanceof NotFoundError) {
        reply.code(404);
        return { error: err.message };
      }
      if (err instanceof LlmAuthVerificationError) {
        reply.code(422);
        return { error: err.message, status: err.status };
      }
      throw err;
    }
  });

  app.delete<{ Params: { id: string } }>('/api/v1/llms/:id', async (request, reply) => {
    try {
      await service.delete(request.params.id);
      reply.code(204);
      return null;
    } catch (err) {
      if (err instanceof NotFoundError) {
        reply.code(404);
        return { error: err.message };
      }
      throw err;
    }
  });

  // v4: list all members of the effective pool that the named Llm belongs to.
  // The path uses an explicit `/members` suffix so it can't collide with the
  // single-Llm `/api/v1/llms/:id` route — an Llm whose name happens to be
  // "members" would otherwise be unaddressable.
  // Returns []  members[] AND a small header object so callers don't need a
  // second roundtrip to compute pool stats; agents.ts and the CLI both want
  // size + activeCount.
  app.get<{ Params: { name: string } }>('/api/v1/llms/:name/members', async (request, reply) => {
    try {
      const viewer = viewerFromRequest(request);
      const anchor = await getByIdOrName(service, request.params.name, viewer);
      const allMembers = await service.listPoolMembers(effectivePoolName(anchor));
      // v7: filter pool members by visibility too — without this, a
      // pool with private members would leak their names through the
      // /members endpoint even though the list endpoint hides them.
      const members = viewer === null
        ? allMembers
        : allMembers.filter((m) => {
            if (m.visibility !== 'private') return true;
            if (m.ownerId !== null && m.ownerId === viewer.userId) return true;
            if (viewer.allowedNames.has(m.name)) return true;
            return viewer.wildcard;
          });
      return {
        poolName: effectivePoolName(anchor),
        explicitPoolName: anchor.poolName,
        size: members.length,
        activeCount: members.filter((m) => m.status === 'active').length,
        members,
      };
    } catch (err) {
      if (err instanceof NotFoundError) {
        reply.code(404);
        return { error: err.message };
      }
      throw err;
    }
  });
}

/** v4: convenience type for the new `/members` endpoint response. */
export interface PoolMembersResponse {
  /** Effective pool key (poolName ?? name on the anchor row). */
  poolName: string;
  /** Anchor row's literal poolName field — null when it falls back to its own name. */
  explicitPoolName: string | null;
  size: number;
  activeCount: number;
  members: LlmView[];
}

const CUID_RE = /^c[a-z0-9]{24}/i;

/**
 * Look up by CUID first; if the input doesn't look like one, fall back to
 * findByName. Lets the same URL serve both `mcpctl describe llm <name>` and
 * the FailoverRouter's name-based RBAC check. v7: the optional viewer
 * threads through to the service so foreign-private rows 404 cleanly.
 */
async function getByIdOrName(service: LlmService, idOrName: string, viewer: Viewer | null = null) {
  if (CUID_RE.test(idOrName)) {
    return service.getById(idOrName, viewer);
  }
  return service.getByName(idOrName, viewer);
}
