import { v4 as uuidv4 } from 'uuid';
import Axios from 'axios';
import { baseURLApi } from '../../config';

function extractExtensionFrom(filename) {
  if (!filename) {
    return null;
  }

  const regex = /(?:\.([^.]+))?$/;
  return regex.exec(filename)[1];
}

/**
 * Valid MIME type prefixes and specific types for each asset format
 */
const VALID_MIME_TYPES = {
  image: {
    prefixes: ['image/'],
    extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'],
  },
  video: {
    prefixes: ['video/'],
    extensions: ['mp4', 'webm', 'mov', 'avi', 'mkv', 'm4v', 'ogv'],
  },
  audio: {
    prefixes: ['audio/'],
    extensions: ['mp3', 'wav', 'ogg', 'aac', 'm4a', 'flac', 'weba'],
  },
};

/**
 * Validate that a file matches the expected asset type
 * @param {File} file - The file to validate
 * @param {string} expectedType - Expected type: 'image', 'video', or 'audio'
 * @returns {{ valid: boolean, error?: string }}
 */
function validateAssetType(file, expectedType) {
  if (!expectedType || !VALID_MIME_TYPES[expectedType]) {
    return { valid: true };
  }

  const { prefixes, extensions } = VALID_MIME_TYPES[expectedType];
  const mimeType = (file.type || '').toLowerCase();
  const extension = extractExtensionFrom(file.name)?.toLowerCase();

  // Check MIME type prefix
  const hasMimeMatch = prefixes.some((prefix) => mimeType.startsWith(prefix));

  // Check file extension as fallback (some browsers don't report MIME correctly)
  const hasExtensionMatch = extension && extensions.includes(extension);

  if (!hasMimeMatch && !hasExtensionMatch) {
    const typeLabel = expectedType.charAt(0).toUpperCase() + expectedType.slice(1);
    return {
      valid: false,
      error: `Invalid file type. Expected ${typeLabel} file but got "${mimeType || 'unknown'}" (${file.name})`,
    };
  }

  return { valid: true };
}

export default class FileUploader {
  /**
   * Validate file against schema
   * @param {File} file - File to validate
   * @param {Object} schema - Validation schema
   * @param {boolean} [schema.image] - Must be an image
   * @param {boolean} [schema.video] - Must be a video
   * @param {boolean} [schema.audio] - Must be audio
   * @param {string} [schema.assetType] - Asset type: 'image', 'video', 'audio'
   * @param {number} [schema.size] - Max file size in bytes
   * @param {string[]} [schema.formats] - Allowed extensions
   */
  static validate(file, schema) {
    if (!schema) {
      return;
    }

    // Asset type validation (new unified approach)
    if (schema.assetType) {
      const result = validateAssetType(file, schema.assetType);
      if (!result.valid) {
        throw new Error(result.error);
      }
    }

    // Legacy image validation
    if (schema.image) {
      const result = validateAssetType(file, 'image');
      if (!result.valid) {
        throw new Error('You must upload an image');
      }
    }

    // Legacy video validation
    if (schema.video) {
      const result = validateAssetType(file, 'video');
      if (!result.valid) {
        throw new Error('You must upload a video');
      }
    }

    // Legacy audio validation
    if (schema.audio) {
      const result = validateAssetType(file, 'audio');
      if (!result.valid) {
        throw new Error('You must upload an audio file');
      }
    }

    // File size validation
    if (schema.size && file.size > schema.size) {
      const maxSizeMB = (schema.size / (1024 * 1024)).toFixed(1);
      const fileSizeMB = (file.size / (1024 * 1024)).toFixed(1);
      throw new Error(`File is too big. Maximum ${maxSizeMB}MB, got ${fileSizeMB}MB`);
    }

    // Extension validation
    const extension = extractExtensionFrom(file.name)?.toLowerCase();
    if (schema.formats && extension && !schema.formats.includes(extension)) {
      throw new Error(`Invalid format. Allowed: ${schema.formats.join(', ')}`);
    }
  }

  static async upload(path, file, schema) {
    try {
      this.validate(file, schema);
    } catch (error) {
      return Promise.reject(error);
    }

    const extension = extractExtensionFrom(file.name);
    const id = uuidv4();
    const filename = `${id}.${extension}`;
    const privateUrl = `${path}/${filename}`;

    const publicUrl = await this.uploadToServer(file, path, filename);

    return {
      id: id,
      name: file.name,
      sizeInBytes: file.size,
      privateUrl,
      publicUrl,
      new: true,
    };
  }

  static async uploadChunked(path, file, schema, options = {}) {
    try {
      this.validate(file, schema);
    } catch (error) {
      return Promise.reject(error);
    }

    const chunkSize =
      Number(options.chunkSize) > 0
        ? Number(options.chunkSize)
        : 5 * 1024 * 1024;
    const maxRetries =
      Number(options.maxRetries) > 0 ? Number(options.maxRetries) : 3;
    const onProgress =
      typeof options.onProgress === 'function' ? options.onProgress : null;
    const onStatus =
      typeof options.onStatus === 'function' ? options.onStatus : null;
    const signal = options.signal || null;
    const extension = extractExtensionFrom(file.name);
    const id = uuidv4();
    const filename = extension ? `${id}.${extension}` : id;
    const privateUrl = `${path}/${filename}`;
    const totalChunks = Math.max(1, Math.ceil(file.size / chunkSize));

    if (signal?.aborted) {
      throw new Error('Upload aborted');
    }

    const initResponse = await Axios.post(
      '/file/upload-sessions/init',
      {
        folder: path,
        filename,
        size: file.size,
        contentType: file.type || '',
        totalChunks,
      },
      { signal },
    );

    const sessionId = initResponse?.data?.sessionId;

    if (!sessionId) {
      throw new Error('Upload session was not created');
    }

    for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) {
      if (signal?.aborted) {
        throw new Error('Upload aborted');
      }

      const start = chunkIndex * chunkSize;
      const end = Math.min(file.size, start + chunkSize);
      const chunk = file.slice(start, end);

      if (onStatus) {
        onStatus('uploading', {
          chunkIndex,
          totalChunks,
        });
      }

      let retry = 0;

      while (retry <= maxRetries) {
        try {
          await Axios.put(
            `/file/upload-sessions/${sessionId}/chunks/${chunkIndex}`,
            chunk,
            {
              headers: {
                'Content-Type': 'application/octet-stream',
              },
              signal,
            },
          );
          break;
        } catch (error) {
          if (signal?.aborted) {
            throw new Error('Upload aborted');
          }
          retry += 1;
          if (retry > maxRetries) {
            throw error;
          }
          await new Promise((resolve) => setTimeout(resolve, 500 * retry));
        }
      }

      if (onProgress) {
        const percent = Math.round(((chunkIndex + 1) / totalChunks) * 100);
        onProgress(percent, {
          chunkIndex: chunkIndex + 1,
          totalChunks,
        });
      }
    }

    if (signal?.aborted) {
      throw new Error('Upload aborted');
    }

    if (onStatus) {
      onStatus('finalizing', null);
    }

    const finalizeResponse = await Axios.post(
      `/file/upload-sessions/${sessionId}/finalize`,
      null,
      { signal },
    );
    const responsePublicUrl = finalizeResponse?.data?.url;
    const publicUrl = responsePublicUrl
      ? responsePublicUrl.startsWith('http')
        ? responsePublicUrl
        : `${baseURLApi}${responsePublicUrl}`
      : `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(privateUrl)}`;

    return {
      id,
      name: file.name,
      sizeInBytes: file.size,
      privateUrl,
      publicUrl,
      new: true,
    };
  }

  static async uploadToServer(file, path, filename) {
    const formData = new FormData();
    formData.append('file', file);
    formData.append('filename', filename);
    const uri = `/file/upload/${path}`;
    await Axios.post(uri, formData, {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    });

    const privateUrl = `${path}/${filename}`;

    return `${baseURLApi}/file/download?privateUrl=${privateUrl}`;
  }
}
