import { Request, Response } from 'express';
import crypto from 'crypto';
import path from 'path';
import fs from 'fs';
import { Document } from '@models/Document';
import { User } from '@models/User';
import { WorkflowRequest } from '@models/WorkflowRequest';
import { Participant } from '@models/Participant';
import { ApprovalLevel } from '@models/ApprovalLevel';
import { WorkNote } from '@models/WorkNote';
import { WorkNoteAttachment } from '@models/WorkNoteAttachment';
import { Op } from 'sequelize';
import { ResponseHandler } from '@utils/responseHandler';
import { activityService } from '@services/activity.service';
import { gcsStorageService } from '@services/gcsStorage.service';
import { emailNotificationService } from '@services/emailNotification.service';
import { notificationService } from '@services/notification.service';
import type { AuthenticatedRequest } from '../types/express';
import { getRequestMetadata } from '@utils/requestUtils';
import { getConfigNumber, getConfigValue } from '@services/configReader.service';
import { logDocumentEvent, logWithContext } from '@utils/logger';
import { UPLOAD_DIR } from '../config/storage';
import { Storage } from '@google-cloud/storage';
import logger from '@utils/logger';

export class DocumentController {
  async upload(req: AuthenticatedRequest, res: Response): Promise<void> {
    try {
      const userId = req.user?.userId;
      if (!userId) {
        ResponseHandler.error(res, 'Unauthorized', 401);
        return;
      }

      // Extract requestId from body (multer should parse form fields)
      // Try both req.body and req.body.requestId for compatibility
      const identifier = String((req.body?.requestId || req.body?.request_id || '').trim());
      if (!identifier || identifier === 'undefined' || identifier === 'null') {
        logWithContext('error', 'RequestId missing or invalid in document upload', {
          body: req.body,
          bodyKeys: Object.keys(req.body || {}),
          userId: req.user?.userId
        });
        ResponseHandler.error(res, 'requestId is required', 400);
        return;
      }

      // Helper to check if identifier is UUID
      const isUuid = (id: string): boolean => {
        const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
        return uuidRegex.test(id);
      };

      // Get workflow request - handle both UUID (requestId) and requestNumber
      let workflowRequest: WorkflowRequest | null = null;
      if (isUuid(identifier)) {
        workflowRequest = await WorkflowRequest.findByPk(identifier);
      } else {
        workflowRequest = await WorkflowRequest.findOne({ where: { requestNumber: identifier } });
      }

      if (!workflowRequest) {
        logWithContext('error', 'Workflow request not found for document upload', {
          identifier,
          isUuid: isUuid(identifier),
          userId: req.user?.userId
        });
        ResponseHandler.error(res, 'Workflow request not found', 404);
        return;
      }

      // Get the actual requestId (UUID) and requestNumber
      const requestId = (workflowRequest as any).requestId || (workflowRequest as any).request_id;
      const requestNumber = (workflowRequest as any).requestNumber || (workflowRequest as any).request_number;
      
      if (!requestNumber) {
        logWithContext('error', 'Request number not found for workflow', {
          requestId,
          workflowRequest: JSON.stringify(workflowRequest.toJSON()),
          userId: req.user?.userId
        });
        ResponseHandler.error(res, 'Request number not found for workflow', 500);
        return;
      }

      const file = (req as any).file as Express.Multer.File | undefined;
      if (!file) {
        ResponseHandler.error(res, 'No file uploaded', 400);
        return;
      }

      // Validate file size against database configuration
      const maxFileSizeMB = await getConfigNumber('MAX_FILE_SIZE_MB', 10);
      const maxFileSizeBytes = maxFileSizeMB * 1024 * 1024;
      
      if (file.size > maxFileSizeBytes) {
        ResponseHandler.error(
          res,
          `File size exceeds the maximum allowed size of ${maxFileSizeMB}MB. Current size: ${(file.size / (1024 * 1024)).toFixed(2)}MB`,
          400
        );
        return;
      }

      // Validate file type against database configuration
      const allowedFileTypesStr = await getConfigValue('ALLOWED_FILE_TYPES', 'pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png,gif');
      const allowedFileTypes = allowedFileTypesStr.split(',').map(ext => ext.trim().toLowerCase());
      const fileExtension = path.extname(file.originalname).replace('.', '').toLowerCase();
      
      if (!allowedFileTypes.includes(fileExtension)) {
        ResponseHandler.error(
          res,
          `File type "${fileExtension}" is not allowed. Allowed types: ${allowedFileTypes.join(', ')}`,
          400
        );
        return;
      }

      // Get file buffer
      const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from(''));
      const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
      const extension = path.extname(file.originalname).replace('.', '').toLowerCase();
      const category = (req.body?.category as string) || 'OTHER';

      // Upload with automatic fallback to local storage
      const uploadResult = await gcsStorageService.uploadFileWithFallback({
        buffer: fileBuffer,
        originalName: file.originalname,
        mimeType: file.mimetype,
        requestNumber: requestNumber,
        fileType: 'documents'
      });
      
      const storageUrl = uploadResult.storageUrl;
      const gcsFilePath = uploadResult.filePath;
      
      // Clean up local temporary file if it exists (from multer disk storage)
      if (file.path && fs.existsSync(file.path)) {
        try {
          fs.unlinkSync(file.path);
        } catch (unlinkError) {
          logWithContext('warn', 'Failed to delete local temporary file', { filePath: file.path });
        }
      }

      // Check if storageUrl exceeds database column limit (500 chars)
      // GCS signed URLs can be very long (500-1000+ chars)
      const MAX_STORAGE_URL_LENGTH = 500;
      let finalStorageUrl = storageUrl;
      if (storageUrl && storageUrl.length > MAX_STORAGE_URL_LENGTH) {
        logWithContext('warn', 'Storage URL exceeds database column limit, truncating', {
          originalLength: storageUrl.length,
          maxLength: MAX_STORAGE_URL_LENGTH,
          urlPrefix: storageUrl.substring(0, 100),
        });
        // For signed URLs, we can't truncate as it will break the URL
        // Instead, store null and generate signed URLs on-demand when needed
        // The filePath is sufficient to generate a new signed URL later
        finalStorageUrl = null as any;
        logWithContext('info', 'Storing null storageUrl - will generate signed URL on-demand', {
          filePath: gcsFilePath,
          reason: 'Signed URL too long for database column',
        });
      }

      // Truncate file names if they exceed database column limits (255 chars)
      const MAX_FILE_NAME_LENGTH = 255;
      const originalFileName = file.originalname;
      let truncatedOriginalFileName = originalFileName;
      
      if (originalFileName.length > MAX_FILE_NAME_LENGTH) {
        // Preserve file extension when truncating
        const ext = path.extname(originalFileName);
        const nameWithoutExt = path.basename(originalFileName, ext);
        const maxNameLength = MAX_FILE_NAME_LENGTH - ext.length;
        
        if (maxNameLength > 0) {
          truncatedOriginalFileName = nameWithoutExt.substring(0, maxNameLength) + ext;
        } else {
          // If extension itself is too long, just use the extension
          truncatedOriginalFileName = ext.substring(0, MAX_FILE_NAME_LENGTH);
        }
        
        logWithContext('warn', 'File name truncated to fit database column', {
          originalLength: originalFileName.length,
          truncatedLength: truncatedOriginalFileName.length,
          originalName: originalFileName.substring(0, 100) + '...',
          truncatedName: truncatedOriginalFileName,
        });
      }

      // Generate fileName (basename of the generated file name in GCS)
      const generatedFileName = path.basename(gcsFilePath);
      let truncatedFileName = generatedFileName;
      
      if (generatedFileName.length > MAX_FILE_NAME_LENGTH) {
        const ext = path.extname(generatedFileName);
        const nameWithoutExt = path.basename(generatedFileName, ext);
        const maxNameLength = MAX_FILE_NAME_LENGTH - ext.length;
        
        if (maxNameLength > 0) {
          truncatedFileName = nameWithoutExt.substring(0, maxNameLength) + ext;
        } else {
          truncatedFileName = ext.substring(0, MAX_FILE_NAME_LENGTH);
        }
        
        logWithContext('warn', 'Generated file name truncated', {
          originalLength: generatedFileName.length,
          truncatedLength: truncatedFileName.length,
        });
      }

      // Prepare document data
      const documentData = {
        requestId,
        uploadedBy: userId,
        fileName: truncatedFileName,
        originalFileName: truncatedOriginalFileName,
        fileType: extension,
        fileExtension: extension,
        fileSize: file.size,
        filePath: gcsFilePath, // Store GCS path or local path
        storageUrl: finalStorageUrl, // Store GCS URL or local URL (null if too long)
        mimeType: file.mimetype,
        checksum,
        isGoogleDoc: false,
        googleDocUrl: null as any,
        category,
        version: 1,
        parentDocumentId: null as any,
        isDeleted: false,
        downloadCount: 0,
      };

      logWithContext('info', 'Creating document record', {
        requestId,
        userId,
        fileName: file.originalname,
        filePath: gcsFilePath,
        storageUrl: storageUrl,
        documentData: JSON.stringify(documentData, null, 2),
      });

      let doc;
      try {
        doc = await Document.create(documentData as any);
        logWithContext('info', 'Document record created successfully', {
          documentId: doc.documentId,
          requestId,
          fileName: file.originalname,
        });
      } catch (createError) {
        const createErrorMessage = createError instanceof Error ? createError.message : 'Unknown error';
        const createErrorStack = createError instanceof Error ? createError.stack : undefined;
        // Check if it's a Sequelize validation error
        const sequelizeError = (createError as any)?.errors || (createError as any)?.parent;
        logWithContext('error', 'Document.create() failed', {
          error: createErrorMessage,
          stack: createErrorStack,
          sequelizeErrors: sequelizeError,
          requestId,
          userId,
          fileName: file.originalname,
          filePath: gcsFilePath,
          storageUrl: storageUrl,
          documentData: JSON.stringify(documentData, null, 2),
        });
        throw createError; // Re-throw to be caught by outer catch block
      }

      // Log document upload event
      logDocumentEvent('uploaded', doc.documentId, {
        requestId,
        userId,
        fileName: file.originalname,
        fileType: extension,
        fileSize: file.size,
        category,
      });

      // Get user details for activity logging
      const user = await User.findByPk(userId);
      const uploaderName = (user as any)?.displayName || (user as any)?.email || 'User';
      
      // Log activity for document upload
      const requestMeta = getRequestMetadata(req);
      await activityService.log({
        requestId,
        type: 'document_added',
        user: { userId, name: uploaderName },
        timestamp: new Date().toISOString(),
        action: 'Document Added',
        details: `Added ${file.originalname} as supporting document by ${uploaderName}`,
        metadata: { 
          fileName: file.originalname, 
          fileSize: file.size, 
          fileType: extension,
          category 
        },
        ipAddress: requestMeta.ipAddress,
        userAgent: requestMeta.userAgent
      });

      // Send notifications for additional document added
      try {
        const initiatorId = (workflowRequest as any).initiatorId || (workflowRequest as any).initiator_id;
        const isInitiator = userId === initiatorId;

        // Get all participants (spectators)
        const spectators = await Participant.findAll({
          where: { 
            requestId,
            participantType: 'SPECTATOR'
          },
          include: [{
            model: User,
            as: 'user',
            attributes: ['userId', 'email', 'displayName']
          }]
        });

        // Get current approver (pending or in-progress approval level)
        const currentApprovalLevel = await ApprovalLevel.findOne({
          where: {
            requestId,
            status: { [Op.in]: ['PENDING', 'IN_PROGRESS'] }
          },
          order: [['levelNumber', 'ASC']],
          include: [{
            model: User,
            as: 'approver',
            attributes: ['userId', 'email', 'displayName']
          }]
        });

        logWithContext('info', 'Current approver lookup for document notification', {
          requestId,
          currentApprovalLevelFound: !!currentApprovalLevel,
          approverUserId: currentApprovalLevel ? ((currentApprovalLevel as any).approver || (currentApprovalLevel as any).Approver)?.userId : null,
          isInitiator
        });

        // Determine who to notify based on who uploaded
        const recipientsToNotify: Array<{ userId: string; email: string; displayName: string }> = [];

        if (isInitiator) {
          // Initiator added → notify spectators and current approver
          spectators.forEach((spectator: any) => {
            const spectatorUser = spectator.user || spectator.User;
            if (spectatorUser && spectatorUser.userId !== userId) {
              recipientsToNotify.push({
                userId: spectatorUser.userId,
                email: spectatorUser.email,
                displayName: spectatorUser.displayName || spectatorUser.email
              });
            }
          });

          if (currentApprovalLevel) {
            const approverUser = (currentApprovalLevel as any).approver || (currentApprovalLevel as any).Approver;
            if (approverUser && approverUser.userId !== userId) {
              recipientsToNotify.push({
                userId: approverUser.userId,
                email: approverUser.email,
                displayName: approverUser.displayName || approverUser.email
              });
            }
          }
        } else {
          // Check if uploader is a spectator
          const uploaderParticipant = await Participant.findOne({
            where: {
              requestId,
              userId,
              participantType: 'SPECTATOR'
            }
          });

          if (uploaderParticipant) {
            // Spectator added → notify initiator and current approver
            const initiator = await User.findByPk(initiatorId);
            if (initiator) {
              const initiatorData = initiator.toJSON();
              if (initiatorData.userId !== userId) {
                recipientsToNotify.push({
                  userId: initiatorData.userId,
                  email: initiatorData.email,
                  displayName: initiatorData.displayName || initiatorData.email
                });
              }
            }

            if (currentApprovalLevel) {
              const approverUser = (currentApprovalLevel as any).approver || (currentApprovalLevel as any).Approver;
              if (approverUser && approverUser.userId !== userId) {
                recipientsToNotify.push({
                  userId: approverUser.userId,
                  email: approverUser.email,
                  displayName: approverUser.displayName || approverUser.email
                });
              }
            }
          } else {
            // Approver added → notify initiator and spectators
            const initiator = await User.findByPk(initiatorId);
            if (initiator) {
              const initiatorData = initiator.toJSON();
              if (initiatorData.userId !== userId) {
                recipientsToNotify.push({
                  userId: initiatorData.userId,
                  email: initiatorData.email,
                  displayName: initiatorData.displayName || initiatorData.email
                });
              }
            }

            spectators.forEach((spectator: any) => {
              const spectatorUser = spectator.user || spectator.User;
              if (spectatorUser && spectatorUser.userId !== userId) {
                recipientsToNotify.push({
                  userId: spectatorUser.userId,
                  email: spectatorUser.email,
                  displayName: spectatorUser.displayName || spectatorUser.email
                });
              }
            });
          }
        }

        // Send notifications (email, in-app, and web-push)
        const requestData = {
          requestNumber: requestNumber,
          requestId: requestId,
          title: (workflowRequest as any).title || 'Request'
        };

        // Prepare user IDs for in-app and web-push notifications
        const recipientUserIds = recipientsToNotify.map(r => r.userId);

        // Send in-app and web-push notifications
        if (recipientUserIds.length > 0) {
          try {
            await notificationService.sendToUsers(
              recipientUserIds,
              {
                title: 'Additional Document Added',
                body: `${uploaderName} added "${file.originalname}" to ${requestNumber}`,
                requestId,
                requestNumber,
                url: `/request/${requestNumber}`,
                type: 'document_added',
                priority: 'MEDIUM',
                actionRequired: false,
                metadata: {
                  documentName: file.originalname,
                  fileSize: file.size,
                  addedByName: uploaderName,
                  source: 'Documents Tab'
                }
              }
            );
            logWithContext('info', 'In-app and web-push notifications sent for additional document', {
              requestId,
              documentName: file.originalname,
              recipientsCount: recipientUserIds.length
            });
          } catch (notifyError) {
            logWithContext('error', 'Failed to send in-app/web-push notifications for additional document', {
              requestId,
              error: notifyError instanceof Error ? notifyError.message : 'Unknown error'
            });
          }
        }

        // Send email notifications
        for (const recipient of recipientsToNotify) {
          await emailNotificationService.sendAdditionalDocumentAdded(
            requestData,
            recipient,
            {
              documentName: file.originalname,
              fileSize: file.size,
              addedByName: uploaderName,
              source: 'Documents Tab'
            }
          );
        }

        logWithContext('info', 'Additional document notifications sent', {
          requestId,
          documentName: file.originalname,
          recipientsCount: recipientsToNotify.length,
          isInitiator
        });
      } catch (notifyError) {
        // Don't fail document upload if notifications fail
        logWithContext('error', 'Failed to send additional document notifications', {
          requestId,
          error: notifyError instanceof Error ? notifyError.message : 'Unknown error'
        });
      }

      ResponseHandler.success(res, doc, 'File uploaded', 201);
    } catch (error) {
      const message = error instanceof Error ? error.message : 'Unknown error';
      const errorStack = error instanceof Error ? error.stack : undefined;
      logWithContext('error', 'Document upload failed', {
        userId: req.user?.userId,
        requestId: req.body?.requestId || req.body?.request_id,
        body: req.body,
        bodyKeys: Object.keys(req.body || {}),
        file: req.file ? {
          originalname: req.file.originalname,
          size: req.file.size,
          mimetype: req.file.mimetype,
          hasBuffer: !!req.file.buffer,
          hasPath: !!req.file.path
        } : 'No file',
        error: message,
        stack: errorStack
      });
      ResponseHandler.error(res, 'Upload failed', 500, message);
    }
  }

  /**
   * Helper function to create proper Content-Disposition header
   */
  private createContentDisposition(disposition: 'inline' | 'attachment', filename: string): string {
    const cleanFilename = filename
      .replace(/[<>:"|?*\x00-\x1F\x7F]/g, '_')
      .replace(/\\/g, '_')
      .trim();

    const hasNonASCII = /[^\x00-\x7F]/.test(filename);

    if (hasNonASCII) {
      const encodedFilename = encodeURIComponent(filename);
      return `${disposition}; filename="${cleanFilename}"; filename*=UTF-8''${encodedFilename}`;
    } else {
      return `${disposition}; filename="${cleanFilename}"`;
    }
  }

  /**
   * Preview or Download a standard workflow document
   */
  async getWorkflowDocument(req: AuthenticatedRequest, res: Response, mode: 'preview' | 'download'): Promise<void> {
    try {
      const { documentId } = req.params;
      const userRole = req.user?.role;
      const userId = req.user?.userId;

      const document = await Document.findOne({ where: { documentId } });
      if (!document) {
        ResponseHandler.error(res, 'Document not found', 404);
        return;
      }

      // Authorization Check
      if (userRole !== 'ADMIN' && userRole !== 'MANAGEMENT') {
        const participant = await Participant.findOne({
          where: { requestId: document.requestId, userId, isActive: true }
        });

        if (!participant) {
          ResponseHandler.error(res, 'Access denied. You are not a participant in this workflow.', 403);
          return;
        }

        const canAccess = mode === 'download' ? participant.canDownloadDocuments : participant.canViewDocuments;
        if (!canAccess) {
          ResponseHandler.error(res, `Access denied. You do not have permission to ${mode} documents in this workflow.`, 403);
          return;
        }
      }

      return this.serveFile(res, {
        storageUrl: (document as any).storageUrl || (document as any).storage_url,
        filePath: (document as any).filePath || (document as any).file_path,
        fileName: (document as any).originalFileName || (document as any).original_file_name || (document as any).fileName,
        mimeType: (document as any).mimeType || (document as any).mime_type,
        mode
      });
    } catch (error) {
      logger.error(`[DocumentController] Error getting workflow document:`, error);
      ResponseHandler.error(res, 'Failed to access document', 500);
    }
  }

  /**
   * Preview or Download a work note attachment
   */
  async getWorkNoteAttachment(req: AuthenticatedRequest, res: Response, mode: 'preview' | 'download'): Promise<void> {
    try {
      const { attachmentId } = req.params;
      const userRole = req.user?.role;
      const userId = req.user?.userId;

      const attachment = await WorkNoteAttachment.findOne({ where: { attachmentId } });
      if (!attachment) {
        ResponseHandler.error(res, 'Attachment not found', 404);
        return;
      }

      const note = await WorkNote.findOne({ where: { noteId: attachment.noteId } });
      if (!note) {
        ResponseHandler.error(res, 'Associated work note not found', 404);
        return;
      }

      // Authorization Check (Work note attachments follow general document permissions)
      if (userRole !== 'ADMIN' && userRole !== 'MANAGEMENT') {
        const participant = await Participant.findOne({
          where: { requestId: note.requestId, userId, isActive: true }
        });

        if (!participant) {
          ResponseHandler.error(res, 'Access denied. You are not a participant in this workflow.', 403);
          return;
        }

        const canAccess = mode === 'download' ? participant.canDownloadDocuments : participant.canViewDocuments;
        if (!canAccess) {
          ResponseHandler.error(res, `Access denied. You do not have permission to ${mode} documentation in this workflow.`, 403);
          return;
        }
      }

      return this.serveFile(res, {
        storageUrl: (attachment as any).storageUrl || (attachment as any).storage_url,
        filePath: (attachment as any).filePath || (attachment as any).file_path,
        fileName: (attachment as any).fileName || (attachment as any).file_name,
        mimeType: (attachment as any).fileType || (attachment as any).file_type,
        mode
      });
    } catch (error) {
      logger.error(`[DocumentController] Error getting work note attachment:`, error);
      ResponseHandler.error(res, 'Failed to access attachment', 500);
    }
  }

  /**
   * Common logic to serve files from GCS or local storage
   */
  private async serveFile(res: Response, options: { 
    storageUrl?: string, 
    filePath?: string, 
    fileName: string, 
    mimeType?: string, 
    mode: 'preview' | 'download' 
  }): Promise<void> {
    const { storageUrl, filePath, fileName, mimeType, mode } = options;
    const isGcsUrl = storageUrl && (storageUrl.startsWith('https://storage.googleapis.com') || storageUrl.startsWith('gs://'));

    // Set CORS and basic headers
    res.setHeader('Access-Control-Expose-Headers', 'Content-Type, Content-Disposition');
    const dispositionType = mode === 'download' ? 'attachment' : (mimeType?.includes('pdf') || mimeType?.includes('image') ? 'inline' : 'attachment');
    res.setHeader('Content-Disposition', this.createContentDisposition(dispositionType, fileName));
    res.contentType(mimeType || 'application/octet-stream');

    if (isGcsUrl) {
      res.redirect(storageUrl!);
      return;
    }

    // Stream from GCS if filePath is a GCS path
    if (!storageUrl && filePath && (filePath.startsWith('requests/') || filePath.startsWith('worknotes/'))) {
      try {
        const keyFilePath = process.env.GCP_KEY_FILE || '';
        const bucketName = process.env.GCP_BUCKET_NAME || '';
        const resolvedKeyPath = path.isAbsolute(keyFilePath) ? keyFilePath : path.resolve(process.cwd(), keyFilePath);

        const storage = new Storage({
          projectId: process.env.GCP_PROJECT_ID || '',
          keyFilename: resolvedKeyPath,
        });

        const bucket = storage.bucket(bucketName);
        const file = bucket.file(filePath);

        const [exists] = await file.exists();
        if (!exists) {
          ResponseHandler.error(res, 'File not found in storage', 404);
          return;
        }

        file.createReadStream()
          .on('error', (err) => {
            logger.error('[DocumentController] GCS Stream Error:', err);
            if (!res.headersSent) ResponseHandler.error(res, 'Streaming failed', 500);
          })
          .pipe(res);
        return;
      } catch (err) {
        logger.error('[DocumentController] GCS Access Error:', err);
        ResponseHandler.error(res, 'Failed to access cloud storage', 500);
        return;
      }
    }

    // Local file handling
    const absolutePath = filePath && !path.isAbsolute(filePath) ? path.join(UPLOAD_DIR, filePath) : filePath;
    if (absolutePath && fs.existsSync(absolutePath)) {
      res.sendFile(absolutePath, (err) => {
        if (err && !res.headersSent) ResponseHandler.error(res, 'Failed to send file', 500);
      });
      return;
    }

    ResponseHandler.error(res, 'File not found on server', 404);
  }
}

export const documentController = new DocumentController();


