/**
 * CDN Pipeline routes — admin asset management, purge, and origin config.
 * @module cdn/routes
 */

import type { FastifyInstance } from 'fastify';
import {
  UnauthorizedError,
  ForbiddenError,
  NotFoundError,
  ConflictError,
} from '../../lib/errors.js';
import { getRequestProductId } from '../../lib/request-context.js';
import {
  UploadAssetSchema,
  PurgeRequestSchema,
  UpdateOriginConfigSchema,
  type CdnAssetDoc,
  type CdnPurgeRequestDoc,
  type CdnOriginConfigDoc,
  type AssetCategory,
} from './types.js';
import * as repo from './repository.js';

function requireAuth(req: { jwtPayload?: { sub: string } }): string {
  if (!req.jwtPayload?.sub) throw new UnauthorizedError('Authentication required');
  return req.jwtPayload.sub;
}

function requireAdmin(req: { jwtPayload?: { sub: string; role?: string } }): string {
  const userId = requireAuth(req);
  if (req.jwtPayload?.role !== 'admin') throw new ForbiddenError('Admin access required');
  return userId;
}

export async function cdnRoutes(app: FastifyInstance): Promise<void> {
  // ── Register asset (after blob upload) ─────────────────────
  app.post('/cdn/assets', async (req, reply) => {
    const userId = requireAdmin(req);
    const productId = getRequestProductId(req);
    const input = UploadAssetSchema.parse(req.body);

    // Dedup by content hash
    const existing = await repo.getAssetByHash(productId, input.contentHash);
    if (existing) {
      throw new ConflictError(
        `Asset with hash ${input.contentHash} already exists: ${existing.id}`
      );
    }

    const now = new Date().toISOString();
    const cdnBase = process.env.CDN_BASE_URL ?? 'https://cdn.bytelyst.com';
    const doc: CdnAssetDoc = {
      id: `asset_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
      productId,
      originalName: input.originalName,
      blobPath: input.blobPath,
      cdnUrl: `${cdnBase}/${productId}/${input.contentHash}/${input.originalName}`,
      contentHash: input.contentHash,
      mimeType: input.mimeType,
      sizeBytes: input.sizeBytes,
      category: input.category,
      metadata: input.metadata,
      uploadedBy: userId,
      isPublic: input.isPublic,
      createdAt: now,
      updatedAt: now,
    };

    const created = await repo.createAsset(doc);
    req.log.info({ assetId: created.id, userId }, 'CDN asset registered');
    reply.status(201);
    return created;
  });

  // ── List assets ────────────────────────────────────────────
  app.get('/cdn/assets', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const {
      category,
      limit: limitStr,
      offset: offsetStr,
    } = req.query as {
      category?: string;
      limit?: string;
      offset?: string;
    };

    const parsedLimit = limitStr ? parseInt(limitStr, 10) : 50;
    const parsedOffset = offsetStr ? parseInt(offsetStr, 10) : 0;
    const safeLimit =
      Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 200) : 50;
    const safeOffset = Number.isFinite(parsedOffset) && parsedOffset >= 0 ? parsedOffset : 0;

    return repo.listAssets(productId, {
      category: category as AssetCategory | undefined,
      limit: safeLimit,
      offset: safeOffset,
    });
  });

  // ── Get single asset ───────────────────────────────────────
  app.get<{ Params: { id: string } }>('/cdn/assets/:id', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;
    const asset = await repo.getAsset(id, productId);
    if (!asset) throw new NotFoundError('CDN asset not found');
    return asset;
  });

  // ── Delete asset ───────────────────────────────────────────
  app.delete<{ Params: { id: string } }>('/cdn/assets/:id', async (req, reply) => {
    const userId = requireAdmin(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;

    const deleted = await repo.deleteAsset(id, productId);
    if (!deleted) throw new NotFoundError('CDN asset not found');

    req.log.info({ assetId: id, userId }, 'CDN asset deleted');
    reply.status(204);
    return;
  });

  // ── Storage stats ──────────────────────────────────────────
  app.get('/cdn/stats', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    return repo.getStorageStats(productId);
  });

  // ── Purge request ──────────────────────────────────────────
  app.post('/cdn/purge', async (req, reply) => {
    const userId = requireAdmin(req);
    const productId = getRequestProductId(req);
    const input = PurgeRequestSchema.parse(req.body);

    const now = new Date().toISOString();
    const doc: CdnPurgeRequestDoc = {
      id: `purge_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
      productId,
      patterns: input.patterns,
      status: 'pending',
      purgedCount: 0,
      requestedBy: userId,
      completedAt: null,
      error: null,
      createdAt: now,
    };

    const created = await repo.createPurgeRequest(doc);

    // In production, this would enqueue an async job via the event bus.
    // For MVP, mark as completed immediately (simulating CDN purge).
    await repo.updatePurgeStatus(created.id, productId, 'completed', input.patterns.length);

    req.log.info({ purgeId: created.id, patterns: input.patterns, userId }, 'CDN purge requested');
    reply.status(201);
    return { ...created, status: 'completed', purgedCount: input.patterns.length };
  });

  // ── List purge history ─────────────────────────────────────
  app.get('/cdn/purges', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const { limit: limitStr } = req.query as { limit?: string };
    const parsedLimit = limitStr ? parseInt(limitStr, 10) : 20;
    const safeLimit =
      Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 100) : 20;
    return { purges: await repo.listPurgeRequests(productId, safeLimit) };
  });

  // ── Get origin config ──────────────────────────────────────
  app.get('/cdn/config', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const config = await repo.getOriginConfig(productId);

    if (!config) {
      // Return defaults
      return {
        productId,
        customDomain: null,
        defaultCacheTtlSeconds: 86400,
        categoryTtlOverrides: {},
        corsOrigins: ['*'],
        brotliEnabled: true,
        webpAutoConvert: true,
      };
    }

    return config;
  });

  // ── Update origin config ───────────────────────────────────
  app.put('/cdn/config', async req => {
    const userId = requireAdmin(req);
    const productId = getRequestProductId(req);
    const input = UpdateOriginConfigSchema.parse(req.body);

    const existing = await repo.getOriginConfig(productId);
    const now = new Date().toISOString();

    const doc: CdnOriginConfigDoc = {
      id: productId, // singleton per product
      productId,
      customDomain: input.customDomain ?? existing?.customDomain ?? null,
      defaultCacheTtlSeconds:
        input.defaultCacheTtlSeconds ?? existing?.defaultCacheTtlSeconds ?? 86400,
      categoryTtlOverrides: input.categoryTtlOverrides ?? existing?.categoryTtlOverrides ?? {},
      corsOrigins: input.corsOrigins ?? existing?.corsOrigins ?? ['*'],
      brotliEnabled: input.brotliEnabled ?? existing?.brotliEnabled ?? true,
      webpAutoConvert: input.webpAutoConvert ?? existing?.webpAutoConvert ?? true,
      updatedAt: now,
      updatedBy: userId,
    };

    const saved = await repo.upsertOriginConfig(doc);
    req.log.info({ productId, userId }, 'CDN origin config updated');
    return saved;
  });
}
