// src/routes/admin.routes.ts
import { Router, NextFunction, Request, Response } from 'express';
import passport from './passport.routes';
import { isAdmin } from './passport.routes'; // Correctly imported
import multer from 'multer';// --- Zod Schemas for Admin Routes (as per ADR-003) ---
import { z } from 'zod';

import * as db from '../services/db/index.db';
import { logger } from '../services/logger.server';
import { UserProfile } from '../types';
import { geocodingService } from '../services/geocodingService.server';
import { requireFileUpload } from '../middleware/fileUpload.middleware'; // This was a duplicate, fixed.
import { NotFoundError, ValidationError } from '../services/db/errors.db';
import { validateRequest } from '../middleware/validation.middleware';

// --- Bull Board (Job Queue UI) Imports ---
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ExpressAdapter } from '@bull-board/express';

import type { Queue } from 'bullmq';
import { backgroundJobService } from '../services/backgroundJobService';
import { flyerQueue, emailQueue, analyticsQueue, cleanupQueue, weeklyAnalyticsQueue, flyerWorker, emailWorker, analyticsWorker, cleanupWorker, weeklyAnalyticsWorker } from '../services/queueService.server'; // Import your queues
import { getSimpleWeekAndYear } from '../utils/dateUtils';

// Helper for consistent required string validation (handles missing/null/empty)
const requiredString = (message: string) => z.preprocess((val) => val ?? '', z.string().min(1, message));

/**
 * A factory for creating a Zod schema that validates a UUID in the request parameters.
 * @param key The name of the parameter key (e.g., 'userId').
 * @param message A custom error message for invalid UUIDs.
 */
const uuidParamSchema = (key: string, message = `Invalid UUID for parameter '${key}'.`) => z.object({
  params: z.object({ [key]: z.string().uuid({ message }) }),
});

/**
 * A factory for creating a Zod schema that validates a numeric ID in the request parameters.
 */
const numericIdParamSchema = (key: string, message = `Invalid ID for parameter '${key}'. Must be a positive integer.`) => z.object({
  params: z.object({ [key]: z.coerce.number().int({ message }).positive({ message }) }),
});

const updateCorrectionSchema = numericIdParamSchema('id').extend({
  body: z.object({
    suggested_value: requiredString('A new suggested_value is required.'),
  }),
});

const updateRecipeStatusSchema = numericIdParamSchema('id').extend({
  body: z.object({
    status: z.enum(['private', 'pending_review', 'public', 'rejected']),
  }),
});

const updateCommentStatusSchema = numericIdParamSchema('id').extend({
  body: z.object({
    status: z.enum(['visible', 'hidden', 'reported']),
  }),
});

const updateUserRoleSchema = uuidParamSchema('id', 'A valid user ID is required.').extend({
  body: z.object({
    role: z.enum(['user', 'admin']),
  }),
});

const activityLogSchema = z.object({
  query: z.object({
    limit: z.coerce.number().int().positive().optional().default(50),
    offset: z.coerce.number().int().nonnegative().optional().default(0),
  }),
});

const jobRetrySchema = z.object({
  params: z.object({
    queueName: z.enum(['flyer-processing', 'email-sending', 'analytics-reporting', 'file-cleanup', 'weekly-analytics-reporting']),
    jobId: requiredString('A valid Job ID is required.'),
  }),
});

const router = Router();

// --- Multer Configuration for File Uploads ---
const storagePath = process.env.STORAGE_PATH || '/var/www/flyer-crawler.projectium.com/flyer-images';
const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, storagePath);
    },
    filename: function (req, file, cb) {
        const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
        cb(null, file.fieldname + '-' + uniqueSuffix + '-' + file.originalname);
    }
});
const upload = multer({ storage: storage });

// --- Bull Board (Job Queue UI) Setup ---
const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath('/api/admin/jobs'); // Set the base path for the UI

createBullBoard({
  queues: [
    new BullMQAdapter(flyerQueue),
    new BullMQAdapter(emailQueue),
    new BullMQAdapter(analyticsQueue),
    new BullMQAdapter(cleanupQueue),
    new BullMQAdapter(weeklyAnalyticsQueue), // Add the weekly analytics queue to the board
  ],
  options: {
    uiConfig: {
      boardTitle: 'Bull Dashboard',
    },
  },
  serverAdapter: serverAdapter,
});

// Mount the Bull Board UI router. This must be done BEFORE the isAdmin middleware
// so the UI's own assets can be served, but the routes within it will be protected
// by the router-level `isAdmin` middleware below.
router.use('/jobs', serverAdapter.getRouter());

// --- Middleware for all admin routes ---
router.use(passport.authenticate('jwt', { session: false }), isAdmin);

// --- Admin Routes ---

router.get('/corrections', async (req, res, next: NextFunction) => {
  try {
    const corrections = await db.adminRepo.getSuggestedCorrections(req.log);
    res.json(corrections);
  } catch (error) {
    next(error);
  }
});

router.get('/brands', async (req, res, next: NextFunction) => {
  try {
    const brands = await db.flyerRepo.getAllBrands(req.log);
    res.json(brands);
  } catch (error) {
    next(error);
  }
});

router.get('/stats', async (req, res, next: NextFunction) => {
  try {
    const stats = await db.adminRepo.getApplicationStats(req.log);
    res.json(stats);
  } catch (error) {
    next(error);
  }
});

router.get('/stats/daily', async (req, res, next: NextFunction) => {
  try {
    const dailyStats = await db.adminRepo.getDailyStatsForLast30Days(req.log);
    res.json(dailyStats);
  } catch (error) {
    next(error);
  }
});

router.post('/corrections/:id/approve', validateRequest(numericIdParamSchema('id')), async (req: Request, res: Response, next: NextFunction) => {
    // Apply ADR-003 pattern for type safety
    const { params } = req as unknown as z.infer<ReturnType<typeof numericIdParamSchema>>;
    try {
        await db.adminRepo.approveCorrection(params.id, req.log); // params.id is now safely typed as number
        res.status(200).json({ message: 'Correction approved successfully.' });
    } catch (error) {
        next(error);
    }
});

router.post('/corrections/:id/reject', validateRequest(numericIdParamSchema('id')), async (req: Request, res: Response, next: NextFunction) => {
    // Apply ADR-003 pattern for type safety
    const { params } = req as unknown as z.infer<ReturnType<typeof numericIdParamSchema>>;
    try {
        await db.adminRepo.rejectCorrection(params.id, req.log); // params.id is now safely typed as number
        res.status(200).json({ message: 'Correction rejected successfully.' });
    } catch (error) {
        next(error);
    }
});

router.put('/corrections/:id', validateRequest(updateCorrectionSchema), async (req: Request, res: Response, next: NextFunction) => {
    // Apply ADR-003 pattern for type safety
    const { params, body } = req as unknown as z.infer<typeof updateCorrectionSchema>;
    try {
        const updatedCorrection = await db.adminRepo.updateSuggestedCorrection(params.id, body.suggested_value, req.log);
        res.status(200).json(updatedCorrection);
    } catch (error) {
        next(error);
    }
});

router.put('/recipes/:id/status', validateRequest(updateRecipeStatusSchema), async (req: Request, res: Response, next: NextFunction) => {
    // Apply ADR-003 pattern for type safety
    const { params, body } = req as unknown as z.infer<typeof updateRecipeStatusSchema>;
    try {
        const updatedRecipe = await db.adminRepo.updateRecipeStatus(params.id, body.status, req.log); // This is still a standalone function in admin.db.ts
        res.status(200).json(updatedRecipe);
    } catch (error) {
        next(error); // Pass all errors to the central error handler
    }
});

router.post('/brands/:id/logo', validateRequest(numericIdParamSchema('id')), upload.single('logoImage'), requireFileUpload('logoImage'), async (req: Request, res: Response, next: NextFunction) => {
    // Apply ADR-003 pattern for type safety
    const { params } = req as unknown as z.infer<ReturnType<typeof numericIdParamSchema>>;
    try {
        // Although requireFileUpload middleware should ensure the file exists,
        // this check satisfies TypeScript and adds robustness.
        if (!req.file) {
            throw new ValidationError([], 'Logo image file is missing.');
        }
        const logoUrl = `/assets/${req.file.filename}`;
        await db.adminRepo.updateBrandLogo(params.id, logoUrl, req.log);

        logger.info({ brandId: params.id, logoUrl }, `Brand logo updated for brand ID: ${params.id}`);
        res.status(200).json({ message: 'Brand logo updated successfully.', logoUrl });
    } catch (error) {
        next(error);
    }
});

router.get('/unmatched-items', async (req, res, next: NextFunction) => {
    try {
    const items = await db.adminRepo.getUnmatchedFlyerItems(req.log);
    res.json(items);
    } catch (error) {
        next(error);
    }
});

/**
 * DELETE /api/admin/recipes/:recipeId - Admin endpoint to delete any recipe.
 */
router.delete('/recipes/:recipeId', validateRequest(numericIdParamSchema('recipeId')), async (req: Request, res: Response, next: NextFunction) => {
    const adminUser = req.user as UserProfile;
    // Infer the type directly from the schema generator function.
    const { params } = req as unknown as z.infer<ReturnType<typeof numericIdParamSchema>>;
    try {
        // The isAdmin flag bypasses the ownership check in the repository method.
        await db.recipeRepo.deleteRecipe(params.recipeId, adminUser.user_id, true, req.log);
        res.status(204).send();
    } catch (error: unknown) {
        next(error);
    }
});

/**
 * DELETE /api/admin/flyers/:flyerId - Admin endpoint to delete a flyer and its items.
 */
router.delete('/flyers/:flyerId', validateRequest(numericIdParamSchema('flyerId')), async (req: Request, res: Response, next: NextFunction) => {
    // Infer the type directly from the schema generator function.
    const { params } = req as unknown as z.infer<ReturnType<typeof numericIdParamSchema>>;
    try {
        await db.flyerRepo.deleteFlyer(params.flyerId, req.log);
        res.status(204).send();
    } catch (error: unknown) {
        next(error);
    }
});

router.put('/comments/:id/status', validateRequest(updateCommentStatusSchema), async (req: Request, res: Response, next: NextFunction) => {
    // Apply ADR-003 pattern for type safety
    const { params, body } = req as unknown as z.infer<typeof updateCommentStatusSchema>;
    try {
        const updatedComment = await db.adminRepo.updateRecipeCommentStatus(params.id, body.status, req.log); // This is still a standalone function in admin.db.ts
        res.status(200).json(updatedComment);
    } catch (error: unknown) {
        next(error);
    }
});

router.get('/users', async (req, res, next: NextFunction) => {
    try {
    const users = await db.adminRepo.getAllUsers(req.log);
    res.json(users);
    } catch (error) {
        next(error);
    }
});

router.get('/activity-log', validateRequest(activityLogSchema), async (req: Request, res: Response, next: NextFunction) => {
    // Apply ADR-003 pattern for type safety.
    // We explicitly coerce query params here because the validation middleware might not
    // replace req.query with the coerced values in all environments.
    const query = req.query as unknown as { limit?: string; offset?: string };
    const limit = query.limit ? Number(query.limit) : 50;
    const offset = query.offset ? Number(query.offset) : 0;

    try {
        const logs = await db.adminRepo.getActivityLog(limit, offset, req.log);
        res.json(logs);
    } catch (error) {
        next(error);
    }
});

router.get('/users/:id', validateRequest(uuidParamSchema('id', 'A valid user ID is required.')), async (req: Request, res: Response, next: NextFunction) => {
    // Apply ADR-003 pattern for type safety
    const { params } = req as unknown as z.infer<ReturnType<typeof uuidParamSchema>>;
    try {
        const user = await db.userRepo.findUserProfileById(params.id, req.log);
        res.json(user);
    } catch (error) {
        next(error);
    }
});

router.put('/users/:id', validateRequest(updateUserRoleSchema), async (req: Request, res: Response, next: NextFunction) => {
    // Apply ADR-003 pattern for type safety
    const { params, body } = req as unknown as z.infer<typeof updateUserRoleSchema>;
    try {
        const updatedUser = await db.adminRepo.updateUserRole(params.id, body.role, req.log);
        res.json(updatedUser);
    } catch (error) {
        logger.error({ error }, `Error updating user ${params.id}:`);
        next(error);
    }
});

router.delete('/users/:id', validateRequest(uuidParamSchema('id', 'A valid user ID is required.')), async (req: Request, res: Response, next: NextFunction) => {
    const adminUser = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { params } = req as unknown as z.infer<ReturnType<typeof uuidParamSchema>>;
    try {
        if (adminUser.user.user_id === params.id) {
            throw new ValidationError([], 'Admins cannot delete their own account.');
        }
        await db.userRepo.deleteUserById(params.id, req.log);
        res.status(204).send();
    } catch (error) {
        next(error);
    }
});

/**
 * POST /api/admin/trigger/daily-deal-check - Manually trigger the daily deal check job.
 * This is useful for testing or forcing an update without waiting for the cron schedule.
 */
router.post('/trigger/daily-deal-check', async (req: Request, res: Response, next: NextFunction) => {
    const adminUser = req.user as UserProfile;
    logger.info(`[Admin] Manual trigger for daily deal check received from user: ${adminUser.user_id}`);

    try {
        // We call the function but don't wait for it to finish (no `await`).
        // This is a "fire-and-forget" operation from the client's perspective.
        backgroundJobService.runDailyDealCheck();
        res.status(202).json({ message: 'Daily deal check job has been triggered successfully. It will run in the background.' });
    } catch (error) {
        logger.error({ error }, '[Admin] Failed to trigger daily deal check job.');
        next(error);
    }
});

/**
 * POST /api/admin/trigger/analytics-report - Manually enqueue a job to generate the daily analytics report.
 * This is useful for testing or re-generating a report without waiting for the cron schedule.
 */
router.post('/trigger/analytics-report', async (req: Request, res: Response, next: NextFunction) => {
    const adminUser = req.user as UserProfile;
    logger.info(`[Admin] Manual trigger for analytics report generation received from user: ${adminUser.user_id}`);

    try {
        const reportDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
        // Use a unique job ID for manual triggers to distinguish them from scheduled jobs.
        const jobId = `manual-report-${reportDate}-${Date.now()}`;

        const job = await analyticsQueue.add('generate-daily-report', { reportDate }, { jobId });

        res.status(202).json({ message: `Analytics report generation job has been enqueued successfully. Job ID: ${job.id}` });
    } catch (error) {
        logger.error({ error }, '[Admin] Failed to enqueue analytics report job.');
        next(error);
    }
});

/**
 * POST /api/admin/flyers/:flyerId/cleanup - Enqueue a job to clean up a flyer's files.
 * This is triggered by an admin after they have verified the flyer processing was successful.
 */
router.post('/flyers/:flyerId/cleanup', validateRequest(numericIdParamSchema('flyerId')), async (req: Request, res: Response, next: NextFunction) => {
    const adminUser = req.user as UserProfile;
    // Infer type from the schema generator for type safety, as per ADR-003.
    const { params } = req as unknown as z.infer<ReturnType<typeof numericIdParamSchema>>;
    logger.info(`[Admin] Manual trigger for flyer file cleanup received from user: ${adminUser.user_id} for flyer ID: ${params.flyerId}`);

    // Enqueue the cleanup job. The worker will handle the file deletion.
    try {
        await cleanupQueue.add('cleanup-flyer-files', { flyerId: params.flyerId });
        res.status(202).json({ message: `File cleanup job for flyer ID ${params.flyerId} has been enqueued.` });
    } catch (error) {
        next(error);
    }
});

/**
 * POST /api/admin/trigger/failing-job - Enqueue a test job designed to fail.
 * This is for testing the retry mechanism and Bull Board UI.
 */
router.post('/trigger/failing-job', async (req: Request, res: Response, next: NextFunction) => {
    const adminUser = req.user as UserProfile;
    logger.info(`[Admin] Manual trigger for a failing job received from user: ${adminUser.user_id}`);

    try {
        // Add a job with a special 'forceFail' flag that the worker will recognize.
        const job = await analyticsQueue.add('generate-daily-report', { reportDate: 'FAIL' });
        res.status(202).json({ message: `Failing test job has been enqueued successfully. Job ID: ${job.id}` });
    } catch (error) {
        next(error);
    }
});

/**
 * POST /api/admin/system/clear-geocode-cache - Clears the Redis cache for geocoded addresses.
 * Requires admin privileges.
 */
router.post('/system/clear-geocode-cache', async (req: Request, res: Response, next: NextFunction) => {
    const adminUser = req.user as UserProfile;
    logger.info(`[Admin] Manual trigger for geocode cache clear received from user: ${adminUser.user_id}`);

    try {
        const keysDeleted = await geocodingService.clearGeocodeCache(req.log);
        res.status(200).json({ message: `Successfully cleared the geocode cache. ${keysDeleted} keys were removed.` });
    } catch (error) {
        logger.error({ error }, '[Admin] Failed to clear geocode cache.');
        next(error);
    }
});

/**
 * GET /api/admin/workers/status - Get the current running status of all BullMQ workers.
 * This is useful for a system health dashboard to see if any workers have crashed.
 */
router.get('/workers/status', async (req: Request, res: Response) => {
    const workers = [flyerWorker, emailWorker, analyticsWorker, cleanupWorker, weeklyAnalyticsWorker ];
    
    const workerStatuses = await Promise.all(
        workers.map(async (worker) => {
            return {
                name: worker.name,
                isRunning: worker.isRunning(),
            };
        })
    );

    res.json(workerStatuses);
});

/**
 * GET /api/admin/queues/status - Get job counts for all BullMQ queues.
 * This is useful for monitoring the health and backlog of background jobs.
 */
router.get('/queues/status', async (req: Request, res: Response, next: NextFunction) => {
    try {
        const queues = [flyerQueue, emailQueue, analyticsQueue, cleanupQueue, weeklyAnalyticsQueue];
        
        const queueStatuses = await Promise.all(
            queues.map(async (queue) => {
                return {
                    name: queue.name,
                    counts: await queue.getJobCounts('waiting', 'active', 'completed', 'failed', 'delayed', 'paused'),
                };
            })
        );
        res.json(queueStatuses);
    } catch (error) {
        next(error);
    }
});

/**
 * POST /api/admin/jobs/:queueName/:jobId/retry - Retries a specific failed job.
 */
router.post('/jobs/:queueName/:jobId/retry', validateRequest(jobRetrySchema), async (req: Request, res: Response, next: NextFunction) => {
    const adminUser = req.user as UserProfile;
    const { params: { queueName, jobId } } = req as unknown as z.infer<typeof jobRetrySchema>;
    
    const queueMap: { [key: string]: Queue } = {
        'flyer-processing': flyerQueue,
        'email-sending': emailQueue,
        'analytics-reporting': analyticsQueue,
        'file-cleanup': cleanupQueue,
    };

    const queue = queueMap[queueName];

    if (!queue) {
        // Throw a NotFoundError to be handled by the central error handler.
        throw new NotFoundError(`Queue '${queueName}' not found.`);
    }

    try {
        const job = await queue.getJob(jobId);
        if (!job) throw new NotFoundError(`Job with ID '${jobId}' not found in queue '${queueName}'.`);

        const jobState = await job.getState();
        if (jobState !== 'failed') throw new ValidationError([], `Job is not in a 'failed' state. Current state: ${jobState}.`);

        await job.retry();
        logger.info(`[Admin] User ${adminUser.user_id} manually retried job ${jobId} in queue ${queueName}.`);
        res.status(200).json({ message: `Job ${jobId} has been successfully marked for retry.` });
    } catch (error) {
        next(error);
    }
});

/**
 * POST /api/admin/trigger/weekly-analytics - Manually trigger the weekly analytics report job.
 */
router.post('/trigger/weekly-analytics', async (req: Request, res: Response, next: NextFunction) => {
  const adminUser = req.user as UserProfile;
  logger.info(`[Admin] Manual trigger for weekly analytics report received from user: ${adminUser.user_id}`);

  try {
    const { year: reportYear, week: reportWeek } = getSimpleWeekAndYear();
    const { weeklyAnalyticsQueue } = await import('../services/queueService.server');
    const job = await weeklyAnalyticsQueue.add('generate-weekly-report', { reportYear, reportWeek }, {
      jobId: `manual-weekly-report-${reportYear}-${reportWeek}-${Date.now()}` // Add timestamp to avoid ID conflict
    });

    res.status(202).json({ message: 'Successfully enqueued weekly analytics job.', jobId: job.id });
  } catch (error) {
    next(error);
  }
});

export default router;