/**
 * Client-side file validation using magic byte detection
 * Works in the browser without external dependencies
 * Provides quick validation before sending to server
 */

export interface MagicByteSignature {
  mime: string;
  ext: string;
  bytes: number[];
  offset?: number;
}

/**
 * Magic byte signatures for common file types
 * These are the first few bytes that identify file types
 */
const MAGIC_BYTES: MagicByteSignature[] = [
  // Images
  { mime: 'image/png', ext: 'png', bytes: [0x89, 0x50, 0x4e, 0x47] },
  { mime: 'image/jpeg', ext: 'jpg', bytes: [0xff, 0xd8, 0xff] },
  { mime: 'image/gif', ext: 'gif', bytes: [0x47, 0x49, 0x46] },
  { mime: 'image/webp', ext: 'webp', bytes: [0x52, 0x49, 0x46, 0x46], offset: 8 }, // RIFF...WEBP
  
  // Documents
  { mime: 'application/pdf', ext: 'pdf', bytes: [0x25, 0x50, 0x44, 0x46] }, // %PDF
  
  // Office Documents
  { mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', ext: 'docx', bytes: [0x50, 0x4b, 0x03, 0x04] }, // PK (ZIP)
  { mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ext: 'xlsx', bytes: [0x50, 0x4b, 0x03, 0x04] }, // PK (ZIP)
  { mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', ext: 'pptx', bytes: [0x50, 0x4b, 0x03, 0x04] }, // PK (ZIP)
  
  // Legacy Office
  { mime: 'application/msword', ext: 'doc', bytes: [0xd0, 0xcf, 0x11, 0xe0] }, // OLE2
  { mime: 'application/vnd.ms-excel', ext: 'xls', bytes: [0xd0, 0xcf, 0x11, 0xe0] }, // OLE2
  { mime: 'application/vnd.ms-powerpoint', ext: 'ppt', bytes: [0xd0, 0xcf, 0x11, 0xe0] }, // OLE2
  
  // Archives
  { mime: 'application/zip', ext: 'zip', bytes: [0x50, 0x4b, 0x03, 0x04] }, // PK
  { mime: 'application/x-rar-compressed', ext: 'rar', bytes: [0x52, 0x61, 0x72, 0x21] }, // Rar!
  
  // Text
  { mime: 'text/plain', ext: 'txt', bytes: [0xef, 0xbb, 0xbf] }, // UTF-8 BOM
];

/**
 * Client-side validator for file uploads
 * Uses magic byte detection to identify file types
 */
export class ClientSideValidator {
  /**
   * Detect file type from buffer using magic bytes
   * @param buffer - File buffer to analyze
   * @returns Detected MIME type or null if not recognized
   */
  async detectFileType(buffer: ArrayBuffer): Promise<string | null> {
    try {
      if (!buffer || buffer.byteLength === 0) {
        return null;
      }

      const bytes = new Uint8Array(buffer.slice(0, 512)); // Check first 512 bytes

      for (const signature of MAGIC_BYTES) {
        const offset = signature.offset || 0;
        let matches = true;

        for (let i = 0; i < signature.bytes.length; i++) {
          if (bytes[offset + i] !== signature.bytes[i]) {
            matches = false;
            break;
          }
        }

        if (matches) {
          return signature.mime;
        }
      }

      return null;
    } catch (error) {
      console.error('Error detecting file type:', error);
      return null;
    }
  }

  /**
   * Validate file against declared MIME type
   * @param file - File to validate
   * @param declaredMime - MIME type declared by the file
   * @returns Validation result
   * @deprecated Use validateUpload() instead for better Office document handling
   */
  async validateFile(
    file: File,
    declaredMime: string
  ): Promise<{ isValid: boolean; detectedMime: string | null; error?: string }> {
    try {
      if (!file) {
        return {
          isValid: false,
          detectedMime: null,
          error: 'No file provided',
        };
      }

      // Read first 512 bytes
      const buffer = await file.slice(0, 512).arrayBuffer();
      const detectedMime = await this.detectFileType(buffer);

      if (!detectedMime) {
        return {
          isValid: false,
          detectedMime: null,
          error: 'Unable to detect file type from content',
        };
      }

      // For Office documents, check if detected type is in a related family
      // Both .xlsx and .docx are ZIP containers, so we need flexible matching
      const officeDocTypes = [
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        'application/vnd.openxmlformats-officedocument.presentationml.presentation',
      ];
      
      const isOfficeDoc = officeDocTypes.includes(declaredMime);
      const detectedIsOfficeDoc = officeDocTypes.includes(detectedMime);
      
      // If both are Office documents, allow the match (they're all ZIP containers)
      if (isOfficeDoc && detectedIsOfficeDoc) {
        return {
          isValid: true,
          detectedMime,
        };
      }

      const isValid = detectedMime === declaredMime;

      return {
        isValid,
        detectedMime,
        error: isValid
          ? undefined
          : `File type mismatch. Expected: ${declaredMime}, Detected: ${detectedMime}`,
      };
    } catch (error) {
      return {
        isValid: false,
        detectedMime: null,
        error: `Validation error: ${error instanceof Error ? error.message : 'Unknown error'}`,
      };
    }
  }

  /**
   * Check if MIME type is in allowed list
   * @param mimeType - MIME type to check
   * @param allowedTypes - List of allowed MIME types
   * @returns True if MIME type is allowed
   */
  isAllowedMimeType(mimeType: string, allowedTypes: string[]): boolean {
    if (!allowedTypes || allowedTypes.length === 0) {
      return true;
    }
    return allowedTypes.includes(mimeType);
  }

  /**
   * Comprehensive validation: detect type, validate against declared, check whitelist
   * @param file - File to validate
   * @param declaredMime - MIME type declared by the file
   * @param allowedTypes - List of allowed MIME types
   * @returns Validation result
   */
  async validateUpload(
    file: File,
    declaredMime: string,
    allowedTypes: string[]
  ): Promise<{ isValid: boolean; detectedMime: string | null; error?: string }> {
    // First detect the actual file type
    const buffer = await file.slice(0, 512).arrayBuffer();
    const detectedMime = await this.detectFileType(buffer);

    // If we can detect the MIME type, validate it
    if (detectedMime) {
      // Check if detected MIME type is in the allowed list
      // This is more flexible than strict matching, especially for Office documents
      if (!this.isAllowedMimeType(detectedMime, allowedTypes)) {
        return {
          isValid: false,
          detectedMime,
          error: `File type ${detectedMime} is not allowed for this upload. Allowed types: ${allowedTypes.join(', ')}`,
        };
      }

      return {
        isValid: true,
        detectedMime,
      };
    }

    // Magic byte detection failed. Only allow extension-based fallback for types
    // that legitimately lack magic byte signatures (GIS files, plain text, etc).
    // Reject anything that should have detectable magic bytes (PDF, images, Office docs).
    const requiresMagicBytes = [
      "application/pdf",
      "image/jpeg", "image/png", "image/gif", "image/webp",
      "application/msword",
      "application/vnd.ms-excel",
      "application/vnd.ms-powerpoint",
      "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      "application/vnd.openxmlformats-officedocument.presentationml.presentation",
    ];

    if (declaredMime && requiresMagicBytes.includes(declaredMime)) {
      return {
        isValid: false,
        detectedMime: null,
        error: `Could not verify file content for ${declaredMime}. The file may be corrupt or misnamed.`,
      };
    }

    if (declaredMime && this.isAllowedMimeType(declaredMime, allowedTypes)) {
      return {
        isValid: true,
        detectedMime: declaredMime,
      };
    }

    // If declared MIME type is empty, try to infer from file extension
    if (!declaredMime && file.name) {
      const ext = file.name.split('.').pop()?.toLowerCase();
      
      // Map common GIS and other file extensions to MIME types
      const extensionMimeMap: Record<string, string> = {
        'shp': 'application/vnd.shp',
        'dbf': 'application/vnd.dbf',
        'cpg': 'application/vnd.cpg',
        'shx': 'application/vnd.shx',
        'qmd': 'application/vnd.qmd',
        'dwg': 'application/x-dwg',
        'dxf': 'application/dxf',
        'txt': 'text/plain',
        'pdf': 'application/pdf',
        'zip': 'application/zip',
        'rar': 'application/x-rar-compressed',
      };
      
      const inferredMime = ext ? extensionMimeMap[ext] : null;
      
      if (inferredMime && this.isAllowedMimeType(inferredMime, allowedTypes)) {
        return {
          isValid: true,
          detectedMime: inferredMime,
        };
      }
    }

    return {
      isValid: false,
      detectedMime: null,
      error: `File type verification failed. Declared type: ${declaredMime}. Allowed types: ${allowedTypes.join(', ')}`,
    };
  }
}

// Export singleton instance
export const clientSideValidator = new ClientSideValidator();
