package ru.soune.nocopy.service.file;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.file.FileEntityResponse;
import ru.soune.nocopy.dto.file.FileResponse;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.FileUploadSession;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity;
import ru.soune.nocopy.entity.monitoring.MonitoringType;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.DuplicateImageException;
import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.handler.MonitoringHandler;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.FileMonitoringRepository;
import ru.soune.nocopy.repository.SimilarImageProjection;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.ImageHashService;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class FileEntityService {

    private final FileEntityRepository fileEntityRepository;

    private final ImageHashService imageHashService;

    private final FileSimilarityService fileSimilarityService;

    private final UserRepository userRepository;

    private final ImageResizeService imageResizeService;

    @Value("${server.baseurl}")
    private String baseUrl;

    @Transactional(noRollbackFor = DuplicateImageException.class)
    public FileEntity createFromUploadSession(FileUploadSession session, String checksum) {
        log.info("Creating FileEntity for upload session: {}", session.getUploadId());

        try {
            Path filePath = Paths.get(session.getFilePath());

            if (!Files.exists(filePath)) {
                throw new IOException("File not found on disk: " + filePath);
            }

            Map<String, Long> imageHash = Map.of();

            if (session.getFileType().startsWith("image")) {
                imageHash = imageHashService.calculateHash(filePath);
                List<SimilarImageProjection> duplicatedByHash = fileSimilarityService.findDuplicatedByHash(
                        imageHash.get("hi"), imageHash.get("low"));

                if (!duplicatedByHash.isEmpty()) {
                    SimilarImageProjection similarImageProjection = duplicatedByHash.get(0);

                    throw new DuplicateImageException("Duplicate", similarImageProjection.getId(),
                            similarImageProjection.getUserId());
                }
            }

            long fileSize = Files.size(filePath);
            String originalName = session.getFileName();
            String storedName = filePath.getFileName().toString();

            FileEntity fileEntity = FileEntity.builder()
                    .userId(session.getUserId())
                    .originalFileName(originalName)
                    .storedFileName(storedName)
                    .filePath(session.getFilePath())
                    .fileSize(fileSize)
                    .mimeType(session.getFileType())
                    .fileExtension(session.getExtension())
                    .checksum(checksum)
                    .uploadSessionId(session.getUploadId())
                    .status(FileStatus.ACTIVE)
                    .build();

            FileEntity saved = fileEntityRepository.save(fileEntity);

            if (!imageHash.isEmpty()) {
                imageHashService.create(saved, imageHash);
            }

            return saved;
        } catch (IOException e) {
            log.error("Failed to create FileEntity for session {}: {}",
                    session.getUploadId(), e.getMessage(), e);
            throw new RuntimeException("Failed to create file entity: " + e.getMessage(), e);
        }
    }

    @Transactional(readOnly = true)
    public FileEntityResponse getById(String fileId, int version) {
        FileEntity fileEntity = fileEntityRepository.findById(fileId)
                .orElseThrow(() -> new FileEntityNotFoundException(fileId));

        return convertToResponse(fileEntity, version);
    }

    @Transactional(readOnly = true)
    public FileEntityResponse getByUploadSessionId(String uploadSessionId, int version) {
        FileEntity fileEntity = fileEntityRepository.findByUploadSessionId(uploadSessionId)
                .orElseThrow(() -> new FileEntityNotFoundException(
                        "Not found for upload session: " + uploadSessionId));

        return convertToResponse(fileEntity, version);
    }

    @Transactional(readOnly = true)
    public FileEntityResponse getByFilePath(String filePath, int version) {
        FileEntity fileEntity = fileEntityRepository.findByFilePath(filePath)
                .orElseThrow(() -> new FileEntityNotFoundException("Path: " + filePath));

        return convertToResponse(fileEntity, version);
    }

    @Transactional(readOnly = true)
    public List<FileEntity> getAllUserFiles(Long userId) {
        User user = userRepository.findById(userId).orElseThrow();
        List<FileEntity> allFiles = new ArrayList<>();

        if (user.getCompany() != null) {
            for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream()
                    .map(User::getId)
                    .toList()) {
                allFiles.addAll(fileEntityRepository.findByUserIdAndStatus(uId, FileStatus.ACTIVE));
            }
        } else {
            allFiles = fileEntityRepository.findByUserIdAndStatus(userId, FileStatus.ACTIVE);
        }

        return allFiles;
    }

    @Transactional(readOnly = true)
    public FileResponse getUserFiles(Long userId, int page, int pageSize, int version) {
        User user = userRepository.findById(userId).orElseThrow();
        List<FileEntity> allFiles = new ArrayList<>();

        if (user.getCompany() != null) {
            for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream()
                    .map(User::getId)
                    .toList()) {
                allFiles.addAll(fileEntityRepository.findByUserIdAndStatus(uId, FileStatus.ACTIVE));
            }

        } else {
            allFiles = fileEntityRepository.findByUserIdAndStatus(
                    userId, FileStatus.ACTIVE);
        }

        int start = (page - 1) * pageSize;
        int end = Math.min(start + pageSize, allFiles.size());

        if (start >= allFiles.size()) {
            return FileResponse.builder()
                    .files(List.of())
                    .totalCount(allFiles.size())
                    .totalSize(0)
                    .formattedTotalSize("0 B")
                    .page(page)
                    .pageSize(pageSize)
                    .build();
        }

        List<FileEntity> pageFiles = allFiles.subList(start, end);

        List<FileEntityResponse> files = pageFiles.stream()
                .map(file -> convertToResponse(file, version))
                .collect(Collectors.toList());

        long totalSize = allFiles.stream()
                .mapToLong(FileEntity::getFileSize)
                .sum();

        return FileResponse.builder()
                .files(files)
                .totalCount(allFiles.size())
                .totalSize(totalSize)
                .formattedTotalSize(formatFileSize(totalSize))
                .page(page)
                .pageSize(pageSize)
                .build();
    }

    @Transactional
    public FileEntity markAsDeleted(FileEntity fileEntity) throws IOException {
        fileEntity.setStatus(FileStatus.DELETED);
        fileEntity.setUpdatedAt(LocalDateTime.now());
        fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED);

        Path path = Paths.get(fileEntity.getProtectedFilePath());
        Files.deleteIfExists(path);
        fileEntity.setProtectedFilePath("");

        return fileEntityRepository.save(fileEntity);
    }

    public void softDeleteFileWithHash(FileEntity fileEntity) throws IOException {
        if (fileEntity.getImageHash() != null) {
            fileEntity.setImageHash(null);
        }

        fileEntity.setStatus(FileStatus.DELETED);
        fileEntity.setUpdatedAt(LocalDateTime.now());
        fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED);

        Path path = Paths.get(fileEntity.getProtectedFilePath());
        Files.deleteIfExists(path);
        fileEntity.setProtectedFilePath("");

        fileEntityRepository.save(fileEntity);
    }

    public void deleteFromDisk(FileEntity fileEntity) throws IOException {
        Path path = Paths.get(fileEntity.getFilePath());

        if (!Files.exists(path)) {
            return;
        }

        Files.delete(path);

        fileEntityRepository.delete(fileEntity);
    }

    @Transactional(readOnly = true)
    public long getUserStorageUsed(Long userId) {
        Long totalSize = fileEntityRepository.getTotalSizeByUserId(userId);
        return totalSize != null ? totalSize : 0L;
    }

    public void changeStatus(ProtectionStatus newStatus, String fileId) {
        FileEntity fileEntity = fileEntityRepository.findById(fileId)
                .orElseThrow(() -> new FileEntityNotFoundException(fileId));
        fileEntity.setProtectionStatus(newStatus);

        fileEntityRepository.save(fileEntity);
    }

    @Transactional
    public void writeProtectedFile(String id, byte[] data, String fileExt) throws IOException {
        FileEntity fileEntity = fileEntityRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("File not found: " + id));

        String extension = determineFileExtension(fileExt, fileEntity);
        Path protectedFilePath = prepareProtectedPath(fileEntity, extension);

        if (Files.exists(protectedFilePath)) {
            Files.delete(protectedFilePath);
        }

        Files.write(protectedFilePath, data);

        if (imageResizeService.isImage(extension)) {
            imageResizeService.generateSizes(fileEntity, data);
        }

        fileEntity.setProtectedFilePath(protectedFilePath.toString());
        fileEntity.setProtectedAt(LocalDateTime.now());
        fileEntity.setUpdatedAt(LocalDateTime.now());
        fileEntity.setFileExtension(extension);
        fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED);

        fileEntityRepository.save(fileEntity);
    }

    public void clearTempFiles(long userId) throws IOException {
        List<FileEntity> fileByUserIdAndStatus = fileEntityRepository.findFileByUserIdAndStatus(userId, FileStatus.TEMP);

        for (FileEntity fileEntity : fileByUserIdAndStatus) {
            deleteFromDisk(fileEntity);
        }
    }

    public FileEntity findBySignature(String signature) {
        return fileEntityRepository.findBySignature(signature);
    }

    public String findFileIdByPath(String filePath) {
        return fileEntityRepository.findFileIdByFilePath(filePath);
    }

    public File getFileById(String id) {
        try {
            FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
                    new RuntimeException("File not found: " + id));

            File file = new File(fileEntity.getFilePath());

            if (!file.exists()) {
                throw new RuntimeException("File not found on disk: " + fileEntity.getFilePath());
            }

            return file;
        } catch (Exception e) {
            log.error("Error getting file: {}", id, e);
            return null;
        }
    }

    public Path prepareProtectedPath(FileEntity fileEntity, String extension) throws IOException {
        Path originalPath = Paths.get(fileEntity.getFilePath());

        String pathStr = originalPath.toString();
        pathStr = pathStr.replaceFirst("/uploads/uploads/", "/uploads/protected/");

        Path protectedPath = Paths.get(pathStr);
        String fileName = protectedPath.getFileName().toString();

        if (extension != null) {
            String nameWithoutExt = fileName;
            int lastDotIndex = fileName.lastIndexOf('.');
            if (lastDotIndex > 0) {
                nameWithoutExt = fileName.substring(0, lastDotIndex);
            }
            fileName = nameWithoutExt + "." + extension;

            protectedPath = protectedPath.getParent().resolve(fileName);
        }

        Files.createDirectories(protectedPath.getParent());

        return protectedPath;
    }

    public void updateSignature(String signature, String fileId) throws IOException {
        FileEntity fileEntity = fileEntityRepository.findByFileId(fileId);
        fileEntity.setSignature(signature);
        fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED);
        fileEntity.setUpdatedAt(LocalDateTime.now());
        fileEntity.setProtectedAt(LocalDateTime.now());
        fileEntity.setProtectedFilePath(prepareProtectedPath(fileEntity, fileEntity.getFileExtension()).toString());

        fileEntityRepository.save(fileEntity);
    }

    private boolean checkFileExistsOnDisk(String filePath) {
        try {
            return Files.exists(Paths.get(filePath));
        } catch (Exception e) {
            log.warn("Error checking file existence: {}", filePath, e);
            return false;
        }
    }

    private String determineFileExtension(String fileExt, FileEntity fileEntity) {
        if (fileExt != null) {
            return fileExt;
        } else {
            return fileEntity.getFileExtension();
        }
    }

    private final FileMonitoringRepository fileMonitoringRepository;

    private FileEntityResponse convertToResponse(FileEntity fileEntity, int version) {
        boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath());
        User user = userRepository.findById(fileEntity.getUserId()).get();
        String fileName = fileEntity.getOriginalFileName();
        String name = fileName;
        Optional<FileMonitoringEntity> fileMonitoringRepositoryByFileId =
                fileMonitoringRepository.findByFileId(fileEntity.getId());

        String monitoring = fileMonitoringRepositoryByFileId.map(fileMonitoringEntity ->
                fileMonitoringEntity.getMonitoringType().name())
                .orElseGet(MonitoringType.NONE::name);

        int lastDotIndex = fileName.lastIndexOf(".");
        if (lastDotIndex > 0) {
            name = fileName.substring(0, lastDotIndex);
        }

        return FileEntityResponse.builder()
                .id(fileEntity.getId())
                .userId(fileEntity.getUserId())
                .originalFileName(name + "." + fileEntity.getFileExtension())
                .storedFileName(fileEntity.getStoredFileName())
                .filePath(fileEntity.getProtectedFilePath())
                .fileSize(fileEntity.getFileSize())
                .mimeType(fileEntity.getMimeType())
                .fileExtension(fileEntity.getFileExtension())
                .checksum(fileEntity.getChecksum())
                .uploadSessionId(fileEntity.getUploadSessionId())
                .status(fileEntity.getStatus())
                .createdAt(fileEntity.getCreatedAt())
                .updatedAt(fileEntity.getUpdatedAt())
                .formattedSize(formatFileSize(fileEntity.getFileSize()))
                .downloadUrl("/api/v" + version + "/files/download/" + fileEntity.getId())
                .existsOnDisk(existsOnDisk)
                .supportId(fileEntity.getSupportId())
                .protectStatus(fileEntity.getProtectionStatus().toString())
                .fileName(name + "_nocopy_protected" + "." + fileEntity.getFileExtension())
                .ownerName(user.getFullName())
                .ownerEmail(user.getEmail())
                .ownerCompany(user.getCompanyName())
                .monitoring(monitoring)
                //TODO fix after add logic for check for protect file
                .checksCount(0)
                .fileUploadDate(fileEntity.getCreatedAt())
                .protectedFilePath(fileEntity.getProtectedFilePath())
                .thumbnailFileUrl(baseUrl + "/api/files/protected/" + fileEntity.getId() + "/thumbnail")
                .build();
    }

    public String formatFileSize(long size) {
        if (size < 1024) {
            return size + " B";
        } else if (size < 1024 * 1024) {
            return String.format("%.1f KB", size / 1024.0);
        } else if (size < 1024 * 1024 * 1024) {
            return String.format("%.1f MB", size / (1024.0 * 1024.0));
        } else {
            return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
        }
    }
}