import { API_KEY_SCOPES } from '@pig/core';
import type { Database } from '@pig/db';
import { apiKeys, users } from '@pig/db';
import { and, desc, eq, isNull } from 'drizzle-orm';
import { Hono } from 'hono';
import { randomBytes } from 'node:crypto';
import { z } from 'zod';
import { AuthError, hashApiKey, type Principal } from '../lib/auth';
import {
  apiError,
  bodylessMutation,
  MutationError,
  mutation,
  type ApiEnv,
} from '../lib/mutation';

const apiKeyScopesSchema = z
  .array(z.enum(API_KEY_SCOPES))
  .min(1)
  .max(API_KEY_SCOPES.length)
  .refine((scopes) => scopes.includes('read'), 'Every API key must include the read scope.')
  .refine((scopes) => new Set(scopes).size === scopes.length, 'Scopes must be unique.');

export const apiKeyCreateSchema = z
  .object({
    name: z.string().trim().min(1).max(120),
    scopes: apiKeyScopesSchema.default(['read']),
    expiresAt: z.string().datetime().optional(),
    userId: z.string().uuid().optional(),
  })
  .strict();

const apiKeyListSchema = z
  .object({
    userId: z.string().uuid().optional(),
  })
  .strict();

const revokeApiKeySchema = z.object({}).strict();

type ApiKeyRow = typeof apiKeys.$inferSelect;

export interface GeneratedApiKey {
  key: string;
  keyHash: string;
  keyPrefix: string;
}

export function generateApiKey(): GeneratedApiKey {
  const key = `pig_${randomBytes(32).toString('base64url')}`;
  return {
    key,
    keyHash: hashApiKey(key),
    // Six random characters identify the credential without materially
    // reducing its 256-bit search space if a key list is exposed.
    keyPrefix: key.slice(0, 10),
  };
}

export function apiKeyMetadata(row: ApiKeyRow) {
  return {
    id: row.id,
    userId: row.userId,
    name: row.name,
    keyPrefix: row.keyPrefix,
    scopes: row.scopes,
    lastUsedAt: row.lastUsedAt,
    expiresAt: row.expiresAt,
    revokedAt: row.revokedAt,
    createdAt: row.createdAt,
  };
}

export function apiKeyCreationResponse(row: ApiKeyRow, key: string) {
  return { ...apiKeyMetadata(row), key };
}

/** A credential may never mint a successor that outlives its own revocation. */
export function requireApiKeyManagement(principal: Principal): void {
  if (principal.via !== 'api_key') return;
  throw new AuthError(
    'API keys cannot create, list, or revoke credentials.',
    403,
    'credential_management_forbidden',
  );
}

export function resolveApiKeyTarget(principal: Principal, requestedUserId?: string): string {
  requireApiKeyManagement(principal);
  if (!requestedUserId || requestedUserId === principal.userId) return principal.userId;
  if (principal.isPlatformAdmin) return requestedUserId;
  throw new AuthError(
    'Only a platform administrator may manage another user\'s API keys.',
    403,
    'insufficient_permission',
  );
}

export function createApiKeyRoutes(db: Database) {
  const app = new Hono<ApiEnv>();

  app.get('/api/api-keys', async (c) => {
    const principal = c.get('principal');
    requireApiKeyManagement(principal);

    const parsed = apiKeyListSchema.safeParse({ userId: c.req.query('userId') });
    if (!parsed.success) {
      return c.json(
        apiError('invalid_request', 'Invalid API key query.', parsed.error.issues),
        400,
      );
    }

    const userId = resolveApiKeyTarget(principal, parsed.data.userId);
    const [owner] = await db.select({ id: users.id }).from(users).where(eq(users.id, userId)).limit(1);
    if (!owner) return c.json(apiError('not_found', 'User not found.'), 404);

    const rows = await db
      .select()
      .from(apiKeys)
      .where(eq(apiKeys.userId, userId))
      .orderBy(desc(apiKeys.createdAt));
    return c.json(rows.map(apiKeyMetadata));
  });

  app.post(
    '/api/api-keys',
    mutation(db, {
      schema: apiKeyCreateSchema,
      permission: { authorize: requireApiKeyManagement },
      invalidMessage: 'Invalid API key.',
      async mutate({ input, principal, tx, now }) {
        const userId = resolveApiKeyTarget(principal, input.userId);
        const [owner] = await tx
          .select({ id: users.id })
          .from(users)
          .where(eq(users.id, userId))
          .limit(1);
        if (!owner) throw MutationError.notFound('User');

        const expiresAt = input.expiresAt ? new Date(input.expiresAt) : null;
        if (expiresAt && expiresAt <= now) {
          throw new MutationError(
            'invalid_expiry',
            'API key expiry must be in the future.',
            400,
          );
        }

        const material = generateApiKey();
        const scopes = input.scopes.includes('write') ? ['read', 'write'] : ['read'];
        const [created] = await tx
          .insert(apiKeys)
          .values({
            userId,
            name: input.name,
            keyHash: material.keyHash,
            keyPrefix: material.keyPrefix,
            scopes,
            expiresAt,
          })
          .returning();
        if (!created) throw new Error('API key insert returned no row');

        return {
          data: apiKeyCreationResponse(created, material.key),
          activity: {
            type: 'agent_action',
            subject: `Created API key “${created.name}”`,
            meta: {
              action: 'api_key.created',
              apiKeyId: created.id,
              targetUserId: userId,
              keyPrefix: created.keyPrefix,
              scopes: created.scopes,
              expiresAt: created.expiresAt?.toISOString() ?? null,
            },
          },
        };
      },
    }),
  );

  app.delete(
    '/api/api-keys/:id',
    bodylessMutation(db, {
      schema: revokeApiKeySchema,
      permission: { authorize: requireApiKeyManagement },
      invalidMessage: 'Invalid API key revocation.',
      async mutate({ principal, params, tx, now }) {
        const id = params.id;
        if (!id) throw MutationError.notFound('API key');

        // Ownership is part of the lookup so another user's real key and a
        // random UUID are indistinguishable to non-admin callers.
        const visibleKey = principal.isPlatformAdmin
          ? eq(apiKeys.id, id)
          : and(eq(apiKeys.id, id), eq(apiKeys.userId, principal.userId));
        const [record] = await tx.select().from(apiKeys).where(visibleKey).limit(1);
        if (!record) throw MutationError.notFound('API key');

        let revoked = record;
        if (!record.revokedAt) {
          const [updated] = await tx
            .update(apiKeys)
            .set({ revokedAt: now })
            .where(and(eq(apiKeys.id, record.id), isNull(apiKeys.revokedAt)))
            .returning();
          if (updated) {
            revoked = updated;
          } else {
            const [concurrentlyRevoked] = await tx
              .select()
              .from(apiKeys)
              .where(eq(apiKeys.id, record.id))
              .limit(1);
            if (!concurrentlyRevoked) throw MutationError.notFound('API key');
            revoked = concurrentlyRevoked;
          }
        }

        return {
          data: apiKeyMetadata(revoked),
          activity: {
            type: 'agent_action',
            subject: `Revoked API key “${record.name}”`,
            meta: {
              action: 'api_key.revoked',
              apiKeyId: record.id,
              targetUserId: record.userId,
              keyPrefix: record.keyPrefix,
              alreadyRevoked: record.revokedAt !== null,
            },
          },
        };
      },
    }),
  );

  return app;
}
