package org.raddatz.familienarchiv.filestorage;

import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.core.io.InputStreamResource;

import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.UUID;

@Service
public class FileService {

    private static final Logger log = LoggerFactory.getLogger(FileService.class);

    private final S3Client s3Client;
    private final S3Presigner s3Presigner;
    private final String bucketName;

    public FileService(S3Client s3Client, S3Presigner s3Presigner,
                       @Value("${app.s3.bucket}") String bucketName) {
        this.s3Client = s3Client;
        this.s3Presigner = s3Presigner;
        this.bucketName = bucketName;
    }

    /**
     * Uploads a file to S3/MinIO.
     * Returns an {@link UploadResult} containing the S3 key and the SHA-256
     * hash of the file content.  The hash is used to link annotations to the
     * specific file version they were created against.
     */
    public UploadResult uploadFile(MultipartFile file, String originalFilename) throws IOException {
        byte[] bytes = file.getBytes();
        String fileHash = sha256Hex(bytes);
        String s3Key = "documents/" + UUID.randomUUID() + "_" + originalFilename;

        try {
            PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(s3Key)
                    .contentType(file.getContentType())
                    .build();

            s3Client.putObject(putObjectRequest, RequestBody.fromBytes(bytes));

            log.info("Uploaded file to S3: {} (hash={})", s3Key, fileHash);
            return new UploadResult(s3Key, fileHash);
        } catch (S3Exception e) {
            log.error("S3 Upload Error", e);
            throw new IOException("Failed to upload file to storage", e);
        }
    }

    /**
     * Downloads a file stream from S3/MinIO.
     * Returns a wrapper containing the stream and content type.
     */
    public S3FileDownload downloadFile(String s3Key) {
        try {
            GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(s3Key)
                    .build();

            ResponseInputStream<GetObjectResponse> s3Object = s3Client.getObject(getObjectRequest);

            String contentType = s3Object.response().contentType();
            if (contentType == null || contentType.isBlank()) {
                contentType = "application/octet-stream";
            }

            return new S3FileDownload(new InputStreamResource(s3Object), contentType);

        } catch (NoSuchKeyException e) {
            throw new StorageFileNotFoundException("File not found in storage: " + s3Key);
        } catch (S3Exception e) {
            throw new RuntimeException("Storage Error: " + e.getMessage());
        }
    }

    /**
     * Downloads a file from S3/MinIO and returns its raw bytes.
     * Used for hash backfill — callers are responsible for not calling this on large files unnecessarily.
     */
    public byte[] downloadFileBytes(String s3Key) throws IOException {
        try {
            GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(s3Key)
                    .build();
            try (InputStream in = s3Client.getObject(getObjectRequest)) {
                return in.readAllBytes();
            }
        } catch (NoSuchKeyException e) {
            throw new StorageFileNotFoundException("File not found in storage: " + s3Key);
        } catch (S3Exception e) {
            throw new IOException("Failed to download file from storage: " + e.getMessage(), e);
        }
    }

    /**
     * Opens a streaming download from S3/MinIO. The caller is responsible for
     * closing the returned stream — typically via try-with-resources. Preferred
     * over {@link #downloadFileBytes(String)} for large files (multi-MB PDFs
     * during thumbnail generation) because it avoids loading the entire file
     * into heap memory.
     */
    public InputStream downloadFileStream(String s3Key) throws IOException {
        try {
            GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(s3Key)
                    .build();
            return s3Client.getObject(getObjectRequest);
        } catch (NoSuchKeyException e) {
            throw new StorageFileNotFoundException("File not found in storage: " + s3Key);
        } catch (S3Exception e) {
            throw new IOException("Failed to open stream from storage: " + e.getMessage(), e);
        }
    }

    /**
     * Generates a presigned URL for downloading an object from S3/MinIO.
     * Valid for 1 hour — covers multi-page documents on CPU-only OCR hardware
     * (a 100-page document at 10 s/page takes ~17 min; 1 h gives ample headroom).
     */
    public String generatePresignedUrl(String s3Key) {
        GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                .bucket(bucketName)
                .key(s3Key)
                .build();

        GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
                .signatureDuration(Duration.ofHours(1))
                .getObjectRequest(getObjectRequest)
                .build();

        return s3Presigner.presignGetObject(presignRequest).url().toString();
    }

    // ─── private helpers ──────────────────────────────────────────────────────

    private static String sha256Hex(byte[] bytes) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(bytes);
            StringBuilder sb = new StringBuilder(64);
            for (byte b : hash) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 not available", e);
        }
    }

    // ─── result types ─────────────────────────────────────────────────────────

    /** Carries the S3 object key and the content hash back to the caller. */
    public record UploadResult(String s3Key, String fileHash) {}

    /** Carries the download stream and content type. */
    public record S3FileDownload(InputStreamResource resource, String contentType) {}

    public static class StorageFileNotFoundException extends RuntimeException {
        public StorageFileNotFoundException(String message) { super(message); }
    }
}
