package ru.soune.nocopy.service.file.impl;

import com.vrt.NoCopyFileService;
import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.NoCopyCheckResult;
import com.vrt.fileprotection.audio.AudioCheckResult;
import com.vrt.fileprotection.documents.DocumentCheckResult;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.file.UploadProgressResponse;
import ru.soune.nocopy.entity.file.*;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.tokenoperation.OperationType;
import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.FileUploadSessionRepository;
import ru.soune.nocopy.repository.SimilarImageProjection;
import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.ImageHashService;
import ru.soune.nocopy.service.cost.CostService;
import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.service.file.moderation.ModerationFileService;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.tariff.TariffInfoService;
import ru.soune.nocopy.util.FileUtil;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.file.*;
import java.nio.file.attribute.PosixFilePermission;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;

@Slf4j
@Service
@RequiredArgsConstructor
public class FileUploadServiceImpl implements FileUploadService {

    private final FileUploadSessionRepository sessionRepository;

    @Value("${file.storage.base-path}")
    private String basePath;

    @Value("${file.storage.chunk-size}")
    private int chunkSize;

    @Value("${file.storage.max-file-size}")
    private long maxFileSize;

    @Value("${file.storage.max-retry-attempts:3}")
    private int maxRetryAttempts;

    @Value("${file.storage.session-expiry-hours:24}")
    private int sessionExpiryHours;

    private Path storageRoot;

    @Autowired
    private ImageHashService imageHashService;

    @Autowired
    private FileSimilarityService fileSimilarityService;

    @Autowired
    private FileEntityRepository fileEntityRepository;

    @Autowired
    private FileEntityService fileEntityService;

    private final CostService costService;

    @Lazy
    private final NoCopyFileService noCopyFileService;

    private final FileUtil fileUtil;

    private final TariffInfoService tariffInfoService;

    private final NotificationService notificationService;

    private final AuthService authService;

    private final ModerationFileService moderationFileService;

    private final CloudStorageService cloudStorageService;

    @PostConstruct
    public void init() {
        try {
            storageRoot = Paths.get(basePath).toAbsolutePath().normalize();
            log.info("Initializing file storage at: {}", storageRoot);

            createDirectoryIfNotExists(storageRoot);
            createDirectoryIfNotExists(storageRoot.resolve("temp"));
            createDirectoryIfNotExists(storageRoot.resolve("uploads"));

            if (!System.getProperty("os.name").toLowerCase().contains("win")) {
                setDirectoryPermissions(storageRoot);
            }

            log.info("File storage initialized successfully");

        } catch (IOException e) {
            log.error("Failed to initialize file storage at: {}", basePath, e);
            throw new RuntimeException("Storage initialization failed", e);
        }
    }

    @Override
    @Transactional
    public FileUploadSession initUpload(Long userId, String fileName,
                                        String fileType, String extension, String convertTo, long fileSize) {
        log.info("Initializing upload for user {}: {} ({} bytes, type: {})",
                userId, fileName, fileSize, fileType);

        if (fileSize > maxFileSize) {
            throw new FileUploadException(
                    String.format("File size %d exceeds maximum allowed size %d",
                            fileSize, maxFileSize));
        }

        if (fileSize <= 0) {
            throw new FileUploadException("File size must be positive");
        }

        int totalChunks = (int) Math.ceil((double) fileSize / chunkSize);
        log.debug("File will be split into {} chunks (chunk size: {} bytes)",
                totalChunks, chunkSize);
        Long chunkSize = totalChunks == 1 ? fileSize : 1000000L;

        FileUploadSession session = FileUploadSession.builder()
                .userId(userId)
                .extension(extension)
                .fileName(fileName)
                .fileType(fileType)
                .chunkSize(chunkSize)
                .fileSize(fileSize)
                .totalChunks(totalChunks)
                .chunksUploaded(0)
                .status(UploadStatus.INITIATED)
                .expiresAt(LocalDateTime.now().plusHours(sessionExpiryHours))
                .retryCount(0)
                .convertTo(convertTo)
                .build();

        FileUploadSession savedSession = sessionRepository.save(session);
        log.info("Upload session created: {} for file: {}",
                savedSession.getUploadId(), fileName);

        return savedSession;
    }

    @Override
    @Transactional
    public void handleExpiredSession(FileUploadSession session) {
        session.setStatus(UploadStatus.FAILED);
        session.setLastError("Upload session expired");
        sessionRepository.save(session);

        CompletableFuture.runAsync(() -> cleanupSessionFiles(session));
    }

    @Override
    @Transactional
    public void cancelUpload(String uploadId) {
        FileUploadSession session = sessionRepository.findById(uploadId)
                .orElseThrow(() -> new UploadSessionNotFoundException(uploadId));

        if (session.getStatus() == UploadStatus.COMPLETED || session.getStatus() == UploadStatus.CANCELLED) {
            throw new FileUploadException("Cannot cancel completed or cancelled upload");
        }

        session.setStatus(UploadStatus.CANCELLED);
        sessionRepository.save(session);

        CompletableFuture.runAsync(() -> cleanupSessionFiles(session));

        log.info("Upload cancelled: {}", uploadId);
    }

    @Override
    public UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber,
                                              MultipartFile chunkFile, Integer findSimilar) {
        FileUploadSession session = sessionRepository.findById(uploadId)
                .orElseThrow(() -> new UploadSessionNotFoundException(uploadId));

        validateSession(session, chunkNumber, chunkFile);

        return processChunk(session, chunkNumber, chunkFile, findSimilar);
    }

    @Override
    public UploadProgressResponse uploadPassportChunk(String uploadId, Integer chunkNumber, MultipartFile chunkFile,
                                                      Long userId) {
        FileUploadSession session = sessionRepository.findById(uploadId).orElseThrow(() ->
                new UploadSessionNotFoundException(uploadId));

        validateSession(session, chunkNumber, chunkFile);

        UploadProgressResponse uploadProgressResponse = processChunk(session, chunkNumber, chunkFile);

        return uploadProgressResponse;
    }

    @Override
    public UploadProgressResponse getUploadProgress(String uploadId) {
        FileUploadSession session = sessionRepository.findById(uploadId)
                .orElseThrow(() -> new UploadSessionNotFoundException(uploadId));

        return UploadProgressResponse.fromSession(session);
    }

    @Transactional
    @Override
    public FileEntity completeFileProcessing(FileUploadSession session, FileStatus status) {
        try {
            Path filePath = Paths.get(session.getFilePath());
            String checksum = calculateChecksum(filePath);

            FileEntity fileEntity = FileEntity.builder()
                    .userId(session.getUserId())
                    .originalFileName(session.getFileName())
                    .storedFileName(filePath.getFileName().toString())
                    .filePath(session.getFilePath())
                    .fileSize(session.getFileSize())
                    .mimeType(session.getFileType())
                    .fileExtension(session.getExtension())
                    .checksum(checksum)
                    .uploadSessionId(session.getUploadId())
                    .status(status)
                    .build();

            if (status != FileStatus.TEMP) {
                fileEntity.setProtectionStatus(ProtectionStatus.PROCESSING);
            }

            FileEntity saved = fileEntityRepository.save(fileEntity);

            if (session.getFileType().equals("image") && status != FileStatus.PRIVATE) {
                Map<String, Long> hash = imageHashService.calculateHash(filePath);
                imageHashService.create(saved, hash);
            }

            cleanupSessionFiles(session);
            log.info("File processing completed for session: {}", session.getUploadId());

            return saved;
        } catch (Exception e) {
            log.error("Failed to complete file processing for session {}: {}",
                    session.getUploadId(), e.getMessage(), e);
            throw new FileUploadException("Failed to complete file processing", e);
        }
    }

    private void validateSession(FileUploadSession session, Integer chunkNumber,
                                 MultipartFile chunkFile) {
        UploadStatus status = session.getStatus();

        if (status == UploadStatus.FAILED) {
            if (session.getRetryCount() >= maxRetryAttempts) {
                throw new FileUploadException(
                        "Upload failed after maximum retry attempts");
            }
            session.setStatus(UploadStatus.UPLOADING);
            session.setRetryCount(session.getRetryCount() + 1);
            sessionRepository.save(session);
            log.info("Retrying failed upload session: {}, attempt: {}",
                    session.getUploadId(), session.getRetryCount());
        }

        if (status == UploadStatus.COMPLETED) {
            throw new FileUploadException("Upload already completed");
        }

        if (status == UploadStatus.CANCELLED) {
            throw new FileUploadException("Upload was cancelled");
        }

        if (status == UploadStatus.INITIATED) {
            session.setStatus(UploadStatus.UPLOADING);
            sessionRepository.save(session);
        }

        if (session.getExpiresAt().isBefore(LocalDateTime.now())) {
            handleExpiredSession(session);
            throw new FileUploadException("Upload session expired");
        }

        if (chunkNumber < 0 || chunkNumber >= session.getTotalChunks()) {
            throw new FileUploadException(
                    String.format("Invalid chunk number %d. Expected number: %d",
                            chunkNumber, session.getTotalChunks() - 1));
        }

        if (chunkNumber == 0 && chunkFile.getSize() > chunkSize || chunkNumber + 1 == (session.getTotalChunks())
                && chunkFile.getSize() > chunkSize) {
            throw new ChunkSizeExceededException(chunkFile.getSize(), chunkSize);
        }

        if (chunkNumber > 0 && chunkNumber + 1 < session.getTotalChunks() && chunkFile.getSize() != chunkSize) {
            throw new ChunkSizeExceededException(chunkFile.getSize(), chunkSize);
        }
    }

    private UploadProgressResponse processChunk(FileUploadSession session, Integer chunkNumber, MultipartFile chunkFile) {
        String chunkPath = null;

        try {
            chunkPath = saveChunkWithIntegrityCheck(session, chunkNumber, chunkFile);

            session.getChunkPaths().put(chunkNumber, chunkPath);
            session.setChunksUploaded(session.getChunksUploaded() + 1);
            session.setStatus(UploadStatus.UPLOADING);
            session.setExpiresAt(LocalDateTime.now().plusMinutes(1));

            boolean isLastChunk = session.getChunksUploaded().equals(session.getTotalChunks());

            if (isLastChunk) {
                String finalFilePath = assembleFileSynchronously(session);

                session.setStatus(UploadStatus.COMPLETED);
                session.setFilePath(finalFilePath);

                completeFileProcessing(session, FileStatus.PRIVATE);
            } else {
                sessionRepository.save(session);
            }

            return UploadProgressResponse.fromSession(session);
        } catch (Exception e) {
            if (chunkPath != null) {
                cleanupFailedChunk(chunkPath);
            }

            log.error("Failed to process chunk {} for session {}: {}",
                    chunkNumber, session.getUploadId(), e.getMessage(), e);

            session.setStatus(UploadStatus.FAILED);
            session.setLastError(e.getMessage());
            sessionRepository.save(session);

            throw new FileUploadException("Failed to upload chunk: " + e.getMessage(), e);
        }
    }

    private UploadProgressResponse processChunk(FileUploadSession session, Integer chunkNumber, MultipartFile chunkFile,
                                                Integer findSimilar) {
        String chunkPath = null;

        try {
            if (session.getChunkPaths().containsKey(chunkNumber)) {
                return handleExistingChunk(session, chunkNumber, chunkFile, findSimilar);
            }

            chunkPath = saveChunkWithIntegrityCheck(session, chunkNumber, chunkFile);

            session.getChunkPaths().put(chunkNumber, chunkPath);
            session.setChunksUploaded(session.getChunksUploaded() + 1);
            session.setStatus(UploadStatus.UPLOADING);
            session.setExpiresAt(LocalDateTime.now().plusMinutes(1));

            log.debug("Chunk {} saved successfully. Uploaded: {}/{}",
                    chunkNumber, session.getChunksUploaded(), session.getTotalChunks());

            boolean isLastChunk = session.getChunksUploaded().equals(session.getTotalChunks());
        //TODO CHECK
            if (isLastChunk) {
                String finalFilePath = assembleFileSynchronously(session);

                if (findSimilar == 0) {
                    if (session.getFileType().startsWith("image")) {
                        checkForDuplicatesSynchronously(finalFilePath);
                    } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) {
                        fileSimilarityService.hasDuplicatesByHash(finalFilePath ,session.getFileType(),
                                session.getUserId());

                        File file = new File(finalFilePath);

                        if (session.getFileType().startsWith("audio") && !isWavFile(file)) {
                            session.setStatus(UploadStatus.FAILED);
                            session.setLastError("wav file not have RIFF header");

                            sessionRepository.save(session);

                            throw new FileFormatException("Failed to upload chunk: .wav file not have RIFF header or " +
                                    "another problems with .wav file");
                        }

                        FileProtector.Type type = "document".equals(session.getFileType()) ?
                                FileProtector.Type.DOC : FileProtector.Type.valueOf(session.getFileType().toUpperCase());

                        NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, type);

                        switch (checkResult) {
                            case DocumentCheckResult.Success success -> {
                                FileProtector.FileInfo info = success.getInfo();
                                throw new DuplicateImageException("Duplicate", info.getId(),
                                        Long.valueOf(info.getOwnerId()));
                            }
                            case AudioCheckResult.Success success -> {
                                FileProtector.FileInfo info = success.getInfo();
                                throw new DuplicateImageException("Duplicate", info.getId(),
                                        Long.valueOf(info.getOwnerId()));
                            }
                            case DocumentCheckResult.Failed failed ->
                                    log.warn("Document check failed: {}", failed.getMessage());
                            case AudioCheckResult.Failed failed ->
                                    log.warn("Audio check failed: {}", failed.getMessage());
                            default -> log.warn("Unexpected result type: {}", checkResult.getClass().getSimpleName());
                        }
                    }
                }

                session.setStatus(UploadStatus.COMPLETED);
                session.setFilePath(finalFilePath);

                FileStatus status = findSimilar == 0 ? FileStatus.MODERATION: FileStatus.TEMP;

                if (status == FileStatus.TEMP) {
                    fileEntityService.clearTempFiles(session.getUserId());
                }

                FileEntity fileEntity = completeFileProcessing(session, status);

                cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
                        fileEntity.getFilePath());

                if (status != FileStatus.TEMP) {
                    String fileType = session.getFileType();

                    tariffInfoService.writeOffTokens(session.getUserId(), costService.protectFileCost(fileType),
                            OperationType.FILE_UPLOAD);
                    notificationService.addNotification(NotificationType.FILE_ADDED_TO_SYSTEM, session.getUserId());
                }

                if (status != FileStatus.TEMP) {
                    FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, session.getConvertTo());

                    noCopyFileService.addFile(fileInfo);
                }
            } else {
                sessionRepository.save(session);
            }

            return UploadProgressResponse.fromSession(session);

        } catch (DuplicateImageException e) {
            if (chunkPath != null) {
                cleanupFailedChunk(chunkPath);
            }

            log.warn("Duplicate image found for session {}: {}",
                    session.getUploadId(), e.getMessage());

            session.setStatus(UploadStatus.FAILED);
            session.setLastError(e.getMessage());
            sessionRepository.save(session);

            throw e;
        } catch (Exception e) {
            if (chunkPath != null) {
                cleanupFailedChunk(chunkPath);
            }

            log.error("Failed to process chunk {} for session {}: {}",
                    chunkNumber, session.getUploadId(), e.getMessage(), e);

            session.setStatus(UploadStatus.FAILED);
            session.setLastError(e.getMessage());
            sessionRepository.save(session);

            throw new FileUploadException("Failed to upload chunk: " + e.getMessage(), e);
        }
    }

    //TODO убрать когда научимся защищать другие типы
    public boolean isWavFile(File file) {
        if (file == null || !file.exists() || file.length() < 12) {
            return false;
        }

        try (FileInputStream fis = new FileInputStream(file)) {
            byte[] header = new byte[4];
            int bytesRead = fis.read(header);

            if (bytesRead < 4) {
                return false;
            }

            return header[0] == 82 && header[1] == 73 &&
                    header[2] == 70 && header[3] == 70;

        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }

    public static boolean isValidWav(File file) {
        if (file == null || !file.exists() || file.length() < 44) {
            return false;
        }

        try (FileInputStream fis = new FileInputStream(file)) {
            byte[] header = new byte[44];
            if (fis.read(header) < 44) {
                return false;
            }

            if (header[0] != 'R' || header[1] != 'I' ||
                    header[2] != 'F' || header[3] != 'F') {
                return false;
            }

            if (header[8] != 'W' || header[9] != 'A' ||
                    header[10] != 'V' || header[11] != 'E') {
                return false;
            }

            if (header[12] != 'f' || header[13] != 'm' ||
                    header[14] != 't' || header[15] != ' ') {
                return false;
            }

            if (header[36] != 'd' || header[37] != 'a' ||
                    header[38] != 't' || header[39] != 'a') {
                return false;
            }

            int audioFormat = (header[20] & 0xFF) | (header[21] & 0xFF) << 8;
            if (audioFormat != 1 && audioFormat != 3) {
                return false;
            }

            int numChannels = (header[22] & 0xFF) | (header[23] & 0xFF) << 8;
            if (numChannels < 1 || numChannels > 8) {
                return false;
            }

            int sampleRate = (header[24] & 0xFF) | (header[25] & 0xFF) << 8 |
                    (header[26] & 0xFF) << 16 | (header[27] & 0xFF) << 24;
            if (sampleRate < 8000 || sampleRate > 192000) {
                return false;
            }

            int bitsPerSample = (header[34] & 0xFF) | (header[35] & 0xFF) << 8;
            if (bitsPerSample < 8 || bitsPerSample > 32) {
                return false;
            }

            return true;

        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }

    private String assembleFileSynchronously(FileUploadSession session) throws IOException {
        Path finalFilePath = prepareFinalFile(session);
        log.info("file path: " + finalFilePath);
        validateAllChunksExist(session);
        log.info("validateAllChunksExist : " + session);
        mergeChunksToFile(session, finalFilePath);
        log.info("mergeChunksToFile: " + session);
        validateFinalFile(session, finalFilePath);
        log.info("finalFilePath: " + finalFilePath);
        return finalFilePath.toString();
    }

    @Override
    public void checkForDuplicatesSynchronously(String filePath) throws IOException , DuplicateImageException {
        Path path = Paths.get(filePath);

        Map<String, Long> hash = imageHashService.calculateHash(path);

        List<SimilarImageProjection> duplicates = fileSimilarityService.findDuplicatedByHash(
                hash.get("hi"), hash.get("low"));

        if (!duplicates.isEmpty()) {
            throw new DuplicateImageException("Duplicate", duplicates.getFirst().getId(),
                    duplicates.getFirst().getUserId());
        }
    }

//    public Path downloadImageFromUrl(String urlString) throws IOException {
//        String[] urlVariants = buildUrlVariants(urlString);
//
//        IOException lastException = null;
//
//        for (String variantUrl : urlVariants) {
//            try {
//                Path result = attemptDownload(variantUrl);
//                if (result != null) {
//                    log.info("Success download: {} -> {}", urlString, variantUrl);
//                    return result;
//                }
//            } catch (IOException e) {
//                lastException = e;
//                log.warn("Failed download {}: {}", variantUrl, e.getMessage());
//            }
//        }
//
//        throw new IOException("Failed to download from all variants: " + urlString, lastException);
//    }
//
//    private String[] buildUrlVariants(String originalUrl) {
//        List<String> variants = new ArrayList<>();
//
//        variants.add(originalUrl);
//
//        if (originalUrl.contains("i.ytimg.com") || originalUrl.contains("youtube.com")) {
//            try {
//                URL url = new URL(originalUrl);
//                String path = url.getPath();
//
//                if ("https".equals(url.getProtocol())) {
//                    variants.add("http://i.ytimg.com" + path);
//                }
//
//                String[][] alternativeHosts = {
//                        {"i.ytimg.com", "i3.ytimg.com"},
//                        {"i.ytimg.com", "img.youtube.com"},
//                        {"i.ytimg.com", "yt3.ggpht.com"}
//                };
//
//                for (String[] hosts : alternativeHosts) {
//                    if (originalUrl.contains(hosts[0])) {
//                        variants.add(originalUrl.replace(hosts[0], hosts[1]));
//                        variants.add("http://" + hosts[1] + path);
//                    }
//                }
//
//            } catch (MalformedURLException e) {
//                log.warn("Not valid URL: {}", originalUrl);
//            }
//        }
//
//        return variants.toArray(new String[0]);
//    }
//
//    private Path attemptDownload(String urlString) throws IOException {
//        int maxRetries = 2;
//        int connectTimeout = 10000;
//        int readTimeout = 10000;
//
//        IOException lastException = null;
//
//        for (int attempt = 0; attempt < maxRetries; attempt++) {
//            Path tempFile = null;
//            HttpURLConnection connection = null;
//
//            try {
//                URL url = new URL(urlString);
//
//                String extension = getFileExtension(url.getPath());
//
//                tempFile = Files.createTempFile("upload_check_", extension);
//
//                connection = (HttpURLConnection) url.openConnection();
//                connection.setRequestProperty("User-Agent",
//                        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
//                connection.setConnectTimeout(connectTimeout);
//                connection.setReadTimeout(readTimeout);
//                connection.setInstanceFollowRedirects(true);
//
//                int responseCode = connection.getResponseCode();
//                log.debug("{} -> Response code: {} (попытка {})", urlString, responseCode, attempt + 1);
//
//                if (responseCode != 200) {
//                    throw new IOException("Bad response code: " + responseCode);
//                }
//
//                try (InputStream in = connection.getInputStream()) {
//                    Files.copy(in, tempFile, StandardCopyOption.REPLACE_EXISTING);
//                }
//
//                long size = Files.size(tempFile);
//                log.debug("Downloaded: {} -> size: {} byte", urlString, size);
//
//                if (size == 0) {
//                    throw new IOException("Downloaded empty file");
//                }
//
//                return tempFile;
//
//            } catch (SocketTimeoutException e) {
//                lastException = e;
//                log.warn("Timeout {} (try {}/{})", urlString, attempt + 1, maxRetries);
//
//                connectTimeout *= 2;
//                readTimeout *= 2;
//
//                if (attempt < maxRetries - 1) {
//                    try {
//                        Thread.sleep(1000 * (attempt + 1));
//                    } catch (InterruptedException ie) {
//                        Thread.currentThread().interrupt();
//                        break;
//                    }
//                }
//
//            } catch (IOException e) {
//                lastException = e;
//                if (!(e instanceof SocketTimeoutException)) {
//                    break;
//                }
//
//            } finally {
//                if (connection != null) {
//                    connection.disconnect();
//                }
//            }
//
//            if (tempFile != null && Files.exists(tempFile)) {
//                try {
//                    Files.deleteIfExists(tempFile);
//                } catch (IOException e) {
//                    log.warn("Temp file not found: {}", tempFile);
//                }
//            }
//        }
//
//        throw new IOException("Not download " + maxRetries + " tries: " + urlString, lastException);
//    }
//
//    private String getFileExtension(String path) {
//        String extension = ".tmp";
//        int lastDot = path.lastIndexOf('.');
//        if (lastDot > 0) {
//            String ext = path.substring(lastDot).toLowerCase();
//            if (ext.matches("\\.(jpg|jpeg|png|gif|bmp|webp)")) {
//                extension = ext;
//            }
//        }
//        return extension;
//    }

    public Path downloadImageFromUrl(String urlString) throws IOException {
        if (urlString.startsWith("https://i.ytimg.com")) {
            urlString = urlString.replace("https://", "http://");
            log.info("Change blocked domen: {}", urlString);
        }

        URL url = new URL(urlString);

        String extension = ".tmp";
        String path = url.getPath();
        int lastDot = path.lastIndexOf('.');
        if (lastDot > 0) {
            String ext = path.substring(lastDot).toLowerCase();
            if (ext.matches("\\.(jpg|jpeg|png|gif|bmp|webp)")) {
                extension = ext;
            }
        }

        Path tempFile = Files.createTempFile("upload_check_", extension);

        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
        connection.setConnectTimeout(10000);
        connection.setReadTimeout(10000);
        connection.setInstanceFollowRedirects(true);

        try {
            int responseCode = connection.getResponseCode();
            System.out.println("Response code: " + responseCode);

            if (responseCode != 200) {
                throw new IOException("Bad response code: " + responseCode);
            }

            try (InputStream in = connection.getInputStream()) {
                Files.copy(in, tempFile, StandardCopyOption.REPLACE_EXISTING);
            }

            long size = Files.size(tempFile);
            System.out.println("File downloaded: " + tempFile + ", size: " + size + " bytes");

            if (size == 0) {
                throw new IOException("Downloaded empty file");
            }

            return tempFile;

        } catch (Exception e) {
            Files.deleteIfExists(tempFile);
            throw new IOException("Failed to download: " + urlString, e);
        } finally {
            connection.disconnect();
        }
    }

    private UploadProgressResponse handleExistingChunk(FileUploadSession session,
                                                       Integer chunkNumber,
                                                       MultipartFile chunkFile,
                                                       Integer findSimilar) throws IOException {
        String existingPath = session.getChunkPaths().get(chunkNumber);
        Path chunkPath = Paths.get(existingPath);

        if (!Files.exists(chunkPath)) {
            session.getChunkPaths().remove(chunkNumber);
            session.setChunksUploaded(session.getChunksUploaded() - 1);
            sessionRepository.save(session);

            return processChunk(session, chunkNumber, chunkFile, findSimilar);
        }

        long existingSize = Files.size(chunkPath);

        if (existingSize != chunkFile.getSize()) {
            Files.deleteIfExists(chunkPath);
            session.getChunkPaths().remove(chunkNumber);
            session.setChunksUploaded(session.getChunksUploaded() - 1);
            sessionRepository.save(session);

            return processChunk(session, chunkNumber, chunkFile, findSimilar);
        }

        return UploadProgressResponse.fromSession(session);
    }

    private String saveChunkWithIntegrityCheck(FileUploadSession session,
                                               Integer chunkNumber,
                                               MultipartFile chunkFile) throws IOException {

        Long userId = session.getUserId();
        String uploadId = session.getUploadId();

        Path chunkDir = getChunkDirectory(userId, uploadId);
        Files.createDirectories(chunkDir);

        String tempFileName = String.format("chunk_%04d.%s.tmp",
                chunkNumber, UUID.randomUUID());
        String finalFileName = String.format("chunk_%04d.tmp", chunkNumber);

        Path tempPath = chunkDir.resolve(tempFileName);
        Path finalPath = chunkDir.resolve(finalFileName);

        try {
            chunkFile.transferTo(tempPath.toFile());

            long savedSize = Files.size(tempPath);
            long uploadedSize = chunkFile.getSize();

            if (uploadedSize > 0 && savedSize != uploadedSize) {
                throw new IOException(
                        String.format("Size mismatch: saved %d, uploaded %d",
                                savedSize, uploadedSize));
            }

            Files.move(tempPath, finalPath, StandardCopyOption.ATOMIC_MOVE);

            log.debug("Chunk {} saved successfully: {} bytes",
                    chunkNumber, savedSize);

            return finalPath.toString();

        } finally {
            Files.deleteIfExists(tempPath);
        }
    }

    private void cleanupFailedChunk(String chunkPath) {
        try {
            Files.deleteIfExists(Paths.get(chunkPath));
            log.debug("Cleaned up failed chunk: {}", chunkPath);
        } catch (IOException e) {
            log.warn("Failed to cleanup chunk: {}", chunkPath, e);
        }
    }

    private Path prepareFinalFile(FileUploadSession session) throws IOException {
        Path userUploadsDir = storageRoot.resolve("uploads")
                .resolve(String.valueOf(session.getUserId()))
                .resolve(extractCategory(session.getFileType()));

        Files.createDirectories(userUploadsDir);

        String safeFileName = generateUniqueFileName(session.getFileName(), session.getExtension());
        Path finalPath = userUploadsDir.resolve(safeFileName);

        if (Files.exists(finalPath)) {
            throw new IOException("File already exists: " + finalPath);
        }

        return finalPath;
    }

    private String generateUniqueFileName(String originalName, String extension) {
        int dotIndex = originalName.lastIndexOf('.');
        log.info("dotIndex: {}", dotIndex);
        log.info("originalName: {}", originalName);
        String nameWithoutExtension;

        if (dotIndex > 0) {
            nameWithoutExtension = originalName.substring(0, dotIndex)
                    .replaceAll("[^a-zA-Z0-9\\-_]", "_");
        } else {
            nameWithoutExtension = originalName.replaceAll("[^a-zA-Z0-9\\-_]", "_");
        }

        String timestamp = String.valueOf(System.currentTimeMillis());
        String uuid = UUID.randomUUID().toString().substring(0, 8);

        return String.format("%s_%s_%s.%s",
                nameWithoutExtension, timestamp, uuid, extension);
    }

    private void validateAllChunksExist(FileUploadSession session) throws IOException {
        Path chunkDir = getChunkDirectory(session.getUserId(), session.getUploadId());

        for (int i = 0; i < session.getTotalChunks(); i++) {
            Path chunkPath = chunkDir.resolve(String.format("chunk_%04d.tmp", i));
            if (!Files.exists(chunkPath)) {
                throw new IOException("Missing chunk: " + chunkPath.getFileName());
            }

            if (Files.size(chunkPath) == 0) {
                throw new IOException("Empty chunk: " + chunkPath.getFileName());
            }
        }
    }

    private void mergeChunksToFile(FileUploadSession session, Path finalPath)
            throws IOException {

        Path chunkDir = getChunkDirectory(session.getUserId(), session.getUploadId());
        byte[] buffer = new byte[8192];

        try (BufferedOutputStream outputStream = new BufferedOutputStream(
                Files.newOutputStream(finalPath, StandardOpenOption.CREATE_NEW))) {

            long totalWritten = 0;

            for (int i = 0; i < session.getTotalChunks(); i++) {
                Path chunkPath = chunkDir.resolve(String.format("chunk_%04d.tmp", i));

                try (BufferedInputStream inputStream = new BufferedInputStream(
                        Files.newInputStream(chunkPath))) {

                    int bytesRead;
                    while ((bytesRead = inputStream.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, bytesRead);
                        totalWritten += bytesRead;
                    }
                }

                if (log.isDebugEnabled()) {
                    log.debug("Processed chunk {}: {} bytes (total: {})",
                            i, Files.size(chunkPath), totalWritten);
                }
            }

            outputStream.flush();
            log.info("Total bytes written to final file: {}", totalWritten);
        }
    }

    private void validateFinalFile(FileUploadSession session, Path finalPath)
            throws IOException {

        long actualSize = Files.size(finalPath);

        if (actualSize != session.getFileSize()) {
            throw new IOException(
                    String.format("File size mismatch: expected %d, got %d",
                            session.getFileSize(), actualSize));
        }

        String checksum = calculateChecksum(finalPath);
        log.debug("Final file checksum: {}", checksum);
    }

    private void cleanupSessionFiles(FileUploadSession session) {
        try {
            Path chunkDir = getChunkDirectory(session.getUserId(), session.getUploadId());
            if (Files.exists(chunkDir)) {
                deleteDirectoryRecursively(chunkDir);
                log.info("Cleaned up temp directory for session: {}",
                        session.getUploadId());
            }
        } catch (IOException e) {
            log.warn("Failed to cleanup temp files for session {}: {}",
                    session.getUploadId(), e.getMessage());
        }
    }

    private void deleteDirectoryRecursively(Path directory) throws IOException {
        try (Stream<Path> walk = Files.walk(directory)) {
            walk.sorted(Comparator.reverseOrder())
                    .forEach(path -> {
                        try {
                            Files.deleteIfExists(path);
                        } catch (IOException e) {
                            log.warn("Failed to delete: {}", path, e);
                        }
                    });
        }
    }

    private void handleAssemblyFailure(FileUploadSession session, Exception e) {
        session.setStatus(UploadStatus.FAILED);
        session.setLastError(e.getMessage());

        sessionRepository.save(session);
    }

    private Path getChunkDirectory(Long userId, String uploadId) {
        return storageRoot.resolve("temp")
                .resolve("user")
                .resolve(String.valueOf(userId))
                .resolve(uploadId);
    }

    private String extractCategory(String fileType) {
        if (fileType == null) {
            return "other";
        }

        String lowerType = fileType.toLowerCase();
        if (lowerType.startsWith("image")) {
            return "images";
        } else if (lowerType.startsWith("video")) {
            return "videos";
        } else if (lowerType.startsWith("audio")) {
            return "audio";
        } else if (lowerType.contains("document")) {
            return "documents";
        } else {
            return "other";
        }
    }

    private String calculateChecksum(Path filePath) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] buffer = new byte[8192];

            try (InputStream inputStream = Files.newInputStream(filePath)) {
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    md.update(buffer, 0, bytesRead);
                }
            }

            byte[] digest = md.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }

            return sb.toString();

        } catch (NoSuchAlgorithmException | IOException e) {
            log.warn("Failed to calculate checksum for file: {}", filePath, e);
            return "N/A";
        }
    }

    private void createDirectoryIfNotExists(Path directory) throws IOException {
        if (!Files.exists(directory)) {
            Files.createDirectories(directory);
            log.debug("Created directory: {}", directory);
        }
    }

    private void setDirectoryPermissions(Path directory) throws IOException {
        try {
            Set<PosixFilePermission> permissions = new HashSet<>();
            permissions.add(PosixFilePermission.OWNER_READ);
            permissions.add(PosixFilePermission.OWNER_WRITE);
            permissions.add(PosixFilePermission.OWNER_EXECUTE);
            permissions.add(PosixFilePermission.GROUP_READ);
            permissions.add(PosixFilePermission.GROUP_EXECUTE);
            permissions.add(PosixFilePermission.OTHERS_READ);

            Files.setPosixFilePermissions(directory, permissions);
            log.debug("Set permissions for directory: {}", directory);

        } catch (UnsupportedOperationException e) {
            log.debug("Posix permissions not supported on this system");
        }
    }
}