package ru.soune.nocopy.service.file;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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.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.FileEntityNotFoundException;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.FileMonitoringRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class FileEntityService {

    private final FileEntityRepository fileEntityRepository;

    private final UserRepository userRepository;

    private final ImageResizeService imageResizeService;

    private final CloudStorageService cloudStorageService;

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

    @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(getAllUserFiles(uId, List.of(FileStatus.ACTIVE, FileStatus.BLOCKED,
                        FileStatus.MODERATION)));
            }
        } else {
            allFiles = getAllUserFiles(userId, List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION));
        }

        return allFiles;
    }

    public List<FileEntity> getAllUserFiles(Long userId, List<FileStatus> statuses) {
        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.findByUserIdAndStatusIn(uId, statuses));
            }
        } else {
            allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId, statuses);
        }

        return allFiles;
    }

    public List<FileEntity> getAllUserFilesWithFileName(Long userId, String fileName, List<FileStatus> statuses) {
        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.searchByFileNameAndStatusIn(uId, fileName, statuses));
            }
        } else {
            allFiles = fileEntityRepository.searchByFileNameAndStatusIn(userId, fileName, statuses);
        }

        return allFiles;
    }

    @Transactional(readOnly = true)
    public FileResponse getUserFiles(Long userId, Pageable pageable, int version) {
        User user = userRepository.findById(userId).orElseThrow();
        List<User> users = user.getCompany() != null ?
                userRepository.findByCompanyId(user.getCompany().getId()) : List.of(user);
        List<FileStatus> statusesForSearch = List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION);

        Page<FileEntity> files = fileEntityRepository.findFiles(users.stream().map(User::getId).toList(),
                statusesForSearch, pageable);

        Long totalSize = fileEntityRepository.sumFileSize(
                users.stream().map(User::getId).toList(),
                statusesForSearch);

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

        return FileResponse.builder()
                .files(filesToResponse)
                .totalCount((int) files.getTotalElements())
                .totalSize(totalSize != null ? totalSize : 0L)
                .formattedTotalSize(formatFileSize(totalSize != null ? totalSize : 0L))
                .page(pageable.getPageNumber())
                .pageSize(pageable.getPageSize())
                .build();
    }

    @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.findByUserIdAndStatusIn(uId,
                        List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION)));
            }

        } else {
            allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId,
                    List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION));
        }

        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();
    }

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

        fileEntity.setStatus(FileStatus.REMOVED);
        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(String deleteFilePath) throws IOException {
        Files.deleteIfExists(Paths.get(deleteFilePath));
    }

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

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

        fileEntityRepository.save(fileEntity);
    }

    public void changeFileStatus(FileStatus newStatus, String fileId) {
        FileEntity fileEntity = fileEntityRepository.findById(fileId)
                .orElseThrow(() -> new FileEntityNotFoundException(fileId));
        fileEntity.setStatus(newStatus);

        fileEntityRepository.save(fileEntity);
    }

    @Transactional
    public FileEntity 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);

        Files.createDirectories(protectedFilePath.getParent());

        Files.deleteIfExists(protectedFilePath);

        try (FileChannel channel = FileChannel.open(protectedFilePath,
                StandardOpenOption.CREATE,
                StandardOpenOption.WRITE)) {

            ByteBuffer buffer = ByteBuffer.wrap(data);
            while (buffer.hasRemaining()) {
                channel.write(buffer);
            }
            channel.force(true);
        }

        if (fileEntity.getMimeType().equals("image")) {
            imageResizeService.generateSizes(fileEntity, data);
        }

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

        return fileEntityRepository.save(fileEntity);
    }

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

        for (FileEntity fileEntity : fileByUserIdAndStatus) {
            cloudStorageService.deleteFromStorageDisk(fileEntity);
        }
    }

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

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

    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 FileEntity 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());

        return fileEntityRepository.save(fileEntity);
    }

    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));
        }
    }

//    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")
                .permissions(fileEntity.getPermissions())
                .build();
    }
}