/**
 * Blob storage REST endpoints.
 *
 * POST   /blob/sas              — generate SAS URL for direct browser upload/download
 * GET    /blob/list             — list blobs in a container (with optional prefix)
 * DELETE /blob/delete           — delete a blob
 * GET    /blob/info                         — get blob metadata
 * GET    /blob/info/:container/*           — legacy path-style blob metadata lookup
 * GET    /blob/containers       — list available containers and their status
 */

import type { FastifyInstance } from 'fastify';
import { verifyToken } from '../auth/jwt.js';
import { BadRequestError, UnauthorizedError, NotFoundError } from '../../lib/errors.js';
import {
  getBucket,
  generateSasUrl,
  isBlobStorageConfigured,
  BLOB_CONTAINERS,
} from '../../lib/blob.js';
import {
  GenerateSasSchema,
  ListBlobsSchema,
  DeleteBlobSchema,
  BlobInfoQuerySchema,
  type BlobInfo,
} from './types.js';

const USER_ALLOWED_CONTAINERS = new Set<string>([
  BLOB_CONTAINERS.audio,
  BLOB_CONTAINERS.transcripts,
  BLOB_CONTAINERS.attachments,
  BLOB_CONTAINERS.avatars,
]);

async function requireAuth(req: { headers: Record<string, string | string[] | undefined> }) {
  const authHeader = req.headers.authorization;
  if (!authHeader || typeof authHeader !== 'string') {
    throw new UnauthorizedError('Missing Authorization header');
  }
  const token = authHeader.replace(/^Bearer\s+/i, '');
  return verifyToken(token);
}

function isAdminRole(role: unknown): boolean {
  return role === 'admin' || role === 'super_admin';
}

function userPrefix(auth: { productId?: string; sub: string }): string {
  if (!auth.productId) return `${auth.sub}/`;
  return `${auth.productId}/${auth.sub}/`;
}

function permissionsInclude(permissions: string, chars: string): boolean {
  for (const ch of chars) {
    if (permissions.includes(ch)) return true;
  }
  return false;
}

function normalizePrefix(input: string | undefined): string | undefined {
  if (!input) return undefined;
  return input.replace(/^\/+/, '');
}

export async function blobRoutes(app: FastifyInstance) {
  async function getBlobInfo(
    container: string,
    blobName: string,
    auth: Awaited<ReturnType<typeof requireAuth>>
  ) {
    if (!Object.values(BLOB_CONTAINERS).includes(container as never)) {
      throw new BadRequestError(`Invalid container: ${container}`);
    }

    const admin = isAdminRole(auth.role);
    if (!admin) {
      if (!USER_ALLOWED_CONTAINERS.has(container)) {
        throw new UnauthorizedError(`Not allowed to access container: ${container}`);
      }
      const prefix = userPrefix({ productId: auth.productId, sub: auth.sub });
      if (!blobName.startsWith(prefix)) {
        throw new UnauthorizedError('Not allowed to access blobs outside your user scope');
      }
    }

    const bucket = await getBucket(container);

    const exists = await bucket.exists(blobName);
    if (!exists) {
      throw new NotFoundError(`Blob not found: ${container}/${blobName}`);
    }

    const blobs = await bucket.list(blobName);
    const meta = blobs.find(blob => blob.key === blobName);
    return {
      name: blobName,
      container,
      contentType: meta?.contentType,
      size: meta?.size ?? 0,
      lastModified: meta?.lastModified,
      url: blobName,
      metadata: meta?.metadata ?? {},
    };
  }

  // Generate SAS URL for direct upload/download
  app.post('/blob/sas', async req => {
    const auth = await requireAuth(req);
    const parsed = GenerateSasSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const { container, blobName, permissions, expiresInMinutes } = parsed.data;

    const admin = isAdminRole(auth.role);
    if (!admin) {
      if (!USER_ALLOWED_CONTAINERS.has(container)) {
        throw new UnauthorizedError(`Not allowed to access container: ${container}`);
      }

      const prefix = userPrefix({ productId: auth.productId, sub: auth.sub });
      if (!blobName.startsWith(prefix)) {
        throw new UnauthorizedError('Not allowed to access blobs outside your user scope');
      }

      // Never grant delete permissions to non-admins.
      if (permissionsInclude(permissions, 'd')) {
        throw new UnauthorizedError('Only admins can generate delete SAS tokens');
      }
    }

    const sasUrl = await generateSasUrl(container, blobName, permissions, expiresInMinutes);
    return {
      sasUrl,
      container,
      blobName,
      permissions,
      expiresInMinutes,
      expiresAt: new Date(Date.now() + expiresInMinutes * 60 * 1000).toISOString(),
    };
  });

  // List blobs in a container
  app.get('/blob/list', async req => {
    const auth = await requireAuth(req);
    const parsed = ListBlobsSchema.safeParse(req.query);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const admin = isAdminRole(auth.role);
    const { container, limit } = parsed.data;

    if (!admin && !USER_ALLOWED_CONTAINERS.has(container)) {
      throw new UnauthorizedError(`Not allowed to access container: ${container}`);
    }

    const requestedPrefix = normalizePrefix(parsed.data.prefix);
    const effectivePrefix = admin
      ? requestedPrefix
      : `${userPrefix({ productId: auth.productId, sub: auth.sub })}${requestedPrefix ?? ''}`;

    const bucket = await getBucket(container);
    const allBlobs = await bucket.list(effectivePrefix || undefined);

    const blobs: BlobInfo[] = allBlobs.slice(0, limit).map(blob => ({
      name: blob.key,
      container,
      contentType: blob.contentType,
      size: blob.size ?? 0,
      lastModified: blob.lastModified,
      url: blob.key,
      metadata: blob.metadata ?? {},
    }));

    return { blobs, count: blobs.length, container, prefix: effectivePrefix || null };
  });

  // Delete a blob
  app.delete('/blob/delete', async req => {
    const auth = await requireAuth(req);
    if (auth.role !== 'admin') {
      throw new UnauthorizedError('Only admins can delete blobs');
    }

    const parsed = DeleteBlobSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const { container, blobName } = parsed.data;
    const bucket = await getBucket(container);

    const exists = await bucket.exists(blobName);
    if (!exists) {
      throw new NotFoundError(`Blob not found: ${container}/${blobName}`);
    }

    await bucket.delete(blobName);
    return { success: true, container, blobName };
  });

  // Get blob metadata/info
  app.get('/blob/info', async req => {
    const auth = await requireAuth(req);
    const parsed = BlobInfoQuerySchema.safeParse(req.query);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    return getBlobInfo(parsed.data.container, parsed.data.blobName, auth);
  });

  // Legacy path-style blob metadata/info
  app.get('/blob/info/:container/:blobName', async req => {
    const auth = await requireAuth(req);
    const { container, blobName } = req.params as { container: string; blobName: string };
    return getBlobInfo(container, blobName, auth);
  });

  app.get('/blob/info/:container/*', async req => {
    const auth = await requireAuth(req);
    const { container } = req.params as { container: string };
    const blobName = (req.params as Record<string, string>)['*'];
    return getBlobInfo(container, blobName, auth);
  });

  // List available containers and config status
  app.get('/blob/containers', async req => {
    await requireAuth(req);
    return {
      configured: isBlobStorageConfigured(),
      containers: Object.entries(BLOB_CONTAINERS).map(([key, name]) => ({
        key,
        name,
      })),
    };
  });
}
