package org.brapi.test.BrAPITestServer.service.upload;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;

import org.brapi.test.BrAPITestServer.config.MinioProperties;
import org.brapi.test.BrAPITestServer.model.dto.upload.ImageUploadResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import io.minio.BucketExistsArgs;
import io.minio.GetObjectArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.StatObjectArgs;
import io.minio.StatObjectResponse;

@Service
public class MinioImageStorageService {
	private static final Logger LOGGER = LoggerFactory.getLogger(MinioImageStorageService.class);
	private static final Set<String> ALLOWED_IMAGE_TYPES = Set.of(
			"image/jpeg",
			"image/jpg",
			"image/png",
			"image/gif",
			"image/webp",
			"image/bmp");
	private static final long MAX_IMAGE_SIZE = 10 * 1024 * 1024;

	private final MinioClient minioClient;
	private final MinioProperties properties;

	public MinioImageStorageService(MinioClient minioClient, MinioProperties properties) {
		this.minioClient = minioClient;
		this.properties = properties;
	}

	public ImageUploadResponse uploadImage(MultipartFile file) throws Exception {
		validateImage(file);

		String filename = cleanFilename(file.getOriginalFilename());
		String objectName = UUID.randomUUID().toString() + extensionOf(filename);
		String contentType = file.getContentType().toLowerCase(Locale.ROOT);

		try {
			ensureBucket();
			try (InputStream inputStream = file.getInputStream()) {
				minioClient.putObject(PutObjectArgs.builder()
						.bucket(properties.getBucket())
						.object(objectName)
						.stream(inputStream, file.getSize(), -1)
						.contentType(contentType)
						.build());
			}
		} catch (Exception e) {
			if (!properties.isLocalFallbackEnabled()) {
				throw e;
			}
			LOGGER.warn("MinIO image upload failed, storing image locally instead: {}", e.getMessage());
			storeLocal(file, objectName, contentType);
		}

		String encodedObjectName = URLEncoder.encode(objectName, StandardCharsets.UTF_8);
		return new ImageUploadResponse("/brapi/v2/upload/image/" + encodedObjectName,
				filename, contentType, file.getSize());
	}

	public StoredImage loadImage(String objectName) throws Exception {
		validateObjectName(objectName);
		Path localImagePath = localImagePath(objectName);
		if (Files.isRegularFile(localImagePath)) {
			return new StoredImage(Files.newInputStream(localImagePath),
					Files.size(localImagePath),
					localContentType(objectName));
		}

		StatObjectResponse stat = statImage(objectName);
		InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
				.bucket(properties.getBucket())
				.object(objectName)
				.build());
		return new StoredImage(inputStream, stat.size(), stat.contentType());
	}

	public StatObjectResponse statImage(String objectName) throws Exception {
		validateObjectName(objectName);
		return minioClient.statObject(StatObjectArgs.builder()
				.bucket(properties.getBucket())
				.object(objectName)
				.build());
	}

	private void ensureBucket() throws Exception {
		boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder()
				.bucket(properties.getBucket())
				.build());
		if (!bucketExists) {
			minioClient.makeBucket(MakeBucketArgs.builder()
					.bucket(properties.getBucket())
					.build());
		}
	}

	private void validateImage(MultipartFile file) {
		if (file == null || file.isEmpty()) {
			throw new IllegalArgumentException("file is required");
		}
		if (file.getSize() > MAX_IMAGE_SIZE) {
			throw new IllegalArgumentException("image file must be 10 MB or smaller");
		}
		String contentType = file.getContentType();
		if (contentType == null || !ALLOWED_IMAGE_TYPES.contains(contentType.toLowerCase(Locale.ROOT))) {
			throw new IllegalArgumentException("only JPG, PNG, GIF, WebP, and BMP images are supported");
		}
	}

	private String cleanFilename(String filename) {
		String clean = StringUtils.cleanPath(filename == null ? "image" : filename);
		if (clean.contains("..")) {
			return "image";
		}
		return clean.isBlank() ? "image" : clean;
	}

	private String extensionOf(String filename) {
		int dotIndex = filename.lastIndexOf('.');
		if (dotIndex < 0 || dotIndex == filename.length() - 1) {
			return "";
		}
		return filename.substring(dotIndex).toLowerCase();
	}

	private void validateObjectName(String objectName) {
		if (objectName == null || objectName.isBlank()
				|| objectName.contains("/") || objectName.contains("\\") || objectName.contains("..")) {
			throw new IllegalArgumentException("invalid object name");
		}
	}

	private void storeLocal(MultipartFile file, String objectName, String contentType) throws Exception {
		Path imagePath = localImagePath(objectName);
		Files.createDirectories(imagePath.getParent());
		try (InputStream inputStream = file.getInputStream()) {
			Files.copy(inputStream, imagePath, StandardCopyOption.REPLACE_EXISTING);
		}

		Properties metadata = new Properties();
		metadata.setProperty("contentType", contentType);
		try (OutputStream outputStream = Files.newOutputStream(localMetadataPath(objectName))) {
			metadata.store(outputStream, null);
		}
	}

	private Path localImagePath(String objectName) {
		return localStorageDirectory().resolve(objectName).normalize();
	}

	private Path localMetadataPath(String objectName) {
		return localStorageDirectory().resolve(objectName + ".properties").normalize();
	}

	private Path localStorageDirectory() {
		return Paths.get(properties.getLocalDirectory()).toAbsolutePath().normalize();
	}

	private String localContentType(String objectName) {
		Path metadataPath = localMetadataPath(objectName);
		if (Files.isRegularFile(metadataPath)) {
			Properties metadata = new Properties();
			try (InputStream inputStream = Files.newInputStream(metadataPath)) {
				metadata.load(inputStream);
				String contentType = metadata.getProperty("contentType");
				if (contentType != null && !contentType.isBlank()) {
					return contentType;
				}
			} catch (Exception e) {
				LOGGER.warn("Failed to read local image metadata for {}: {}", objectName, e.getMessage());
			}
		}
		return contentTypeFromExtension(objectName);
	}

	private String contentTypeFromExtension(String objectName) {
		String lower = objectName.toLowerCase(Locale.ROOT);
		if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) {
			return "image/jpeg";
		}
		if (lower.endsWith(".png")) {
			return "image/png";
		}
		if (lower.endsWith(".gif")) {
			return "image/gif";
		}
		if (lower.endsWith(".webp")) {
			return "image/webp";
		}
		if (lower.endsWith(".bmp")) {
			return "image/bmp";
		}
		return "application/octet-stream";
	}

	public static class StoredImage {
		private final InputStream inputStream;
		private final long size;
		private final String contentType;

		public StoredImage(InputStream inputStream, long size, String contentType) {
			this.inputStream = inputStream;
			this.size = size;
			this.contentType = contentType;
		}

		public InputStream getInputStream() {
			return inputStream;
		}

		public long getSize() {
			return size;
		}

		public String getContentType() {
			return contentType;
		}
	}
}
