import { Application } from 'express';
import { Resource } from 'express-automatic-routes';
import { FileProvider } from '#providers/FileProvider';
import { Req, Res } from '#interfaces/IApi';
import { queryModifier } from '#middlewares/query-modifier';
import verify from '#middlewares/auth';
import { validateZod, validateQueryZod } from '#middlewares/validators';
import { FileUploadBodyValidationSchema } from '#contracts/file/schema';
import { QueryParamsSchema } from '#contracts/shared';
import { sanitizeText } from '#utils/stringUtils';
import { getStorageAdapter } from '#services/storage/adapters/StorageFactory';
import { processAndUpload } from '#services/file-system/fileUploadPipelineService';
import { Errors, GenericError } from '#interfaces/error';
import { FileVariant } from '#models/FileVariant';
import { File as _File } from '#models/File';
import { upload } from '#services/file-system/uploadService';
import { uploadRateLimit } from '#middlewares/rate-limiter';
import { sendSuccess } from '#utils/responseUtils';
import Config from '#config';

export default (_express: Application) => {
	const fileProvider = FileProvider.getInstance();

	return <Resource>{
		get: {
			middleware: [verify, validateQueryZod(QueryParamsSchema), queryModifier],
			handler: async (req: Req, res: Res) => {
				try {
					const isAdmin = req.user?.roles?.some((r) => ['admin', 'system_admin'].includes(r));
					const filter = req.payload?.filters || {};

					// CQ-08: enforce max pageSize of 50 to bound concurrent MinIO calls
					const requestPayload = (req.payload || {}) as Record<string, any>;
					if (requestPayload.limit != null && requestPayload.limit > 50) {
						requestPayload.limit = 50;
					}

					const where = {
						...filter,
						...(isAdmin ? {} : { owner_user_id: req.user?.id }),
					};

					const data = await fileProvider.getAll({ ...requestPayload, where } as any);

					// Inject thumbnail_url into each file item for gallery/list rendering
					// CQ-07: build new array of plain objects instead of mutating Sequelize instances in-place
					const storageAdapter = getStorageAdapter();
					const rawItems: unknown[] = Array.isArray(data)
						? data
						: ((data as { rows?: unknown[]; items?: unknown[] }).rows ??
							(data as { rows?: unknown[]; items?: unknown[] }).items ??
							[]);

					// PERF-02: throttle concurrent MinIO presigned URL calls (configurable via env)
					const CONCURRENCY = Config.featureFlags.fileListUrlConcurrency;
					const enrichedItems: Record<string, unknown>[] = [];
					for (let i = 0; i < rawItems.length; i += CONCURRENCY) {
						const chunk = await Promise.all(
							rawItems.slice(i, i + CONCURRENCY).map(async (item: unknown) => {
								const f = (item as any).toJSON();
								const thumbnail_url =
									f.provider === 'local' || !f.object_key
										? null
										: await storageAdapter.getSignedUrl(f.object_key as string, 3600);
								return { ...f, thumbnail_url };
							}),
						);
						enrichedItems.push(...chunk);
					}

					// Preserve original shape (paginated object or plain array)
					let responseData: unknown;
					if (Array.isArray(data)) {
						responseData = enrichedItems;
					} else {
						const paged = data as Record<string, unknown>;
						responseData = { ...paged, rows: enrichedItems };
					}

					return sendSuccess(res, responseData);
				} catch (error) {
					return res.error(error);
				}
			},
		},
		post: {
			middleware: [verify, queryModifier, uploadRateLimit, upload, validateZod(FileUploadBodyValidationSchema)],
			handler: async (req: Req, res: Res) => {
				const file = req.file;
				if (!file) {
					return res.error(
						Errors.validation([
							{
								field: 'file',
								code: 'MISSING_FILE_FIELDS',
								message: { vi: 'Không có file được upload', en: 'No file was uploaded' },
							},
						]),
					);
				}

				const storageAdapter = getStorageAdapter();
				let uploaded: Awaited<ReturnType<typeof processAndUpload>> | undefined;
				let dbRecordId: string | undefined;

				try {
					uploaded = await processAndUpload(file, storageAdapter);

					// SEC-07: Sanitize all user-provided string fields
					const title = sanitizeText(req.body.title, 150) ?? '';
					const description = sanitizeText(req.body.description, 500) ?? '';
					const note = sanitizeText(req.body.note, 255) ?? '';
					const category = sanitizeText((uploaded.fileType || '').toLowerCase(), 50) || null;
					const rawProvider = uploaded.storageProvider;
					const bucketName: string =
						rawProvider !== 'local'
							? (sanitizeText(Config.storage.minio.bucket || Config.storage.minio.s3Bucket || '', 255) ?? '')
							: '';

					// LOGIC-02: Wrap file and variant creation in transaction with compensating logic
					const transaction = await fileProvider.transaction();
					try {
						const data = await fileProvider.create(
							{
								id: crypto.randomUUID(),
								visibility: 'private',
								created_at: new Date(),
								updated_at: new Date(),
								original_name: uploaded.decodedName,
								provider: rawProvider,
								bucket_name: bucketName,
								object_key: uploaded.storagePath,
								mime_type: file.mimetype,
								category,
								size_bytes: file.size,
								title,
								description,
								note,
								checksum_sha_256: uploaded.checksum,
								is_library: req.body.is_library === 'true' || req.body.is_library === true,
								tenant_id: req.user?.tenantId || null,
								owner_user_id: req.user?.id || null,
								status: 'uploaded',
								created_by: req.user?.id || null,
							},
							{ transaction },
						);

						dbRecordId = data.id;

						if (uploaded.compressInfo) {
							await FileVariant.bulkCreate(
								Object.entries(uploaded.compressInfo).map(([variant_name, object_key]) => ({
									id: crypto.randomUUID(),
									file_id: data.id,
									variant_name,
									provider: rawProvider,
									bucket_name: bucketName,
									object_key,
									mime_type: 'image/webp',
									created_at: new Date(),
								})),
								{ ignoreDuplicates: true, transaction },
							);
						}

						await transaction.commit();

						// LOGIC-02: Send response after successful commit
						// If response fails, the file record remains in DB (status: uploaded) for cleanup job
						return sendSuccess(res, data.toJSON());
					} catch (error) {
						// Transaction failed - rollback and cleanup storage
						await transaction.rollback();
						console.error('[FileUpload] Transaction failed:', error);
						throw error;
					}
				} catch (error) {
					// LOGIC-02: Only cleanup storage if DB transaction failed (uploaded variable set but no dbRecordId)
					// If dbRecordId exists, the transaction succeeded and file should be kept
					console.error('[FileUpload] Upload failed:', error);
					if (uploaded && !dbRecordId) {
						try {
							await storageAdapter.delete(uploaded.storagePath);
						} catch (deleteError) {
							console.error('[FileUpload] Storage deletion failed:', deleteError);
							return res.error(new GenericError('FILE_UPLOAD_FAILED'));
						}
					}
					return res.error(error);
				}
			},
		},
	};
};
