import { Response, Router } from 'express';
import type { Router as RouterType } from 'express';
import { db } from '@/db';
import { organizations, projects, images } from '@banatie/database';
import { eq, and, isNull, sql } from 'drizzle-orm';
import { ImageService, GenerationService, LiveScopeService } from '@/services/core';
import { StorageFactory } from '@/services/StorageFactory';
import { asyncHandler } from '@/middleware/errorHandler';
import { ipRateLimiterMiddleware } from '@/middleware/ipRateLimiter';
import { computeLiveUrlCacheKey } from '@/utils/helpers';
import { GENERATION_LIMITS, ERROR_MESSAGES } from '@/utils/constants';
import type { LiveGenerationQuery } from '@/types/requests';

export const cdnRouter: RouterType = Router();

let imageService: ImageService;
let generationService: GenerationService;
let liveScopeService: LiveScopeService;

const getImageService = (): ImageService => {
  if (!imageService) {
    imageService = new ImageService();
  }
  return imageService;
};

const getGenerationService = (): GenerationService => {
  if (!generationService) {
    generationService = new GenerationService();
  }
  return generationService;
};

const getLiveScopeService = (): LiveScopeService => {
  if (!liveScopeService) {
    liveScopeService = new LiveScopeService();
  }
  return liveScopeService;
};

/**
 * Serve images by filename or project-scoped alias via public CDN
 *
 * Public CDN endpoint for serving images without authentication:
 * - Supports filename-based access (exact match in storageKey)
 * - Supports project-scoped alias access (@alias-name)
 * - Returns raw image bytes with optimal caching headers
 * - Long-term browser caching (1 year max-age)
 * - No rate limiting (public access)
 *
 * URL structure matches MinIO storage organization for efficient lookups.
 *
 * @route GET /cdn/:orgSlug/:projectSlug/img/:filenameOrAlias
 * @authentication None - Public endpoint
 *
 * @param {string} req.params.orgSlug - Organization slug
 * @param {string} req.params.projectSlug - Project slug
 * @param {string} req.params.filenameOrAlias - Filename or @alias
 *
 * @returns {Buffer} 200 - Image file bytes with Content-Type header
 * @returns {object} 404 - Organization, project, or image not found
 * @returns {object} 500 - CDN or storage error
 *
 * @throws {Error} ORG_NOT_FOUND - Organization does not exist
 * @throws {Error} PROJECT_NOT_FOUND - Project does not exist
 * @throws {Error} IMAGE_NOT_FOUND - Image not found
 * @throws {Error} CDN_ERROR - General CDN error
 *
 * @example
 * // Access by filename
 * GET /cdn/acme/website/img/hero-background.jpg
 *
 * @example
 * // Access by alias
 * GET /cdn/acme/website/img/@hero
 *
 * Response Headers:
 * Content-Type: image/jpeg
 * Content-Length: 245810
 * Cache-Control: public, max-age=31536000
 * X-Image-Id: 550e8400-e29b-41d4-a716-446655440000
 */
cdnRouter.get(
  '/:orgSlug/:projectSlug/img/:filenameOrAlias',
  asyncHandler(async (req: any, res: Response) => {
    const { orgSlug, projectSlug, filenameOrAlias } = req.params;

    try {
      // Resolve organization and project
      const org = await db.query.organizations.findFirst({
        where: eq(organizations.slug, orgSlug),
      });

      if (!org) {
        res.status(404).json({
          success: false,
          error: { message: 'Organization not found', code: 'ORG_NOT_FOUND' },
        });
        return;
      }

      const project = await db.query.projects.findFirst({
        where: and(eq(projects.slug, projectSlug), eq(projects.organizationId, org.id)),
      });

      if (!project) {
        res.status(404).json({
          success: false,
          error: { message: 'Project not found', code: 'PROJECT_NOT_FOUND' },
        });
        return;
      }

      let image;

      // Check if filenameOrAlias is an alias (starts with @)
      if (filenameOrAlias.startsWith('@')) {
        // Lookup by project-scoped alias
        const allImages = await db.query.images.findMany({
          where: and(
            eq(images.projectId, project.id),
            eq(images.alias, filenameOrAlias),
            isNull(images.deletedAt),
          ),
        });

        image = allImages[0] || null;
      } else {
        // Lookup by filename in storageKey
        const allImages = await db.query.images.findMany({
          where: and(eq(images.projectId, project.id), isNull(images.deletedAt)),
        });

        // Find image where storageKey ends with filename
        image = allImages.find((img) => img.storageKey.includes(filenameOrAlias)) || null;
      }

      if (!image) {
        res.status(404).json({
          success: false,
          error: { message: ERROR_MESSAGES.IMAGE_NOT_FOUND, code: 'IMAGE_NOT_FOUND' },
        });
        return;
      }

      // Download image from storage
      // Storage key format: {orgSlug}/{projectSlug}/img/{imageId}
      const storageService = await StorageFactory.getInstance();
      const keyParts = image.storageKey.split('/');

      if (keyParts.length < 4 || keyParts[2] !== 'img') {
        throw new Error('Invalid storage key format');
      }

      const storedOrgSlug = keyParts[0]!;
      const storedProjectSlug = keyParts[1]!;
      const imageId = keyParts[3]!;

      const buffer = await storageService.downloadFile(storedOrgSlug, storedProjectSlug, imageId);

      // Set headers
      res.setHeader('Content-Type', image.mimeType);
      res.setHeader('Content-Length', buffer.length);
      res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1 year
      res.setHeader('X-Image-Id', image.id);

      // Stream image bytes
      res.send(buffer);
    } catch (error) {
      console.error('CDN image serve error:', error);
      res.status(500).json({
        success: false,
        error: {
          message: error instanceof Error ? error.message : 'Failed to serve image',
          code: 'CDN_ERROR',
        },
      });
    }
  }),
);

/**
 * Live URL generation with automatic caching and scope management
 *
 * Public endpoint for on-demand image generation via URL parameters:
 * - No authentication required (public access)
 * - Automatic prompt-based caching (cache key computed from params)
 * - IP-based rate limiting (10 new generations per hour)
 * - Scope-based generation limits
 * - Lazy scope creation (automatic on first use)
 * - Returns raw image bytes with caching headers
 *
 * Cache behavior:
 * - Cache HIT: Returns existing image instantly, no rate limit check
 * - Cache MISS: Generates new image, counts toward IP rate limit
 * - Cache key computed from: prompt + aspectRatio + autoEnhance + template
 * - Cached images stored with meta.isLiveUrl = true
 *
 * Scope management:
 * - Scopes separate generation budgets (e.g., "hero", "gallery")
 * - Auto-created on first use if allowNewLiveScopes = true
 * - Generation limits per scope (default: 30)
 * - Scope stats tracked (currentGenerations, lastGeneratedAt)
 *
 * @route GET /cdn/:orgSlug/:projectSlug/live/:scope
 * @authentication None - Public endpoint
 * @rateLimit 10 new generations per hour per IP (cache hits excluded)
 *
 * @param {string} req.params.orgSlug - Organization slug
 * @param {string} req.params.projectSlug - Project slug
 * @param {string} req.params.scope - Scope identifier (alphanumeric + hyphens + underscores)
 * @param {string} req.query.prompt - Image description (required)
 * @param {string} [req.query.aspectRatio='1:1'] - Aspect ratio (1:1, 16:9, 3:2, 9:16)
 * @param {boolean} [req.query.autoEnhance=false] - Enable prompt enhancement
 * @param {string} [req.query.template] - Enhancement template (photorealistic, illustration, etc.)
 *
 * @returns {Buffer} 200 - Image file bytes with headers
 * @returns {object} 400 - Missing/invalid prompt or scope format
 * @returns {object} 403 - Scope creation disabled
 * @returns {object} 404 - Organization or project not found
 * @returns {object} 429 - Rate limit or scope generation limit exceeded
 * @returns {object} 500 - Generation or storage error
 *
 * @throws {Error} VALIDATION_ERROR - Prompt is required
 * @throws {Error} SCOPE_INVALID_FORMAT - Invalid scope format
 * @throws {Error} ORG_NOT_FOUND - Organization does not exist
 * @throws {Error} PROJECT_NOT_FOUND - Project does not exist
 * @throws {Error} SCOPE_CREATION_DISABLED - New scope creation not allowed
 * @throws {Error} SCOPE_GENERATION_LIMIT_EXCEEDED - Scope limit reached
 * @throws {Error} IP_RATE_LIMIT_EXCEEDED - IP rate limit exceeded
 * @throws {Error} LIVE_URL_ERROR - General generation error
 *
 * @example
 * // Basic generation with caching
 * GET /cdn/acme/website/live/hero-section?prompt=mountain+landscape&aspectRatio=16:9
 *
 * @example
 * // With auto-enhancement
 * GET /cdn/acme/website/live/gallery?prompt=product+photo&autoEnhance=true&template=product
 *
 * Response Headers (Cache HIT):
 * Content-Type: image/jpeg
 * Content-Length: 245810
 * Cache-Control: public, max-age=31536000
 * X-Cache-Status: HIT
 * X-Scope: hero-section
 * X-Image-Id: 550e8400-e29b-41d4-a716-446655440000
 *
 * Response Headers (Cache MISS):
 * Content-Type: image/jpeg
 * Content-Length: 198234
 * Cache-Control: public, max-age=31536000
 * X-Cache-Status: MISS
 * X-Scope: hero-section
 * X-Generation-Id: 660e8400-e29b-41d4-a716-446655440001
 * X-Image-Id: 770e8400-e29b-41d4-a716-446655440002
 * X-RateLimit-Limit: 10
 * X-RateLimit-Remaining: 9
 * X-RateLimit-Reset: 3456
 */
cdnRouter.get(
  '/:orgSlug/:projectSlug/live/:scope',
  ipRateLimiterMiddleware,
  asyncHandler(async (req: any, res: Response) => {
    const { orgSlug, projectSlug, scope } = req.params;
    const { prompt, aspectRatio, autoEnhance, template } = req.query as LiveGenerationQuery;

    const genService = getGenerationService();
    const imgService = getImageService();
    const scopeService = getLiveScopeService();

    try {
      // Validate prompt
      if (!prompt || typeof prompt !== 'string') {
        res.status(400).json({
          success: false,
          error: { message: 'Prompt is required', code: 'VALIDATION_ERROR' },
        });
        return;
      }

      // Validate scope format (alphanumeric + hyphens + underscores)
      if (!/^[a-zA-Z0-9_-]+$/.test(scope)) {
        res.status(400).json({
          success: false,
          error: { message: ERROR_MESSAGES.SCOPE_INVALID_FORMAT, code: 'SCOPE_INVALID_FORMAT' },
        });
        return;
      }

      // Resolve organization
      const org = await db.query.organizations.findFirst({
        where: eq(organizations.slug, orgSlug),
      });

      if (!org) {
        res.status(404).json({
          success: false,
          error: { message: 'Organization not found', code: 'ORG_NOT_FOUND' },
        });
        return;
      }

      // Resolve project
      const project = await db.query.projects.findFirst({
        where: and(eq(projects.slug, projectSlug), eq(projects.organizationId, org.id)),
      });

      if (!project) {
        res.status(404).json({
          success: false,
          error: { message: 'Project not found', code: 'PROJECT_NOT_FOUND' },
        });
        return;
      }

      // Compute cache key
      const normalizedAutoEnhance =
        typeof autoEnhance === 'string' ? autoEnhance === 'true' : Boolean(autoEnhance);

      const cacheParams: {
        aspectRatio?: string;
        autoEnhance?: boolean;
        template?: string;
      } = {};
      if (aspectRatio) cacheParams.aspectRatio = aspectRatio as string;
      if (autoEnhance !== undefined) cacheParams.autoEnhance = normalizedAutoEnhance;
      if (template) cacheParams.template = template as string;

      const cacheKey = computeLiveUrlCacheKey(project.id, scope, prompt, cacheParams);

      // Check cache: find image with meta.liveUrlCacheKey = cacheKey
      const cachedImages = await db.query.images.findMany({
        where: and(
          eq(images.projectId, project.id),
          isNull(images.deletedAt),
          sql`${images.meta}->>'scope' = ${scope}`,
          sql`${images.meta}->>'isLiveUrl' = 'true'`,
          sql`${images.meta}->>'cacheKey' = ${cacheKey}`,
        ),
        limit: 1,
      });

      const cachedImage = cachedImages[0];

      if (cachedImage) {
        // Cache HIT - serve existing image
        // Storage key format: {orgSlug}/{projectSlug}/img/{imageId}
        const storageService = await StorageFactory.getInstance();
        const keyParts = cachedImage.storageKey.split('/');

        if (keyParts.length < 4 || keyParts[2] !== 'img') {
          throw new Error('Invalid storage key format');
        }

        const storedOrgSlug = keyParts[0]!;
        const storedProjectSlug = keyParts[1]!;
        const imageId = keyParts[3]!;

        const buffer = await storageService.downloadFile(storedOrgSlug, storedProjectSlug, imageId);

        // Set headers
        res.setHeader('Content-Type', cachedImage.mimeType);
        res.setHeader('Content-Length', buffer.length);
        res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1 year
        res.setHeader('X-Cache-Status', 'HIT');
        res.setHeader('X-Scope', scope);
        res.setHeader('X-Image-Id', cachedImage.id);

        res.send(buffer);
        return;
      }

      // Cache MISS - check IP rate limit before generating
      // Only count new generations (cache MISS) toward IP rate limit
      const isLimited = (req as any).checkIpRateLimit();
      if (isLimited) {
        return; // Rate limit response already sent
      }

      // Cache MISS - check scope and generate
      // Get or create scope
      let liveScope;
      try {
        liveScope = await scopeService.createOrGet(project.id, scope, {
          allowNewLiveScopes: project.allowNewLiveScopes,
          newLiveScopesGenerationLimit: project.newLiveScopesGenerationLimit,
        });
      } catch (error) {
        if (error instanceof Error && error.message === ERROR_MESSAGES.SCOPE_CREATION_DISABLED) {
          res.status(403).json({
            success: false,
            error: {
              message: ERROR_MESSAGES.SCOPE_CREATION_DISABLED,
              code: 'SCOPE_CREATION_DISABLED',
            },
          });
          return;
        }
        throw error;
      }

      // Check if scope allows new generations
      const scopeStats = await scopeService.getByIdWithStats(liveScope.id);
      const canGenerate = await scopeService.canGenerateNew(
        liveScope,
        scopeStats.currentGenerations,
      );

      if (!canGenerate) {
        res.status(429).json({
          success: false,
          error: {
            message: ERROR_MESSAGES.SCOPE_GENERATION_LIMIT_EXCEEDED,
            code: 'SCOPE_GENERATION_LIMIT_EXCEEDED',
          },
        });
        return;
      }

      // Generate new image (no API key, use system generation)
      const generation = await genService.create({
        projectId: project.id,
        apiKeyId: null as unknown as string, // System generation for live URLs
        organizationSlug: orgSlug,
        projectSlug: projectSlug,
        prompt,
        aspectRatio: (aspectRatio as string) || GENERATION_LIMITS.DEFAULT_ASPECT_RATIO,
        autoEnhance: normalizedAutoEnhance,
        requestId: req.requestId,
      });

      if (!generation.outputImage) {
        throw new Error('Generation succeeded but no output image was created');
      }

      // Update image meta to mark as live URL with cache key and scope
      await imgService.update(generation.outputImage.id, {
        meta: {
          ...generation.outputImage.meta,
          scope,
          isLiveUrl: true,
          cacheKey,
        },
      });

      // Download newly generated image
      // Storage key format: {orgSlug}/{projectSlug}/img/{imageId}
      const storageService = await StorageFactory.getInstance();
      const keyParts = generation.outputImage.storageKey.split('/');

      if (keyParts.length < 4 || keyParts[2] !== 'img') {
        throw new Error('Invalid storage key format');
      }

      const storedOrgSlug = keyParts[0]!;
      const storedProjectSlug = keyParts[1]!;
      const imageId = keyParts[3]!;

      const buffer = await storageService.downloadFile(storedOrgSlug, storedProjectSlug, imageId);

      // Set headers
      res.setHeader('Content-Type', generation.outputImage.mimeType);
      res.setHeader('Content-Length', buffer.length);
      res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1 year
      res.setHeader('X-Cache-Status', 'MISS');
      res.setHeader('X-Scope', scope);
      res.setHeader('X-Generation-Id', generation.id);
      res.setHeader('X-Image-Id', generation.outputImage.id);

      res.send(buffer);
    } catch (error) {
      console.error('Live URL generation error:', error);
      res.status(500).json({
        success: false,
        error: {
          message: error instanceof Error ? error.message : 'Generation failed',
          code: 'LIVE_URL_ERROR',
        },
      });
    }
  }),
);
