import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { promises as fs } from 'fs';
import * as path from 'path';
import { createReadStream } from 'fs';
import { Readable } from 'stream';

/**
 * ═══════════════════════════════════════════════════════════════════════
 * NC File Storage Service — Phase 1 + Phase 2A + Phase 2B
 * ═══════════════════════════════════════════════════════════════════════
 *
 * STORAGE LAYOUT:
 *   /var/www/scheme_certiifcation/backend/storage/nc-uploads/
 *     {source}/                                          (qrs or tqs)
 *       nc_docs/{nc_id}/{entry_id}/{timestamp}_{filename}         ← Phase 1
 *       final_closure_pdfs/{nc_id}/{timestamp}_{filename}         ← Phase 2A (signed copy)
 *       final_closure_pdfs/{nc_id}/MERGED_{ts}_{ncCode}.pdf       ← Phase 2B (merged)
 *       auditor_signatures/{timestamp}_{filename}                 ← Phase 2A
 * ═══════════════════════════════════════════════════════════════════════
 */

const NEW_STORAGE_ROOT =
  process.env.NC_UPLOAD_ROOT ||
  '/var/www/scheme_certiifcation/backend/storage/nc-uploads';

const LEGACY_PATHS: Record<'QRS' | 'TQS' | 'NEW', string> = {
  QRS: '/var/www/crm_qrs/storage/app/public',
  TQS: '/var/www/crm_tqs/storage/app/public',
  NEW: NEW_STORAGE_ROOT,
};

const NEW_PATH_PREFIX = 'new:';

// ─── Evidence files (Phase 1) ───
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20 MB
const ALLOWED_MIME_TYPES = new Set([
  'application/pdf',
  'image/png',
  'image/jpeg',
  'image/jpg',
  'image/webp',
  'application/msword',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  'application/vnd.ms-excel',
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  'application/zip',
  'application/x-zip-compressed',
  'application/x-zip',
  'application/octet-stream',
]);

// ─── Closure signed copies (Phase 2A) ───
const MAX_SIGNED_COPY_SIZE = 50 * 1024 * 1024;
const SIGNED_COPY_ALLOWED_MIME_TYPES = new Set([
  'application/pdf',
  'application/msword',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
]);

// ─── Auditor signatures (Phase 2A) ───
const MAX_SIGNATURE_SIZE = 5 * 1024 * 1024;
const SIGNATURE_ALLOWED_MIME_TYPES = new Set([
  'image/png',
  'image/jpeg',
  'image/jpg',
  'image/webp',
]);

@Injectable()
export class NcFileStorageService {
  private readonly logger = new Logger(NcFileStorageService.name);

  // ═══════════════════════════════════════════════════════════════════
  // Path resolution
  // ═══════════════════════════════════════════════════════════════════

  isNewPath(dbPath: string | null | undefined): boolean {
    return !!dbPath && dbPath.startsWith(NEW_PATH_PREFIX);
  }

  resolveAbsolutePath(dbPath: string, source: 'QRS' | 'TQS' | 'NEW'): string {
    if (this.isNewPath(dbPath)) {
      const relative = dbPath.slice(NEW_PATH_PREFIX.length);
      return path.join(NEW_STORAGE_ROOT, relative);
    }
    const laravelRoot = LEGACY_PATHS[source];
    if (!laravelRoot) {
      throw new Error(`Unknown source: ${source}`);
    }
    return path.join(laravelRoot, dbPath);
  }

  // ═══════════════════════════════════════════════════════════════════
  // PHASE 1 — Save evidence file (per finding)
  // ═══════════════════════════════════════════════════════════════════

  async saveEntryEvidence(
    source: 'QRS' | 'TQS' | 'NEW',
    ncId: number,
    entryId: number,
    file: {
      originalname: string;
      mimetype: string;
      buffer: Buffer;
      size: number;
    },
  ): Promise<{ dbPath: string; absolutePath: string; size: number }> {
    this.validateEvidenceFile(file);

    const sourceLower = source.toLowerCase();
    const timestamp = Math.floor(Date.now() / 1000);
    const safeName = this.sanitizeFilename(file.originalname);
    const filename = `${timestamp}_${safeName}`;

    const relativeDir = `${sourceLower}/nc_docs/${ncId}/${entryId}`;
    const absoluteDir = path.join(NEW_STORAGE_ROOT, relativeDir);

    await fs.mkdir(absoluteDir, { recursive: true });

    const absolutePath = path.join(absoluteDir, filename);
    await fs.writeFile(absolutePath, file.buffer);
    await fs.chmod(absolutePath, 0o664).catch(() => null);

    const dbPath = `${NEW_PATH_PREFIX}${relativeDir}/${filename}`;

    this.logger.log(
      `Saved evidence: ${absolutePath} (size=${file.size}) → DB path: ${dbPath}`,
    );

    return { dbPath, absolutePath, size: file.size };
  }

  // ═══════════════════════════════════════════════════════════════════
  // PHASE 2A — Save closure signed copy
  // ═══════════════════════════════════════════════════════════════════

  async saveClosureSignedCopy(
    source: 'QRS' | 'TQS' | 'NEW',
    ncId: number,
    file: {
      originalname: string;
      mimetype: string;
      buffer: Buffer;
      size: number;
    },
  ): Promise<{ dbPath: string; absolutePath: string; size: number }> {
    this.validateClosureSignedCopy(file);

    const sourceLower = source.toLowerCase();
    const timestamp = Math.floor(Date.now() / 1000);
    const safeName = this.sanitizeFilename(file.originalname);
    const filename = `${timestamp}_${safeName}`;

    const relativeDir = `${sourceLower}/final_closure_pdfs/${ncId}`;
    const absoluteDir = path.join(NEW_STORAGE_ROOT, relativeDir);

    await fs.mkdir(absoluteDir, { recursive: true });

    const absolutePath = path.join(absoluteDir, filename);
    await fs.writeFile(absolutePath, file.buffer);
    await fs.chmod(absolutePath, 0o664).catch(() => null);

    const dbPath = `${NEW_PATH_PREFIX}${relativeDir}/${filename}`;
    this.logger.log(
      `Saved closure signed copy: ${absolutePath} (size=${file.size}) → ${dbPath}`,
    );

    return { dbPath, absolutePath, size: file.size };
  }

  // ═══════════════════════════════════════════════════════════════════
  // PHASE 2A — Save auditor signature image
  // ═══════════════════════════════════════════════════════════════════

  async saveAuditorSignature(
    source: 'QRS' | 'TQS' | 'NEW',
    file: {
      originalname: string;
      mimetype: string;
      buffer: Buffer;
      size: number;
    },
  ): Promise<{ dbPath: string; absolutePath: string; size: number }> {
    this.validateSignature(file);

    const sourceLower = source.toLowerCase();
    const timestamp = Math.floor(Date.now() / 1000);
    const safeName = this.sanitizeFilename(file.originalname);
    const filename = `${timestamp}_${safeName}`;

    const relativeDir = `${sourceLower}/auditor_signatures`;
    const absoluteDir = path.join(NEW_STORAGE_ROOT, relativeDir);

    await fs.mkdir(absoluteDir, { recursive: true });

    const absolutePath = path.join(absoluteDir, filename);
    await fs.writeFile(absolutePath, file.buffer);
    await fs.chmod(absolutePath, 0o664).catch(() => null);

    const dbPath = `${NEW_PATH_PREFIX}${relativeDir}/${filename}`;
    this.logger.log(
      `Saved auditor signature: ${absolutePath} (size=${file.size}) → ${dbPath}`,
    );

    return { dbPath, absolutePath, size: file.size };
  }

  // ═══════════════════════════════════════════════════════════════════
  // 🆕 PHASE 2B — Save merged closure PDF
  // ═══════════════════════════════════════════════════════════════════
  //
  // Called by ClosurePdfService after merging the verification page
  // with the client-signed copy. Stored in the same folder as the original
  // signed copy, prefixed with MERGED_ for clarity.

  async saveMergedClosurePdf(
    source: 'QRS' | 'TQS' | 'NEW',
    ncId: number,
    buffer: Buffer,
    ncCode: string,
  ): Promise<{ dbPath: string; absolutePath: string; size: number }> {
    if (!buffer || buffer.length === 0) {
      throw new Error('Cannot save empty merged PDF');
    }

    const sourceLower = source.toLowerCase();
    const timestamp = Math.floor(Date.now() / 1000);
    const safeCode = this.sanitizeFilename(ncCode || `NC${ncId}`);
    const filename = `MERGED_${timestamp}_${safeCode}.pdf`;

    const relativeDir = `${sourceLower}/final_closure_pdfs/${ncId}`;
    const absoluteDir = path.join(NEW_STORAGE_ROOT, relativeDir);

    await fs.mkdir(absoluteDir, { recursive: true });

    const absolutePath = path.join(absoluteDir, filename);
    await fs.writeFile(absolutePath, buffer);
    await fs.chmod(absolutePath, 0o664).catch(() => null);

    const dbPath = `${NEW_PATH_PREFIX}${relativeDir}/${filename}`;
    this.logger.log(
      `Saved merged closure PDF: ${absolutePath} (size=${buffer.length}) → ${dbPath}`,
    );

    return { dbPath, absolutePath, size: buffer.length };
  }

  // ═══════════════════════════════════════════════════════════════════
  // Read file (works for both new and legacy)
  // ═══════════════════════════════════════════════════════════════════

  async openFile(
    dbPath: string,
    source: 'QRS' | 'TQS' | 'NEW',
  ): Promise<{
    stream: Readable;
    size: number;
    mimeType: string;
    filename: string;
  }> {
    const absolutePath = this.resolveAbsolutePath(dbPath, source);

    let stat;
    try {
      stat = await fs.stat(absolutePath);
    } catch (err: any) {
      this.logger.warn(`File not found: ${absolutePath} (DB path: ${dbPath})`);
      throw new NotFoundException(`File not found on disk`);
    }

    if (!stat.isFile()) {
      throw new NotFoundException(`Path is not a file`);
    }

    const filename = path.basename(absolutePath);
    const mimeType = this.guessMimeType(filename);
    const stream = createReadStream(absolutePath);

    return { stream, size: stat.size, mimeType, filename };
  }

  // ═══════════════════════════════════════════════════════════════════
  // Delete NEW file (refuses to delete legacy files)
  // ═══════════════════════════════════════════════════════════════════

  async deleteNewFile(
    dbPath: string,
    source: 'QRS' | 'TQS' | 'NEW',
  ): Promise<boolean> {
    if (!this.isNewPath(dbPath)) {
      this.logger.warn(
        `Refused to delete legacy file: ${dbPath} (source=${source}). Legacy files are read-only from NestJS.`,
      );
      return false;
    }

    const absolutePath = this.resolveAbsolutePath(dbPath, source);

    try {
      await fs.unlink(absolutePath);
      this.logger.log(`Deleted new file: ${absolutePath}`);
      return true;
    } catch (err: any) {
      if (err.code === 'ENOENT') return true;
      this.logger.error(
        `Failed to delete ${absolutePath}: ${err.message}`,
        err.stack,
      );
      throw err;
    }
  }

  // ═══════════════════════════════════════════════════════════════════
  // Validators
  // ═══════════════════════════════════════════════════════════════════

  private validateEvidenceFile(file: {
    originalname: string;
    mimetype: string;
    size: number;
  }): void {
    if (!file.originalname || !file.originalname.trim()) {
      throw new Error('Filename is required');
    }
    if (file.size > MAX_FILE_SIZE) {
      throw new Error(
        `File too large (${(file.size / 1024 / 1024).toFixed(1)} MB). Max ${MAX_FILE_SIZE / 1024 / 1024} MB.`,
      );
    }
    if (file.size === 0) throw new Error('Empty file not allowed');

    const ext = file.originalname.toLowerCase().split('.').pop() || '';
    const allowedExts = new Set([
      'pdf', 'png', 'jpg', 'jpeg', 'webp',
      'doc', 'docx', 'xls', 'xlsx', 'zip',
    ]);

    if (!ALLOWED_MIME_TYPES.has(file.mimetype) && !allowedExts.has(ext)) {
      throw new Error(`File type not allowed: ${file.mimetype}`);
    }
  }

  private validateClosureSignedCopy(file: {
    originalname: string;
    mimetype: string;
    size: number;
  }): void {
    if (!file.originalname?.trim()) throw new Error('Filename is required');
    if (file.size === 0) throw new Error('Empty file not allowed');
    if (file.size > MAX_SIGNED_COPY_SIZE) {
      throw new Error(
        `Signed copy too large (${(file.size / 1024 / 1024).toFixed(1)} MB). Max ${MAX_SIGNED_COPY_SIZE / 1024 / 1024} MB.`,
      );
    }
    if (!SIGNED_COPY_ALLOWED_MIME_TYPES.has(file.mimetype)) {
      throw new Error(
        `Signed copy must be PDF, DOC, or DOCX. Got: ${file.mimetype}`,
      );
    }
  }

  private validateSignature(file: {
    originalname: string;
    mimetype: string;
    size: number;
  }): void {
    if (!file.originalname?.trim()) throw new Error('Filename is required');
    if (file.size === 0) throw new Error('Empty file not allowed');
    if (file.size > MAX_SIGNATURE_SIZE) {
      throw new Error(
        `Signature too large (${(file.size / 1024 / 1024).toFixed(1)} MB). Max ${MAX_SIGNATURE_SIZE / 1024 / 1024} MB.`,
      );
    }
    if (!SIGNATURE_ALLOWED_MIME_TYPES.has(file.mimetype)) {
      throw new Error(
        `Signature must be PNG, JPG, or WEBP. Got: ${file.mimetype}`,
      );
    }
  }

  // ═══════════════════════════════════════════════════════════════════
  // Helpers
  // ═══════════════════════════════════════════════════════════════════

  private sanitizeFilename(name: string): string {
    return (
      name
        .replace(/^.*[\\/]/, '')
        .replace(/[\x00-\x1f]/g, '')
        .replace(/[<>:"|?*]/g, '_')
        .trim()
        .slice(0, 200) || 'file'
    );
  }

  private guessMimeType(filename: string): string {
    const ext = filename.toLowerCase().split('.').pop() || '';
    const map: Record<string, string> = {
      pdf: 'application/pdf',
      png: 'image/png',
      jpg: 'image/jpeg',
      jpeg: 'image/jpeg',
      webp: 'image/webp',
      doc: 'application/msword',
      docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      xls: 'application/vnd.ms-excel',
      xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      txt: 'text/plain',
      zip: 'application/zip', 
    };
    return map[ext] || 'application/octet-stream';
  }
}
