/**
 * Backup Data Exporter
 *
 * Exports company data from MongoDB to a structured directory that can be
 * zipped into a backup archive. Each category is exported as a data.json
 * file (plus sub-model JSON files), and associated binary files (images,
 * PDFs, videos, etc.) are copied from the uploads/ directory.
 *
 * Supports multi-model categories where a single category contains data from
 * multiple related models (e.g., "books" includes Book, BookChapter,
 * BookSection, BookContentBlock).
 *
 * File extraction is recursive — scans all document fields including nested
 * objects and arrays for paths starting with /uploads/ or stored locally on disk.
 */

import fs from 'fs';
import path from 'path';
import { getModels } from '../../models';
import { BACKUP_CATEGORIES, getCategoryModels, type BackupCategory, type BackupModelEntry } from './categoryMap';

export interface ExportStats {
  files: number;
  images: number;
  videos: number;
  documents: number;
  csvs: number;
  dbRecords: number;
  errors: string[];
}

export interface ExportProgress {
  step: string;
  progress: number;
}

// ─── File-path field detection ──────────────────────────────────────────

/**
 * Known field name suffixes that indicate a file path or URL.
 * Used by the heuristic scanner to detect file references in arbitrary
 * document structures.
 */
const FILE_PATH_SUFFIXES = [
  'Url', 'url', 'Path', 'path', 'Link', 'link',
  'Image', 'image', 'Photo', 'photo', 'Logo', 'logo',
  'Thumbnail', 'thumbnail', 'Banner', 'banner',
  'Video', 'video', 'Audio', 'audio',
  'File', 'file', 'Document', 'document',
  'Media', 'media', 'Source', 'source',
  'Cover', 'cover', 'Hero', 'hero',
  'Screenshot', 'screenshot', 'Attachment', 'attachment',
  'Template', 'template', 'Rendered', 'rendered',
  'Populated', 'populated',
];

/**
 * Known top-level field names that contain file paths.
 * These are checked first before the heuristic scanner.
 */
const KNOWN_FILE_FIELDS = new Set([
  // Direct file path / URL fields
  'filePath', 'imageUrl', 'image', 'file', 'thumbnail', 'logo', 'document',
  'url', 'videoUrl', 'audioUrl', 'thumbnailUrl', 'sourceUrl',
  'coverImageUrl', 'coverThumbnailUrl', 'bannerImageUrl', 'banner',
  'featuredImage', 'heroImage', 'ogImage', 'clientLogo', 'customerPhoto',
  'fileUrl', 'templateUrl', 'previewImageUrl', 'renderedImageUrl',
  'videoFilePath', 'sampleChapterUrl', 'fullDocumentUrl', 'audioFileUrl',
  'catalogPdfUrl', 'designUrl', 'websiteUrl', 'deployUrl',
  'consentDocumentUrl', 'subtitlesUrl', 'screenshotUrl',
  // Arrays of file paths
  'images', 'photos', 'videoUrls', 'documentUrls', 'imageUrls',
  'mediaFilePaths', 'mediaPublicUrls', 'pdfReferences',
  'downloadableResources', 'visualInspirationLinks',
  'externalLinks',
]);

/**
 * Fields that should always be excluded from file path extraction
 * because they are external URLs (not local files) or non-file data.
 */
const EXCLUDED_FILE_FIELDS = new Set([
  'website', 'linkedin', 'twitter', 'instagram', 'facebook', 'tikTok',
  'youTube', 'pinterest', 'threads', 'quora', 'medium', 'reddit',
  'telegram', 'whatsApp', 'googleBusiness', 'meetup', 'spotifyPodcast',
  'applePodcast', 'github', 'googleMaps', 'appleMaps', 'bingMaps',
  'hereMaps', 'openStreetMap', 'what3Words', 'ctaUrl', 'preOrderUrl',
  'launchWebinarUrl', 'driveLink',
]);

/**
 * Check if a value looks like a local file path (not an external URL).
 * Local paths start with /uploads/ or are relative paths without a protocol.
 */
function isLocalFilePath(value: string): boolean {
  if (typeof value !== 'string' || value.length < 2) return false;

  // Explicit local path starting with /uploads/
  if (value.startsWith('/uploads/')) return true;

  // Explicit local path starting with uploads/
  if (value.startsWith('uploads/')) return true;

  // Skip external URLs (http://, https://, ftp://, etc.)
  if (/^[a-zA-Z]+:\/\//.test(value)) return false;

  // Skip data URIs (base64)
  if (value.startsWith('data:')) return false;

  // Skip pure numeric IDs
  if (/^\d+$/.test(value)) return false;

  // Paths starting with / that aren't /uploads/ are unlikely to be files
  if (value.startsWith('/')) return false;

  // Relative paths with common file extensions are likely local files
  if (/\.\w{1,5}$/.test(value) && !value.includes('://')) {
    // Could be a relative file path in uploads directory
    const ext = value.split('.').pop()?.toLowerCase() || '';
    const fileExtensions = [
      'png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'ico', 'bmp', 'tiff',
      'mp4', 'mov', 'avi', 'webm', 'mkv', 'flv',
      'mp3', 'wav', 'm4a', 'ogg', 'aac', 'flac',
      'pdf', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx',
      'csv', 'txt', 'html', 'htm', 'json', 'xml', 'zip',
    ];
    return fileExtensions.includes(ext);
  }

  return false;
}

/**
 * Recursively extract all file paths from a document.
 * Scans all fields including nested objects and arrays.
 * Returns an array of unique file paths relative to process.cwd().
 */
function extractFilePaths(doc: any): string[] {
  const paths = new Set<string>();

  function scan(value: any, depth: number = 0) {
    if (depth > 10) return; // Prevent infinite recursion

    if (value === null || value === undefined) return;

    if (typeof value === 'string') {
      if (isLocalFilePath(value)) {
        paths.add(value);
      }
      return;
    }

    if (Array.isArray(value)) {
      for (const item of value) {
        scan(item, depth + 1);
      }
      return;
    }

    if (typeof value === 'object') {
      for (const [key, val] of Object.entries(value)) {
        // Skip known non-file fields
        if (EXCLUDED_FILE_FIELDS.has(key)) continue;

        // Skip Mongoose internals
        if (key === '_id' || key === '__v' || key === 'createdAt' || key === 'updatedAt') continue;

        // Skip numeric/boolean/null values
        if (typeof val === 'number' || typeof val === 'boolean' || val === null) continue;

        // Check if the field value is a file path
        if (typeof val === 'string') {
          if (KNOWN_FILE_FIELDS.has(key) || FILE_PATH_SUFFIXES.some(suffix => key.endsWith(suffix))) {
            if (isLocalFilePath(val)) {
              paths.add(val);
            }
          } else {
            // Also try the heuristic for unknown field names
            if (isLocalFilePath(val)) {
              paths.add(val);
            }
          }
        } else {
          // Recurse into objects and arrays
          scan(val, depth + 1);
        }
      }
    }
  }

  scan(doc);
  return Array.from(paths);
}

/**
 * Resolve a file path to an absolute disk path.
 * Handles /uploads/... paths, relative paths, and bare filenames.
 */
function resolveFilePath(filePath: string): string {
  const cwd = process.cwd();

  if (filePath.startsWith('/uploads/')) {
    return path.join(cwd, filePath.substring(1)); // Strip leading /
  }

  if (filePath.startsWith('uploads/')) {
    return path.join(cwd, filePath);
  }

  if (path.isAbsolute(filePath)) {
    return filePath;
  }

  // Relative path — resolve from cwd
  return path.join(cwd, filePath.startsWith('/') ? filePath.substring(1) : filePath);
}

/**
 * Copy a file from source to destination, creating directories as needed.
 * Returns true if successful, false if file doesn't exist.
 */
function copyFileToBackup(srcPath: string, destDir: string, preserveSubdirs: boolean): { success: boolean; destPath?: string } {
  const absSrcPath = resolveFilePath(srcPath);

  if (!fs.existsSync(absSrcPath)) {
    return { success: false };
  }

  let destPath: string;

  if (preserveSubdirs) {
    // Preserve the directory structure under uploads/
    // e.g., /uploads/brand-assets/logo.png → assets/brand-assets/logo.png
    const uploadsPrefix = '/uploads/';
    const idx = srcPath.indexOf(uploadsPrefix);
    if (idx !== -1) {
      const subPath = srcPath.substring(idx + uploadsPrefix.length);
      destPath = path.join(destDir, 'assets', subPath);
    } else {
      destPath = path.join(destDir, 'assets', path.basename(srcPath));
    }
  } else {
    destPath = path.join(destDir, 'assets', path.basename(srcPath));
  }

  fs.mkdirSync(path.dirname(destPath), { recursive: true });

  try {
    fs.copyFileSync(absSrcPath, destPath);
    return { success: true, destPath };
  } catch (err) {
    // File might be locked or inaccessible
    return { success: false };
  }
}

/**
 * Classify a file extension into a type category.
 */
function classifyFile(filePath: string): 'image' | 'video' | 'document' | 'csv' | 'other' {
  const ext = (filePath.split('.').pop() || '').toLowerCase();
  if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'ico', 'bmp', 'tiff', 'tif', 'avif'].includes(ext)) return 'image';
  if (['mp4', 'mov', 'avi', 'webm', 'mkv', 'flv', 'wmv', 'm4v', '3gp'].includes(ext)) return 'video';
  if (['pdf', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'txt', 'html', 'htm'].includes(ext)) return 'document';
  if (ext === 'csv') return 'csv';
  return 'other';
}

// ─── Main export ─────────────────────────────────────────────────────────

/**
 * Export all selected categories for a company to the output directory.
 * Returns stats about what was exported.
 */
export async function exportCompanyData(
  companyId: string,
  categories: string[],
  outputDir: string,
  onProgress?: (progress: ExportProgress) => void,
): Promise<ExportStats> {
  const models = getModels();
  const stats: ExportStats = {
    files: 0,
    images: 0,
    videos: 0,
    documents: 0,
    csvs: 0,
    dbRecords: 0,
    errors: [],
  };

  // If "everything" is selected, use all categories
  const selectedCategories = categories.length > 0 ? categories : Object.keys(BACKUP_CATEGORIES);
  const totalSteps = selectedCategories.length;
  let completedSteps = 0;

  for (const categoryId of selectedCategories) {
    const category = BACKUP_CATEGORIES[categoryId];

    if (!category) {
      console.warn(`[BackupExporter] Unknown category: ${categoryId}, skipping`);
      stats.errors.push(`Unknown category: ${categoryId}`);
      continue;
    }

    completedSteps++;
    const progressPercent = Math.round((completedSteps / totalSteps) * 100 * 0.85); // 0–85% for data export
    onProgress?.({
      step: `Exporting ${category.label}...`,
      progress: progressPercent,
    });

    const categoryDir = path.join(outputDir, category.directory);
    fs.mkdirSync(categoryDir, { recursive: true });

    // Get all model entries for this category (primary + sub-models)
    const modelEntries = getCategoryModels(category);

    for (const entry of modelEntries) {
      const model = models[entry.modelName as keyof typeof models] as any;

      if (!model) {
        console.warn(`[BackupExporter] Model not found: ${entry.modelName}, skipping`);
        stats.errors.push(`Model not found: ${entry.modelName}`);
        continue;
      }

      try {
        await exportModel(
          model,
          entry,
          companyId,
          category,
          categoryDir,
          stats,
        );
      } catch (err: any) {
        console.error(`[BackupExporter] Error exporting ${entry.modelName} in ${categoryId}:`, err);
        stats.errors.push(`Error exporting ${entry.modelName}: ${err.message}`);
      }
    }
  }

  // Copy the entire uploads/ directory for the company if it has company-specific subdirs
  await exportUploadsDirectory(companyId, outputDir, stats, onProgress);

  return stats;
}

/**
 * Export a single model's data for a company.
 */
async function exportModel(
  model: any,
  entry: BackupModelEntry,
  companyId: string,
  category: BackupCategory,
  categoryDir: string,
  stats: ExportStats,
): Promise<void> {
  // Determine the subdirectory for this model
  const subDir = entry.subdirectory || 'data';
  const modelDir = path.join(categoryDir, subDir);

  // Query all documents for this company
  let documents: any[];
  try {
    documents = await model.find({ companyId }).lean();
  } catch {
    // Some models might not have companyId — try alternative fields
    try {
      // For models like Company that use _id or other identifiers
      documents = await model.find({ _id: companyId }).lean();
    } catch {
      try {
        // For truly global/shared models (SubscriptionPackage, CurrencyConfig, etc.)
        // export ALL documents since they don't belong to a single company.
        // These are needed for a complete restore — the target system may not have them.
        const globalModels = [
          'SubscriptionPackage', 'CurrencyConfig',
        ];
        if (entry.modelName === 'Company') {
          documents = await model.find({ _id: companyId }).lean();
        } else if (globalModels.includes(entry.modelName)) {
          // Global configs — export all since they're shared across companies
          documents = await model.find({}).lean();
        } else {
          documents = [];
        }
      } catch {
        documents = [];
      }
    }
  }

  if (!documents || documents.length === 0) {
    return;
  }

  // Filter out excluded fields
  const allExcludeFields = new Set([...(category.excludeFields || []), ...(entry.excludeFields || [])]);

  const sanitizedDocs = documents.map((doc: any) => {
    const sanitized = { ...doc };
    for (const field of allExcludeFields) {
      delete sanitized[field];
    }
    // Remove Mongoose internals
    delete sanitized.__v;
    delete sanitized._id;
    return sanitized;
  });

  stats.dbRecords += sanitizedDocs.length;

  // Write data.json for this model
  fs.mkdirSync(modelDir, { recursive: true });
  fs.writeFileSync(
    path.join(modelDir, 'data.json'),
    JSON.stringify(sanitizedDocs, null, 2),
    'utf-8',
  );

  // Extract and copy file references
  if (category.hasFiles) {
    for (const doc of sanitizedDocs) {
      const filePaths = extractFilePaths(doc);

      for (const filePath of filePaths) {
        const result = copyFileToBackup(filePath, categoryDir, true);
        if (result.success) {
          stats.files++;
          const classification = classifyFile(filePath);
          if (classification === 'image') stats.images++;
          else if (classification === 'video') stats.videos++;
          else if (classification === 'document') stats.documents++;
          else if (classification === 'csv') stats.csvs++;
        }
      }
    }
  }
}

/**
 * Copy files from the uploads/ directory that belong to this company.
 * This catches files that might not be referenced in the database (e.g.,
 * generated landing pages, presentations, etc.) as well as files in
 * company-specific subdirectories.
 */
async function exportUploadsDirectory(
  companyId: string,
  outputDir: string,
  stats: ExportStats,
  onProgress?: (progress: ExportProgress) => void,
): Promise<void> {
  onProgress?.({
    step: 'Copying uploaded files...',
    progress: 87, // 87–95% for file copying
  });

  const uploadsBase = path.join(process.cwd(), 'uploads');
  if (!fs.existsSync(uploadsBase)) {
    return;
  }

  const uploadsDestDir = path.join(outputDir, '_uploads');

  // Dynamically discover all subdirectories under uploads/ instead of a hardcoded list.
  // This ensures new upload directories added in the future are automatically included.
  // We skip a few well-known non-data directories (temp, tmp) but include everything else.
  const skipDirs = new Set(['temp', 'tmp', 'tmp-uploads']);

  try {
    const entries = fs.readdirSync(uploadsBase, { withFileTypes: true });

    for (const entry of entries) {
      if (!entry.isDirectory()) continue;
      if (skipDirs.has(entry.name)) continue;

      const srcDir = path.join(uploadsBase, entry.name);
      const destDir = path.join(uploadsDestDir, entry.name);

      try {
        copyDirectoryRecursive(srcDir, destDir, stats);
      } catch (err: any) {
        console.error(`[BackupExporter] Error copying uploads/${entry.name}:`, err);
        stats.errors.push(`Error copying uploads/${entry.name}: ${err.message}`);
      }
    }
  } catch (err: any) {
    console.error('[BackupExporter] Error reading uploads directory:', err);
    stats.errors.push(`Error reading uploads directory: ${err.message}`);
  }

  // Also copy any standalone files in the uploads/ root that might belong to this company
  // (e.g., general file uploads stored directly in uploads/)
  try {
    const rootFiles = fs.readdirSync(uploadsBase);
    for (const file of rootFiles) {
      const fullPath = path.join(uploadsBase, file);
      if (fs.statSync(fullPath).isFile()) {
        const destPath = path.join(uploadsDestDir, file);
        if (!fs.existsSync(destPath)) {
          fs.mkdirSync(uploadsDestDir, { recursive: true });
          fs.copyFileSync(fullPath, destPath);
          stats.files++;
        }
      }
    }
  } catch {
    // Ignore errors reading root uploads dir
  }
}

/**
 * Recursively copy a directory, counting files as we go.
 */
function copyDirectoryRecursive(src: string, dest: string, stats: ExportStats): void {
  fs.mkdirSync(dest, { recursive: true });

  const entries = fs.readdirSync(src, { withFileTypes: true });

  for (const entry of entries) {
    const srcPath = path.join(src, entry.name);
    const destPath = path.join(dest, entry.name);

    if (entry.isDirectory()) {
      copyDirectoryRecursive(srcPath, destPath, stats);
    } else if (entry.isFile()) {
      try {
        fs.copyFileSync(srcPath, destPath);
        stats.files++;
        const classification = classifyFile(entry.name);
        if (classification === 'image') stats.images++;
        else if (classification === 'video') stats.videos++;
        else if (classification === 'document') stats.documents++;
        else if (classification === 'csv') stats.csvs++;
      } catch {
        // Skip files that can't be copied (locked, permissions, etc.)
      }
    }
  }
}