import type { FastifyRequest, FastifyReply } from 'fastify';
import { isMcpToken, hashToken } from '@mcpctl/shared';

export interface McpTokenPrincipal {
  tokenId: string;
  tokenName: string;
  tokenSha: string;
  projectId: string;
  projectName: string;
  ownerId: string;
}

export interface McpTokenLookup {
  tokenId: string;
  tokenName: string;
  tokenSha: string;
  projectId: string;
  projectName: string;
  ownerId: string;
  expiresAt: Date | null;
  revokedAt: Date | null;
}

export interface AuthDeps {
  findSession: (token: string) => Promise<{ userId: string; expiresAt: Date } | null>;
  /**
   * Look up an McpToken by SHA-256 hash. Optional — when absent, Bearer tokens
   * that look like `mcpctl_pat_…` are rejected (400).
   */
  findMcpToken?: (tokenHash: string) => Promise<McpTokenLookup | null>;
}

declare module 'fastify' {
  interface FastifyRequest {
    userId?: string;
    /**
     * v7: extended with `isAdmin` to distinguish `*` (cross-resource
     * admin) grants from plain `view:llms` resource-wide grants. The
     * preSerialization filter and the route-level Viewer use this to
     * decide whether to skip the visibility filter (admins bypass;
     * regular wildcard does not).
     */
    rbacScope?: { wildcard: boolean; isAdmin: boolean; names: Set<string> };
    /** Set by the auth hook when the caller authenticated via a McpToken bearer (prefix `mcpctl_pat_`). */
    mcpToken?: McpTokenPrincipal;
  }
}

export function createAuthMiddleware(deps: AuthDeps) {
  return async function authMiddleware(request: FastifyRequest, reply: FastifyReply): Promise<void> {
    const header = request.headers.authorization;
    if (header === undefined || !header.startsWith('Bearer ')) {
      reply.code(401).send({ error: 'Missing or invalid Authorization header' });
      return;
    }

    const token = header.slice(7);
    if (token.length === 0) {
      reply.code(401).send({ error: 'Empty token' });
      return;
    }

    // Dispatch on the prefix: `mcpctl_pat_…` → McpToken path; anything else → session path.
    if (isMcpToken(token)) {
      if (deps.findMcpToken === undefined) {
        reply.code(401).send({ error: 'McpToken auth not enabled' });
        return;
      }
      const row = await deps.findMcpToken(hashToken(token));
      if (row === null) {
        reply.code(401).send({ error: 'Invalid token' });
        return;
      }
      if (row.revokedAt !== null) {
        reply.code(401).send({ error: 'Token revoked' });
        return;
      }
      if (row.expiresAt !== null && row.expiresAt < new Date()) {
        reply.code(401).send({ error: 'Token expired' });
        return;
      }
      request.userId = row.ownerId;
      request.mcpToken = {
        tokenId: row.tokenId,
        tokenName: row.tokenName,
        tokenSha: row.tokenSha,
        projectId: row.projectId,
        projectName: row.projectName,
        ownerId: row.ownerId,
      };
      return;
    }

    const session = await deps.findSession(token);
    if (session === null) {
      reply.code(401).send({ error: 'Invalid token' });
      return;
    }

    if (session.expiresAt < new Date()) {
      reply.code(401).send({ error: 'Token expired' });
      return;
    }

    request.userId = session.userId;
  };
}
