package com.ffii.lioner.modules.lioner.service;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Blob;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import javax.imageio.ImageIO;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.xmlbeans.impl.common.IOUtil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import com.ffii.lioner.modules.lioner.entity.File;
import com.ffii.lioner.modules.lioner.entity.FileBlob;
import com.ffii.lioner.modules.lioner.entity.FileRef;
import com.ffii.lioner.modules.lioner.entity.FileRepository;
import com.ffii.core.support.AbstractBaseEntityService;
import com.ffii.core.support.JdbcDao;
import com.ffii.core.utils.FileUtils;
import com.ffii.core.utils.StringUtils;

@Service
public class FileService extends AbstractBaseEntityService<File, Long, FileRepository> {

	private FileBlobService fileBlobService;
	private FileRefService fileRefService;

	private static final int NUM_THREADS = 4; // Number of threads to use for parallel processing
	private ExecutorService executor;

	public FileService(JdbcDao jdbcDao, FileRepository repository, FileBlobService fileBlobService,
			FileRefService fileRefService) {
		super(jdbcDao, repository);
		this.fileBlobService = fileBlobService;
		this.fileRefService = fileRefService;
		executor = Executors.newFixedThreadPool(NUM_THREADS);
	}

	@Transactional
	public Map<String, Object> deleteFile(Long id) {
		File file = find(id).get();
		FileRef fileRef = fileRefService.findByFileId(id).get();
		Long fileBlobId = fileBlobService.findIdByFileId(id);
		if (fileRef.getThumbnailFileId() != null) {
			Long thumbnailId = fileRef.getThumbnailFileId();
			Long fileThumbnailBlobId = fileBlobService.find(thumbnailId).get().getId();
			fileBlobService.delete(fileThumbnailBlobId);
			fileRefService.delete(thumbnailId);
		}
		delete(file.getId());
		fileRefService.delete(fileRef.getId());
		fileBlobService.delete(fileBlobId);
		return Map.of(
				"fileId", file.getId(),
				"fileRefId", fileRef.getId(),
				"fileBlobId", fileBlobId);
	}

	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	public Optional<File> findFileByIdAndKey(Long fileId, String skey) {

		Map<String, Object> args = Map.of("fileId", fileId, "skey", skey);
		return jdbcDao.queryForEntity("SELECT * " +
				"FROM file f where f.id = :fileId " +
				"AND f.skey = :skey", args, File.class);
	}

	public Pair<File, byte[]> loadFile(Long id, String skey, String filename) {
		// String data = id + skey + filename + FilenameUtils.getExtension(filename);
		File file = findFileByIdAndKey(id, skey).get();
		byte[] fileByte = getfileBlob(id);

		return ImmutablePair.of(file, fileByte);
	}

	public File loadFileOnly(Long id, String skey, String filename) {
		// String data = id + skey + filename + FilenameUtils.getExtension(filename);
		File file = findFileByIdAndKey(id, skey).get();
		return file;
	}

	@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class, readOnly = false)
	public Long updateFileDisplay(Long fileId, MultipartFile file, String remarks) throws IOException {
		// String data = id + skey + filename + FilenameUtils.getExtension(filename);
		FileRef fileRef = fileRefService.findByFileId(fileId).get();
		fileRef.setRemarks(remarks);

		if (file != null) {
			FileBlob thumbnail = fileBlobService.find(fileRef.getThumbnailFileId()).get();
			thumbnail.setBytes(file.getBytes());
			fileBlobService.save(thumbnail);
		}
		fileRefService.saveOrUpdate(fileRef);

		return fileRef.getId();
	}

	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	public Optional<FileBlob> findFileBlobByFileId(Long fileId) {
		Map<String, Object> args = Map.of("fileId", fileId);

		return jdbcDao.queryForEntity("SELECT * " +
				"FROM file_blob fb where fb.fileId = :fileId", args, FileBlob.class);
	}

	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	public byte[] getfileBlob(Long fileId) {
		Map<String, Object> args = Map.of("fileId", fileId);

		try {
			Blob blob = jdbcDao
					.queryForBlob("SELECT bytes FROM file_blob fb where fb.fileId = :fileId LIMIT 1", args);
			int blobLength = (int) blob.length();
			byte[] bytes = blob.getBytes(1, blobLength);
			blob.free();
			return bytes;

		} catch (Exception e) {
			logger.info(ExceptionUtils.getStackTrace(e));
		}

		return null;
	}

	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	public byte[] getfileBlobById(Long id) {
		Map<String, Object> args = Map.of("id", id);

		try {
			Blob blob = jdbcDao
					.queryForBlob("SELECT bytes FROM file_blob fb where fb.id = :id LIMIT 1", args);
			int blobLength = (int) blob.length();
			byte[] bytes = blob.getBytes(1, blobLength);
			blob.free();
			return bytes;

		} catch (Exception e) {
			logger.info(ExceptionUtils.getStackTrace(e));
		}

		return null;
	}

	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = false)
	public Map<String, Object> saveOrUpdate(MultipartFile[] files, Long refId, String refType) throws Exception {
		List<Long> fileIdList = new ArrayList<Long>();
		List<Long> blobIdList = new ArrayList<Long>();
		List<Long> refIdList = new ArrayList<Long>();

		for (MultipartFile multipartFile : files) {
			File instance = new File();
			// byte[] compressedData = zipData(multipartFile.getBytes(),
			// multipartFile.getOriginalFilename());

			instance.setFilename(multipartFile.getOriginalFilename());
			instance.setSkey(RandomStringUtils.randomAlphanumeric(16));
			instance.setExtension(StringUtils.substringAfterLast(multipartFile.getOriginalFilename(), "."));
			instance.setMimetype(FileUtils.guessMimetype(instance.getFilename()));
			instance.setFilesize(multipartFile.getSize());
			// instance.setFilesize(Long.parseLong(String.valueOf(compressedData.length)));

			instance = save(instance);
			Long fileId = instance.getId();
			fileIdList.add(fileId);
			// fileBlob.setBytes(compressedData);
			Long blobId = fileBlobService.saveOrUpdate(fileId, multipartFile.getInputStream());
			blobIdList.add(blobId);

			FileRef fileRef = new FileRef();
			fileRef.setFileId(fileId);
			fileRef.setRefId(refId);
			fileRef.setRefType(refType);

			if (instance.getMimetype().equals("video/mpeg") ||
					instance.getMimetype().equals("video/mp4") ||
					instance.getMimetype().equals("video/x-matroska") ||
					instance.getMimetype().equals("video/quicktime") ||
					instance.getMimetype().equals("video/x-msvideo")) {

				byte[] thumbnailBlob;
				if (instance.getMimetype().equals("video/mp4") || instance.getMimetype().equals("video/quicktime")) {
					thumbnailBlob = processVideoAndGetThumbnail(multipartFile.getInputStream(), instance.getExtension(),
							true);

				} else {
					thumbnailBlob = processVideoAndGetThumbnail(multipartFile.getInputStream(), instance.getExtension(),
							false);
				}
				Long thumbnailBlobId = fileBlobService.saveOrUpdate((long) -1, new ByteArrayInputStream(thumbnailBlob));
				fileRef.setThumbnailFileId(thumbnailBlobId);
			}

			Long fileRefId = fileRefService.saveOrUpdate(fileRef);
			refIdList.add(fileRefId);
		}

		return Map.of(
				"fileIdList", fileIdList,
				"blobIdList", blobIdList,
				"refIdList", refIdList);
	}

	public List<Map<String, Object>> listFile(Map<String, Object> args) {
		StringBuilder sql = new StringBuilder("SELECT"
				+ " fr.refType, "
				+ " fr.remarks, "
				+ " fr.fileId, "
				+ " f.id, "
				+ " f.skey, "
				+ " f.extension, "
				+ " f.filename, "
				+ " f.filesize, "
				+ " f.mimetype "
				+ " FROM file_ref fr "
				+ " LEFT JOIN file f ON f.id = fr.fileId  "
				+ " WHERE fr.deleted = FALSE "
				+ " AND f.id IS NOT NULL");

		if (args != null) {
			if (args.containsKey("refId"))
				sql.append(" AND fr.refId = :refId ");
			if (args.containsKey("refType"))
				sql.append(" AND fr.refType = :refType");
		}

		sql.append(" ORDER BY fr.modified ");

		return jdbcDao.queryForList(sql.toString(), args);
	}

	private byte[] processVideoAndGetThumbnail(InputStream inputStream, String extension, Boolean isMp4)
			throws IOException {

		// TODO Temporary to avoid memroy leak using inpustream on FFmpegFrameGrabber #javacv 1.5.9
		FileOutputStream tempOutStream = null;
		java.io.File tempFile = null;
		byte[] thumbnailBlob = null;
		try {
			tempFile = java.io.File.createTempFile("temp", "." + extension);
			tempOutStream = new FileOutputStream(tempFile);
			IOUtils.copy(inputStream, tempOutStream);

			// Create a frame grabber to read the video blob
			FFmpegFrameGrabber frameGrabber = new FFmpegFrameGrabber(tempFile);
			if (isMp4) {
				frameGrabber.setOption("-pix_fmt", "yuv420p"); // Set the pixel format option
			}
			frameGrabber.start();
			// 5000000 = 5 seconds
			frameGrabber.setTimestamp(5000000);

			// Submit the frame grabbing task to the executor service
			Future<Frame> frameFuture = executor.submit(() -> frameGrabber.grabImage());

			// Wait for the frame grabbing task to complete and get the frame
			Frame frame;
			frame = frameFuture.get();
			// Grab first frame if video < 5 seconds
			if (frame == null) {
				frameGrabber.setTimestamp(0);
				frameFuture = executor.submit(() -> frameGrabber.grabImage());
				frame = frameFuture.get();
			}

			// Convert the frame to a BufferedImage
			Java2DFrameConverter converter = new Java2DFrameConverter();
			BufferedImage thumbnailImage = converter.getBufferedImage(frame);

			// Convert the thumbnail image to a byte array
			ByteArrayOutputStream thumbnailOutputStream = new ByteArrayOutputStream();
			ImageIO.write(thumbnailImage, "jpg", thumbnailOutputStream);
			thumbnailBlob = thumbnailOutputStream.toByteArray();

			// Clean up resources
			frame.close();
			frameGrabber.close();
			thumbnailImage.flush();
			converter.close();
			thumbnailOutputStream.close();
		} catch (Exception e) {
			// Handle any exceptions that occurred during frame grabbing
			throw new IOException("Error occurred while grabbing frame", e);
		} finally {
			tempOutStream.close();
			inputStream.close();
			if (tempFile.exists()) {
				tempFile.delete();
			}
		}

		return thumbnailBlob;
	}
}
