import { Storage } from '@google-cloud/storage';
import path from 'path';
import fs from 'fs';
import logger from '@utils/logger';
import { UPLOAD_DIR } from '@config/storage';

interface UploadFileOptions {
  filePath?: string;
  buffer?: Buffer;
  originalName: string;
  mimeType: string;
  requestNumber: string; // Request number (e.g., 'REQ-2025-12-0001')
  fileType: 'documents' | 'attachments'; // Type of file: documents or attachments
}

/** CPC/CSD uploads — same GCS vs local rules as {@link uploadFileWithFallback}. */
export interface UploadCpcCsdFileOptions {
  buffer: Buffer;
  originalName: string;
  mimeType: string;
  channel: 'csd' | 'cpc';
  /** Booking / claim id (caller may pre-sanitize; service sanitizes again) */
  bookingSegment: string;
  /** When set, used as the final filename (no path segments). Otherwise same pattern as workflow documents. */
  fileName?: string;
}

interface UploadResult {
  storageUrl: string;
  filePath: string; // GCS path
  fileName: string; // Generated file name in GCS
}

class GCSStorageService {
  private storage: Storage | null = null;
  private bucketName: string = '';
  private projectId: string = '';

  private getAbsoluteUploadPath(relativePath: string): string {
    return path.join(UPLOAD_DIR, ...relativePath.replace(/\\/g, '/').split('/').filter(Boolean));
  }

  private renameLocalStoredFile(oldNorm: string, newRelativePath: string, newFileName: string): {
    storageUrl: string;
    filePath: string;
    fileName: string;
  } {
    const fullOld = this.getAbsoluteUploadPath(oldNorm);
    const fullNew = this.getAbsoluteUploadPath(newRelativePath);
    if (!fs.existsSync(fullOld)) {
      throw new Error(`Local file not found: ${oldNorm}`);
    }
    fs.mkdirSync(path.dirname(fullNew), { recursive: true });
    fs.renameSync(fullOld, fullNew);
    const normalizedPath = newRelativePath.replace(/\\/g, '/');
    const storageUrl = `/uploads/${normalizedPath}`;
    logger.info('[GCS] Renamed local Form 16 document', { oldNorm, newRelativePath: normalizedPath });
    return { storageUrl, filePath: normalizedPath, fileName: newFileName };
  }

  constructor() {
    // Check if Google Secret Manager should be used
    const useGoogleSecretManager = process.env.USE_GOOGLE_SECRET_MANAGER === 'true';
    
    if (!useGoogleSecretManager) {
      logger.info('[GCS] USE_GOOGLE_SECRET_MANAGER is not enabled. Will use local storage fallback.');
      return;
    }

    this.projectId = process.env.GCP_PROJECT_ID || '';
    this.bucketName = process.env.GCP_BUCKET_NAME || '';
    const keyFilePath = process.env.GCP_KEY_FILE || '';

    if (!this.projectId || !this.bucketName || !keyFilePath) {
      logger.warn('[GCS] GCP configuration missing. File uploads will use local storage fallback.');
      return;
    }

    try {
      // Resolve key file path (can be relative or absolute)
      const resolvedKeyPath = path.isAbsolute(keyFilePath)
        ? keyFilePath
        : path.resolve(process.cwd(), keyFilePath);

      if (!fs.existsSync(resolvedKeyPath)) {
        logger.error(`[GCS] Key file not found at: ${resolvedKeyPath}. Will use local storage fallback.`);
        return;
      }

      this.storage = new Storage({
        projectId: this.projectId,
        keyFilename: resolvedKeyPath,
      });

      logger.info('[GCS] Initialized successfully', {
        projectId: this.projectId,
        bucketName: this.bucketName,
      });
    } catch (error) {
      logger.error('[GCS] Failed to initialize. Will use local storage fallback:', error);
    }
  }

  /**
   * Ensure the bucket exists, create it if it doesn't
   * This is called lazily on first upload
   */
  private async ensureBucketExists(): Promise<void> {
    if (!this.storage) {
      throw new Error('GCS storage not initialized');
    }

    try {
      const bucket = this.storage.bucket(this.bucketName);
      const [exists] = await bucket.exists();

      if (!exists) {
        logger.info(`[GCS] Bucket "${this.bucketName}" does not exist. Creating...`);
        
        // Get region from env or default to asia-south1 (Mumbai)
        const region = process.env.GCP_BUCKET_REGION || 'asia-south1';
        
        // Create bucket with default settings
        // Note: publicAccessPrevention is not set to allow public file access
        // If you need private buckets, set GCP_BUCKET_PUBLIC=false and use signed URLs
        const bucketOptions: any = {
          location: region,
          storageClass: 'STANDARD',
          uniformBucketLevelAccess: true,
        };
        
        // Only enforce public access prevention if explicitly configured
        if (process.env.GCP_BUCKET_PUBLIC === 'false') {
          bucketOptions.publicAccessPrevention = 'enforced';
        }
        
        await bucket.create(bucketOptions);

        logger.info(`[GCS] Bucket "${this.bucketName}" created successfully in region "${region}"`);
      } else {
        logger.debug(`[GCS] Bucket "${this.bucketName}" already exists`);
      }
    } catch (error) {
      logger.error(`[GCS] Failed to check/create bucket "${this.bucketName}":`, error);
      throw error;
    }
  }

  /**
   * Upload a file to Google Cloud Storage
   * @param options File upload options
   * @returns Upload result with storage URL and file path
   */
  async uploadFile(options: UploadFileOptions): Promise<UploadResult> {
    if (!this.storage) {
      throw new Error('GCS storage not initialized. Check GCP configuration.');
    }

    const { filePath, buffer, originalName, mimeType, requestNumber, fileType } = options;

    if (!filePath && !buffer) {
      throw new Error('Either filePath or buffer must be provided');
    }

    if (!requestNumber) {
      throw new Error('Request number is required for file upload');
    }

    try {
      // Ensure bucket exists before uploading
      await this.ensureBucketExists();
      
      // Generate unique file name with original name first for readability
      // Format: originalName-timestamp-hash.ext (e.g., proposal-1766490022228-qjlojs.pdf)
      const timestamp = Date.now();
      const randomHash = Math.random().toString(36).substring(2, 8);
      const safeName = originalName.replace(/[^a-zA-Z0-9._-]/g, '_');
      const extension = path.extname(originalName);
      // Extract name without extension, then add timestamp and hash before extension
      const nameWithoutExt = safeName.substring(0, safeName.length - extension.length);
      const fileName = `${nameWithoutExt}-${timestamp}-${randomHash}${extension}`;
      
      // Build GCS path: requests/{requestNumber}/{fileType}/{fileName}
      // Example: requests/REQ-2025-12-0001/documents/proposal.pdf
      // Example: requests/REQ-2025-12-0001/attachments/approval_note.pdf
      const gcsFilePath = `requests/${requestNumber}/${fileType}/${fileName}`;

      const bucket = this.storage.bucket(this.bucketName);
      const file = bucket.file(gcsFilePath);

      // Upload options
      const uploadOptions: any = {
        metadata: {
          contentType: mimeType,
          metadata: {
            originalName: originalName,
            uploadedAt: new Date().toISOString(),
          },
        },
      };

      // Upload from buffer or file path
      if (buffer) {
        await file.save(buffer, uploadOptions);
      } else if (filePath) {
        await bucket.upload(filePath, {
          destination: gcsFilePath,
          metadata: uploadOptions.metadata,
        });
      }

      // Make file publicly readable (or use signed URLs for private access)
      // Note: This will fail if bucket has publicAccessPrevention enabled
      let publicUrl: string;
      try {
        await file.makePublic();
        // Get public URL
        publicUrl = `https://storage.googleapis.com/${this.bucketName}/${gcsFilePath}`;
      } catch (makePublicError: any) {
        // If making public fails (e.g., public access prevention), use signed URL
        if (makePublicError?.code === 400 || makePublicError?.message?.includes('publicAccessPrevention')) {
          logger.warn('[GCS] Cannot make file public (public access prevention enabled). Using signed URL.');
          publicUrl = await this.getSignedUrl(gcsFilePath, 60 * 24 * 365); // 1 year expiry
        } else {
          throw makePublicError;
        }
      }

      logger.info('[GCS] File uploaded successfully', {
        fileName: originalName,
        gcsPath: gcsFilePath,
        storageUrl: publicUrl,
        size: buffer ? buffer.length : 'unknown',
      });

      return {
        storageUrl: publicUrl,
        filePath: gcsFilePath,
        fileName: fileName,
      };
    } catch (error) {
      logger.error('[GCS] Upload failed:', error);
      throw new Error(`Failed to upload file to GCS: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  /**
   * Delete a file from Google Cloud Storage
   * @param gcsFilePath The GCS file path (e.g., 'attachments/file-name.ext')
   */
  async deleteFile(gcsFilePath: string): Promise<void> {
    if (!this.storage) {
      throw new Error('GCS storage not initialized. Check GCP configuration.');
    }

    try {
      const bucket = this.storage.bucket(this.bucketName);
      const file = bucket.file(gcsFilePath);
      await file.delete();

      logger.info('[GCS] File deleted successfully', { gcsPath: gcsFilePath });
    } catch (error) {
      logger.error('[GCS] Delete failed:', error);
      throw new Error(`Failed to delete file from GCS: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  /**
   * Get a signed URL for private file access (valid for 1 hour by default)
   * @param gcsFilePath The GCS file path
   * @param expiresInMinutes URL expiration time in minutes (default: 60)
   * @returns Signed URL
   */
  async getSignedUrl(gcsFilePath: string, expiresInMinutes: number = 60): Promise<string> {
    if (!this.storage) {
      throw new Error('GCS storage not initialized. Check GCP configuration.');
    }

    try {
      const bucket = this.storage.bucket(this.bucketName);
      const file = bucket.file(gcsFilePath);

      const [url] = await file.getSignedUrl({
        action: 'read',
        expires: Date.now() + expiresInMinutes * 60 * 1000,
      });

      return url;
    } catch (error) {
      logger.error('[GCS] Failed to generate signed URL:', error);
      throw new Error(`Failed to generate signed URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  /**
   * Save file to local storage with the same folder structure as GCS
   * This is used as a fallback when GCS is not configured or fails
   * @param options File upload options
   * @returns Upload result with local storage URL and file path
   */
  saveToLocalStorage(options: UploadFileOptions): UploadResult {
    const { buffer, originalName, requestNumber, fileType } = options;

    if (!buffer) {
      throw new Error('Buffer is required for local storage fallback');
    }

    if (!requestNumber) {
      throw new Error('Request number is required for file upload');
    }

    try {
      // Generate unique file name (same format as GCS) - original name first for readability
      // Format: originalName-timestamp-hash.ext (e.g., proposal-1766490022228-qjlojs.pdf)
      const timestamp = Date.now();
      const randomHash = Math.random().toString(36).substring(2, 8);
      const safeName = originalName.replace(/[^a-zA-Z0-9._-]/g, '_');
      const extension = path.extname(originalName);
      // Extract name without extension, then add timestamp and hash before extension
      const nameWithoutExt = safeName.substring(0, safeName.length - extension.length);
      const fileName = `${nameWithoutExt}-${timestamp}-${randomHash}${extension}`;

      // Build local path: uploads/requests/{requestNumber}/{fileType}/{fileName}
      // This matches the GCS structure: requests/{requestNumber}/{fileType}/{fileName}
      const localDir = path.join(UPLOAD_DIR, 'requests', requestNumber, fileType);
      
      // Ensure directory exists
      if (!fs.existsSync(localDir)) {
        fs.mkdirSync(localDir, { recursive: true });
      }

      const localFilePath = path.join(localDir, fileName);
      const relativePath = `requests/${requestNumber}/${fileType}/${fileName}`;

      // Save file to disk
      fs.writeFileSync(localFilePath, buffer);

      // Create URL path (will be served by express.static)
      const storageUrl = `/uploads/${relativePath}`;

      logger.info('[GCS] File saved to local storage (fallback)', {
        fileName: originalName,
        localPath: relativePath,
        storageUrl: storageUrl,
        requestNumber: requestNumber,
      });

      return {
        storageUrl: storageUrl,
        filePath: relativePath, // Store relative path (same format as GCS path)
        fileName: fileName,
      };
    } catch (error) {
      logger.error('[GCS] Local storage save failed:', error);
      throw new Error(`Failed to save file to local storage: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  private cpcCsdSanitizeBookingSegment(segment: string): string {
    const s = String(segment || '').trim();
    if (!s) return 'unknown-booking';
    return s.replace(/[^a-zA-Z0-9._-]+/g, '_').replace(/_+/g, '_').slice(0, 120);
  }

  /** Same filename pattern as workflow `saveToLocalStorage` / `uploadFile`. */
  private buildCpcCsdFileName(originalName: string, explicit?: string): string {
    if (explicit && !explicit.includes('/') && !explicit.includes('..')) {
      return explicit;
    }
    const timestamp = Date.now();
    const randomHash = Math.random().toString(36).substring(2, 8);
    const safeName = originalName.replace(/[^a-zA-Z0-9._-]/g, '_');
    const extension = path.extname(originalName);
    const nameWithoutExt = safeName.substring(0, Math.max(0, safeName.length - extension.length));
    return `${nameWithoutExt}-${timestamp}-${randomHash}${extension}`;
  }

  /**
   * Relative object path (same string in GCS and under {@link UPLOAD_DIR} for local fallback).
   * Example: `cpc-csd-files/csd/BOOK-1/documents/scan-1713-abc.pdf`
   */
  private cpcCsdRelativeObjectPath(channel: 'csd' | 'cpc', bookingSeg: string, fileName: string): string {
    const ch = channel === 'cpc' ? 'cpc' : 'csd';
    const b = this.cpcCsdSanitizeBookingSegment(bookingSeg);
    return `cpc-csd-files/${ch}/${b}/documents/${fileName}`.replace(/\\/g, '/');
  }

  /**
   * Upload CPC/CSD document to GCS (same bucket lifecycle as workflow requests).
   */
  async uploadCpcCsdFile(options: UploadCpcCsdFileOptions): Promise<UploadResult> {
    if (!this.storage) {
      throw new Error('GCS storage not initialized. Check GCP configuration.');
    }

    const { buffer, originalName, mimeType, channel, bookingSegment } = options;
    if (!buffer?.length) {
      throw new Error('Buffer is required for CPC/CSD upload');
    }

    const fileName = this.buildCpcCsdFileName(originalName, options.fileName);
    const gcsFilePath = this.cpcCsdRelativeObjectPath(channel, bookingSegment, fileName);

    try {
      await this.ensureBucketExists();

      const bucket = this.storage.bucket(this.bucketName);
      const file = bucket.file(gcsFilePath);

      const uploadOptions: any = {
        metadata: {
          contentType: mimeType,
          metadata: {
            originalName,
            uploadedAt: new Date().toISOString(),
            cpcCsdChannel: channel,
          },
        },
      };

      await file.save(buffer, uploadOptions);

      let publicUrl: string;
      try {
        await file.makePublic();
        publicUrl = `https://storage.googleapis.com/${this.bucketName}/${gcsFilePath}`;
      } catch (makePublicError: any) {
        if (makePublicError?.code === 400 || makePublicError?.message?.includes('publicAccessPrevention')) {
          logger.warn('[GCS] CPC/CSD file cannot be public; using signed URL.');
          publicUrl = await this.getSignedUrl(gcsFilePath, 60 * 24 * 365);
        } else {
          throw makePublicError;
        }
      }

      logger.info('[GCS] CPC/CSD file uploaded', { gcsPath: gcsFilePath, storageUrl: publicUrl });

      return {
        storageUrl: publicUrl,
        filePath: gcsFilePath,
        fileName,
      };
    } catch (error) {
      logger.error('[GCS] CPC/CSD upload failed:', error);
      throw new Error(`Failed to upload CPC/CSD file to GCS: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  /**
   * Local fallback for CPC/CSD — mirrors folder layout used in GCS (`cpc-csd-files/...`).
   */
  saveCpcCsdToLocalStorage(options: UploadCpcCsdFileOptions): UploadResult {
    const { buffer, originalName, channel, bookingSegment } = options;
    if (!buffer?.length) {
      throw new Error('Buffer is required for CPC/CSD local storage');
    }

    const fileName = this.buildCpcCsdFileName(originalName, options.fileName);
    const relativePath = this.cpcCsdRelativeObjectPath(channel, bookingSegment, fileName);
    const segments = relativePath.split('/').filter(Boolean);
    const localDir = path.join(UPLOAD_DIR, ...segments.slice(0, -1));
    const localFilePath = path.join(UPLOAD_DIR, ...segments);

    if (!fs.existsSync(localDir)) {
      fs.mkdirSync(localDir, { recursive: true });
    }
    fs.writeFileSync(localFilePath, buffer);
    const storageUrl = `/uploads/${relativePath}`;

    logger.info('[GCS] CPC/CSD file saved to local storage (fallback)', {
      originalName,
      localPath: relativePath,
      storageUrl,
    });

    return {
      storageUrl,
      filePath: relativePath,
      fileName,
    };
  }

  /**
   * CPC/CSD: try GCS first, then local under `uploads/` — same behaviour as {@link uploadFileWithFallback}.
   */
  async uploadCpcCsdFileWithFallback(options: UploadCpcCsdFileOptions): Promise<UploadResult> {
    if (!this.isConfigured()) {
      logger.info('[GCS] GCS not configured, using local storage for CPC/CSD');
      return this.saveCpcCsdToLocalStorage(options);
    }
    try {
      return await this.uploadCpcCsdFile(options);
    } catch (gcsError) {
      logger.warn('[GCS] CPC/CSD GCS upload failed, falling back to local storage', { error: gcsError });
      return this.saveCpcCsdToLocalStorage(options);
    }
  }

  /**
   * Upload file with automatic fallback to local storage
   * If GCS is configured and works, uploads to GCS. Otherwise, saves to local storage.
   * @param options File upload options
   * @returns Upload result with storage URL and file path
   */
  async uploadFileWithFallback(options: UploadFileOptions): Promise<UploadResult> {
    // If GCS is not configured, use local storage directly
    if (!this.isConfigured()) {
      logger.info('[GCS] GCS not configured, using local storage');
      return this.saveToLocalStorage(options);
    }

    // Try GCS upload first
    try {
      return await this.uploadFile(options);
    } catch (gcsError) {
      logger.warn('[GCS] GCS upload failed, falling back to local storage', { error: gcsError });
      // Fallback to local storage
      return this.saveToLocalStorage(options);
    }
  }

  /**
   * Rename a file already stored under uploads/requests/{requestNumber}/... (same shape as upload).
   * GCS: copy to new name in same folder, delete old object. Local: rename on disk.
   */
  async renameRequestDocumentFile(options: {
    oldRelativePath: string;
    newFileName: string;
  }): Promise<{ storageUrl: string; filePath: string; fileName: string }> {
    const { oldRelativePath } = options;
    let newFileName = path.basename(String(options.newFileName || '').trim());
    logger.info('[GCS] renameRequestDocumentFile called', {
      oldRelativePath,
      requestedNewFileName: options.newFileName,
      sanitizedNewFileName: newFileName,
      nodeEnv: process.env.NODE_ENV,
      bucket: this.bucketName || null,
      useGoogleSecretManager: process.env.USE_GOOGLE_SECRET_MANAGER || null,
    });
    if (!newFileName || newFileName === '.' || newFileName === '..') {
      throw new Error('Invalid new file name');
    }
    if (!oldRelativePath || oldRelativePath.includes('..')) {
      throw new Error('Invalid old path');
    }

    const oldNorm = oldRelativePath.replace(/\\/g, '/');
    const dir = path.posix.dirname(oldNorm);
    const newRelativePath = `${dir}/${newFileName}`;
    const localOldExists = fs.existsSync(this.getAbsoluteUploadPath(oldNorm));
    logger.info('[GCS] renameRequestDocumentFile storage presence check', {
      oldNorm,
      newRelativePath,
      localOldExists,
      gcsConfigured: this.isConfigured(),
    });

    if (!this.isConfigured()) {
      logger.info('[GCS] renameRequestDocumentFile using local storage mode');
      return this.renameLocalStoredFile(oldNorm, newRelativePath, newFileName);
    }

    // Important: if upload previously fell back to local storage, rename must also happen locally
    // even when GCS is configured in the current environment.
    if (localOldExists) {
      logger.info('[GCS] renameRequestDocumentFile detected local-stored file; using local rename');
      return this.renameLocalStoredFile(oldNorm, newRelativePath, newFileName);
    }

    if (!this.storage) {
      throw new Error('GCS storage not initialized');
    }

    try {
      const bucket = this.storage.bucket(this.bucketName);
      const oldFile = bucket.file(oldNorm);
      const [exists] = await oldFile.exists();
      logger.info('[GCS] renameRequestDocumentFile GCS existence check', {
        oldNorm,
        exists,
        newRelativePath,
      });
      if (!exists) {
        throw new Error(`GCS file not found: ${oldNorm}`);
      }
      const newFile = bucket.file(newRelativePath);
      await oldFile.copy(newFile);
      logger.info('[GCS] renameRequestDocumentFile copy success', { from: oldNorm, to: newRelativePath });
      await oldFile.delete();
      logger.info('[GCS] renameRequestDocumentFile delete old success', { oldNorm });

      let publicUrl: string;
      try {
        await newFile.makePublic();
        publicUrl = `https://storage.googleapis.com/${this.bucketName}/${newRelativePath}`;
      } catch (makePublicError: any) {
        if (makePublicError?.code === 400 || makePublicError?.message?.includes('publicAccessPrevention')) {
          logger.warn('[GCS] Renamed file cannot be public; using signed URL.');
          publicUrl = await this.getSignedUrl(newRelativePath, 60 * 24 * 365);
        } else {
          throw makePublicError;
        }
      }

      logger.info('[GCS] Renamed document in bucket', { from: oldNorm, to: newRelativePath });
      return {
        storageUrl: publicUrl,
        filePath: newRelativePath,
        fileName: newFileName,
      };
    } catch (error) {
      logger.error('[GCS] renameRequestDocumentFile failed:', error);
      throw new Error(`Failed to rename file: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  /**
   * Check if GCS is properly configured
   */
  isConfigured(): boolean {
    // Check if Google Secret Manager is enabled
    const useGoogleSecretManager = process.env.USE_GOOGLE_SECRET_MANAGER === 'true';
    if (!useGoogleSecretManager) {
      return false;
    }
    
    return this.storage !== null && this.bucketName !== '' && this.projectId !== '';
  }
}

export const gcsStorageService = new GCSStorageService();
