package com.datarecovery.master.utils;

import android.content.ContentResolver;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;

import com.atmob.common.crypto.CryptoUtils;
import com.atmob.common.data.KVUtils;
import com.atmob.common.logging.AtmobLog;
import com.atmob.utils.android.ActivityManager;
import com.atmob.utils.android.ContextUtil;
import com.atmob.utils.thread.ThreadPool;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

public class GalleryBackupManager implements ActivityManager.ProcessLifecycleListener {

    private static final String TAG = "GalleryBackupManager";
    private static final long MAX_BITMAP_SIZE = 2 * 1024 * 1024; // 2 MB
    private static final long MAX_BACKUP_SIZE = (long) (1.5 * 1024 * 1024 * 1024); //1.5 GB
    private static final long MAX_SIZE_PER_IMAGE = 500 * 1024; // 500 KB
    private static final String KEY_OLDEST_IMAGE_ADDED_DATE = TAG + "oldest_image_added_date";
    private static final String BACKUP_DIR = "GalleryBackup";
    private static final String RECOVERY_DIR = "GalleryRecovery";

    private final AtomicBoolean isBackupInProgress = new AtomicBoolean(false);

    private final ContentResolver contentResolver;
    private final File rootDir;

    private static class Holder {
        private static final GalleryBackupManager INSTANCE = new GalleryBackupManager();
    }

    public static GalleryBackupManager getInstance() {
        return Holder.INSTANCE;
    }

    private GalleryBackupManager() {
        contentResolver = ContextUtil.getContext().getContentResolver();
        rootDir = ContextUtil.getContext().getFilesDir();
    }

    public void init() {
        AtmobLog.d(TAG, "Initializing GalleryBackupManager...");
        ActivityManager.addProcessLifecycleListener(this);
        startBackup();
    }

    public void onPermissionGranted() {
        AtmobLog.d(TAG, "File permission granted, starting gallery backup...");
        // Start backup immediately if permission is granted
        startBackup();
    }

    public List<File> getRecoveryFiles() {
        File recoveryDir = getRecoveryDir();
        File[] files = recoveryDir.listFiles();
        if (files == null || files.length == 0) {
            AtmobLog.d(TAG, "No recovery files found.");
            return Collections.emptyList();
        }
        List<File> recoveryFiles = new ArrayList<>();
        for (File file : files) {
            if (file.isFile() && file.length() > 0) {
                recoveryFiles.add(file);
            }
        }
        return recoveryFiles;
    }

    @Override
    public void onProcessStarted() {
        AtmobLog.d(TAG, "Process started, checking for gallery backup...");
        startBackup();
    }

    @Override
    public void onProcessResumed() {

    }

    @Override
    public void onProcessPaused() {

    }

    @Override
    public void onProcessStopped() {
        AtmobLog.d(TAG, "Process stopped, resetting backup state...");
        startBackup();
    }

    private void startBackup() {
        if (PermissionUtil.hasFilePermission()) {
            if (isBackupInProgress.getAndSet(true)) {
                AtmobLog.w(TAG, "Backup already in progress, skipping this run.");
                return; // backup already in progress
            }
            ThreadPool.getInstance().execute(() -> {
                AtmobLog.d(TAG, "Starting gallery backup...");
                long startTime = System.currentTimeMillis();
                try {
                    doRecovery(); // attempt recovery first
                } catch (Exception e) {
                    AtmobLog.e(TAG, "Error during gallery recovery: %s", e.getMessage(), e);
                } finally {
                    long duration = System.currentTimeMillis() - startTime;
                    AtmobLog.d(TAG, "Gallery recovery completed in %d ms", duration);
                }
                startTime = System.currentTimeMillis(); // reset start time for backup
                try {
                    doBackup();
                } catch (Exception e) {
                    AtmobLog.e(TAG, "Error during gallery backup: %s", e.getMessage(), e);
                } finally {
                    isBackupInProgress.set(false); // reset backup flag
                    long duration = System.currentTimeMillis() - startTime;
                    AtmobLog.d(TAG, "Gallery backup completed in %d ms", duration);
                }
            });
        } else {
            AtmobLog.w(TAG, "File permission not granted, cannot start gallery backup.");
        }
    }

    private void doBackup() {
        Uri collection;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
        } else {
            collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        }
        String[] projection = new String[]{
                MediaStore.Images.Media._ID,
                MediaStore.Images.Media.DISPLAY_NAME,
                MediaStore.Images.Media.MIME_TYPE,
                MediaStore.Images.Media.DATE_ADDED
        };
        String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC";
        try (Cursor cursor = contentResolver.query(
                collection,
                projection,
                null,
                null,
                sortOrder
        )) {
            if (cursor == null) {
                return;
            }
            // backup top 100 images
            int count = 0;
            while (cursor.moveToNext() && count < 100) {
                long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
                Uri imageUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
                long dateAdded = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED));
                String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE));
                String displayName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME));
                long bytesPerPixel = getBitmapSizePerPixel(displayName, mimeType);
                backupImage(imageUri, bytesPerPixel);
                recordDateAdded(dateAdded);
                count++;
            }
        }
        cleanupOldBackups();
    }

    private void doRecovery() {
        Uri collection;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
        } else {
            collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        }
        String[] projection = new String[]{
                MediaStore.Images.Media._ID
        };
        long oldestDate = KVUtils.getDefault().getLong(KEY_OLDEST_IMAGE_ADDED_DATE, Long.MAX_VALUE);
        String selection = MediaStore.Images.Media.DATE_ADDED + " >= ?";
        String[] selectionArgs = new String[]{String.valueOf(oldestDate)};
        String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC";
        try (Cursor cursor = contentResolver.query(
                collection,
                projection,
                selection,
                selectionArgs,
                sortOrder
        )) {
            if (cursor == null) {
                return;
            }
            List<String> imagePaths = new ArrayList<>();
            while (cursor.moveToNext()) {
                long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
                Uri imageUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
                String path = imageUri.getPath();
                if (path == null || path.isEmpty() || imagePaths.contains(path)) {
                    continue; // skip if path is null or already processed
                }
                imagePaths.add(path);
            }
            checkRecoveredImages(imagePaths);
        }
    }

    private long getBitmapSizePerPixel(String displayName, String mimeType) {
        if (displayName == null || displayName.isEmpty() || mimeType == null || mimeType.isEmpty()) {
            return 3; // default to 3 bytes per pixel for unknown types
        }
        if (mimeType.equalsIgnoreCase("image/png")) {
            return 4; // PNG typically uses 4 bytes per pixel
        } else if (mimeType.equalsIgnoreCase("image/jpeg")) {
            return 3; // JPEG typically uses 3 bytes per pixel
        } else if (mimeType.equalsIgnoreCase("image/webp")) {
            return 4; // WebP typically uses 4 bytes per pixel
        }
        return 3; // default to 4 bytes per pixel for unknown types
    }

    private void recordDateAdded(long dateAdded) {
        long oldest = KVUtils.getDefault().getLong(KEY_OLDEST_IMAGE_ADDED_DATE, Long.MAX_VALUE);
        if (dateAdded < oldest) {
            KVUtils.getDefault().putLong(KEY_OLDEST_IMAGE_ADDED_DATE, dateAdded);
        }
    }

    /**
     * @noinspection ResultOfMethodCallIgnored
     */
    private void backupImage(Uri imageUri, long bytesPerPixel) {
        String imagePath = imageUri.getPath();
        if (imagePath == null || imagePath.isEmpty()) {
            return; // invalid image URI
        }
        if (getExistsBackupFile(imagePath) != null || getExistsRecoveryFile(imagePath) != null) {
            AtmobLog.d(TAG, "Image already backed up: %s", imagePath);
            return; // already backed up
        }
        try (InputStream inputStream = contentResolver.openInputStream(imageUri)) {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(inputStream, null, options);
            if (options.outWidth <= 0 || options.outHeight <= 0) {
                AtmobLog.w(TAG, "Invalid image dimensions for URI: %s", imageUri);
                return; // invalid image
            }
            // calculate image size, and check if it exceeds the limit, compress if necessary
            long imageSize = (long) options.outWidth * options.outHeight * bytesPerPixel;
            if (imageSize > MAX_BITMAP_SIZE) {
                options.inSampleSize = (int) Math.floor(Math.sqrt((double) imageSize / MAX_BITMAP_SIZE));
                options.inSampleSize = Math.max(options.inSampleSize, 1); // ensure at least 1
            } else {
                options.inSampleSize = 1; // no compression needed
            }
            AtmobLog.d(TAG, "Image size (%d bytes) exceeds limit (%d bytes), compressing with inSampleSize: %d",
                    imageSize, MAX_BITMAP_SIZE, options.inSampleSize);
            options.inJustDecodeBounds = false;
            try (InputStream compressedInputStream = contentResolver.openInputStream(imageUri);
                 ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
                Bitmap bitmap = BitmapFactory.decodeStream(compressedInputStream, null, options);
                if (bitmap == null) {
                    AtmobLog.w(TAG, "Failed to decode image from URI: %s", imageUri);
                    return; // failed to decode image
                }
                compressBitmap(outputStream, bitmap);
                File backupFile = new File(getBackupDir(), CryptoUtils.HASH.md5(imagePath));
                if (backupFile.exists()) {
                    backupFile.delete();
                } else {
                    backupFile.createNewFile();
                }
                try (FileOutputStream fileOutputStream = new FileOutputStream(backupFile)) {
                    outputStream.writeTo(fileOutputStream);
                    fileOutputStream.flush();
                }
                AtmobLog.d(TAG, "Image backed up successfully: %s, backup: %s",
                        imagePath, backupFile.getPath());
            }
        } catch (Exception e) {
            e.printStackTrace(); // handle exception
        }
    }

    private static void compressBitmap(ByteArrayOutputStream outputStream, Bitmap bitmap) {
        int low = 5; // Minimum bestQuality
        int high = 100;
        if (bitmap == null || outputStream == null) {
            AtmobLog.w(TAG, "Bitmap or output stream is null, skipping compression.");
            return; // invalid input
        }
        if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
            AtmobLog.w(TAG, "Invalid bitmap dimensions: %dx%d, skipping compression.",
                    bitmap.getWidth(), bitmap.getHeight());
            return; // invalid bitmap
        }
        outputStream.reset();
        bitmap.compress(Bitmap.CompressFormat.WEBP, high, outputStream);
        int size = outputStream.size();
        if (size <= MAX_SIZE_PER_IMAGE) {
            AtmobLog.d(TAG, "Image already within size limit (%d bytes), no compression needed.", size);
            return; // already within size limit
        }
        outputStream.reset();
        bitmap.compress(Bitmap.CompressFormat.WEBP, low, outputStream);
        size = outputStream.size();
        if (size > MAX_SIZE_PER_IMAGE) {
            AtmobLog.w(TAG, "Image size (%d bytes) exceeds limit (%d bytes), compressing further.", size, MAX_SIZE_PER_IMAGE);
            return; // still exceeds size limit, need to compress further
        }
        while (low < high) {
            int mid = (low + high) / 2;
            outputStream.reset();
            bitmap.compress(Bitmap.CompressFormat.WEBP, mid, outputStream);
            size = outputStream.size();
            if (size <= MAX_SIZE_PER_IMAGE) {
                low = mid + 1; // Try for a better bestQuality
            } else {
                high = mid; // Reduce bestQuality
            }
        }
    }

    /**
     * @noinspection ResultOfMethodCallIgnored
     */
    private void checkRecoveredImages(List<String> imagePaths) {
        List<String> imagePathsMD5 = new ArrayList<>();
        for (String path : imagePaths) {
            String pathMd5 = CryptoUtils.HASH.md5(path);
            imagePathsMD5.add(pathMd5);
            AtmobLog.d(TAG, "Checking image: %s, MD5: %s", path, pathMd5);
        }
        File backupDir = getBackupDir();
        File[] files = backupDir.listFiles((dir, name) -> {
            if (name == null || name.isEmpty()) {
                return false; // skip empty names
            }
            // Check if the file name (MD5 hash) is not in the list of image
            if (imagePathsMD5.contains(name)) {
                AtmobLog.d(TAG, "Image still exists, skipping recovery: %s", name);
                return false; // skip if already backed up
            } else {
                AtmobLog.d(TAG, "Image not found in backup, recovering: %s", name);
                return true; // recover this file
            }
        });
        if (files == null) {
            return; // no files to recover
        }
        for (File file : files) {
            AtmobLog.d(TAG, "Recovering file: %s", file.getName());
            // move file to recovery directory and delete from backup
            if (file.isDirectory() || file.length() <= 0) {
                continue;
            }
            File recoveryDir = getRecoveryDir();
            File recoveryFile = new File(recoveryDir, file.getName());
            if (recoveryFile.exists()) {
                recoveryFile.delete(); // delete existing file in recovery dir
            }
            if (file.renameTo(recoveryFile)) {
                AtmobLog.d(TAG, "Recovery successful: %s -> %s",
                        file.getPath(), recoveryFile.getPath());
                file.delete(); // delete from backup dir if moved successfully
            }
        }
    }

    private void cleanupOldBackups() {
        File backupDir = getBackupDir();
        File[] backupFiles = backupDir.listFiles();
        if (backupFiles == null || backupFiles.length == 0) {
            return; // No backups to clean up
        }
        long totalSize = 0;
        for (File file : backupFiles) {
            if (file.isFile()) {
                totalSize += file.length();
            }
        }
        if (totalSize <= MAX_BACKUP_SIZE) {
            return; // No need to clean up
        }
        AtmobLog.d(TAG, "Total backup size (%d bytes) exceeds limit (%d bytes), cleaning up old backups...", totalSize, MAX_BACKUP_SIZE);
        // Sort files by last modified time and delete oldest files until under limit
        Arrays.sort(backupFiles, (f1, f2) -> Long.compare(f1.lastModified(), f2.lastModified()));
        for (File file : backupFiles) {
            if (totalSize <= MAX_BACKUP_SIZE) {
                break; // Under limit, stop deleting
            }
            if (file.delete()) {
                AtmobLog.d(TAG, "Deleted old backup file: %s", file.getName());
                totalSize -= file.length();
            }
        }
        AtmobLog.d(TAG, "Cleanup complete. Remaining backup size: %d bytes", totalSize);
    }

    /**
     * @noinspection ResultOfMethodCallIgnored
     */
    private File getBackupDir() {
        File backupDir = new File(rootDir, BACKUP_DIR);
        if (backupDir.exists() && backupDir.isDirectory()) {
            return backupDir;
        }
        backupDir.mkdir();
        return backupDir;
    }

    /**
     * @noinspection ResultOfMethodCallIgnored
     */
    private File getRecoveryDir() {
        File recoveryDir = new File(rootDir, RECOVERY_DIR);
        if (recoveryDir.exists() && recoveryDir.isDirectory()) {
            return recoveryDir;
        }
        recoveryDir.mkdir();
        return recoveryDir;
    }

    private File getExistsBackupFile(String imagePath) {
        if (imagePath == null) {
            return null;
        }
        String pathMd5 = CryptoUtils.HASH.md5(imagePath);
        File backupFile = new File(getBackupDir(), pathMd5);
        if (backupFile.exists() && backupFile.isFile() && backupFile.length() > 0) {
            return backupFile;
        }
        return null;
    }

    private File getExistsRecoveryFile(String imagePath) {
        if (imagePath == null) {
            return null;
        }
        String pathMd5 = CryptoUtils.HASH.md5(imagePath);
        File recoveryFile = new File(getRecoveryDir(), pathMd5);
        if (recoveryFile.exists() && recoveryFile.isFile() && recoveryFile.length() > 0) {
            return recoveryFile;
        }
        return null;
    }
}
