package de.votian.services.service;

import de.votian.services.dto.JobAttachmentDto;
import de.votian.services.dto.JobPhotoDto;
import de.votian.services.entity.GenericDataContainer;
import de.votian.services.entity.Job;
import de.votian.services.entity.Tour;
import de.votian.services.repository.GenericDataContainerRepository;
import de.votian.services.repository.JobRepository;
import de.votian.services.repository.TourRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.FileTime;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

@Service
@Transactional
public class JobMediaService {

    private static final DateTimeFormatter MODIFIED_AT_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    private final JobRepository jobRepository;
    private final TourRepository tourRepository;
    private final GenericDataContainerRepository genericDataContainerRepository;
    private final Path jobDocumentsDir;
    private final Path mobilePhotosDir;
    private final Path stationPhotosDir;

    public JobMediaService(JobRepository jobRepository,
                           TourRepository tourRepository,
                           GenericDataContainerRepository genericDataContainerRepository,
                           @Value("${votian.storage.job-documents-dir:../html/import/upload/a547bc4b2a91355a3aba3ed0827cf6f2}") String jobDocumentsDir,
                           @Value("${votian.storage.job-mobile-photos-dir:../html/documents/photos}") String mobilePhotosDir,
                           @Value("${votian.storage.job-station-photos-dir:../html/temp/photos}") String stationPhotosDir) {
        this.jobRepository = jobRepository;
        this.tourRepository = tourRepository;
        this.genericDataContainerRepository = genericDataContainerRepository;
        this.jobDocumentsDir = Path.of(jobDocumentsDir).toAbsolutePath().normalize();
        this.mobilePhotosDir = Path.of(mobilePhotosDir).toAbsolutePath().normalize();
        this.stationPhotosDir = Path.of(stationPhotosDir).toAbsolutePath().normalize();
    }

    @Transactional(readOnly = true)
    public List<JobAttachmentDto> getAttachments(Long jobId) {
        requireJob(jobId);

        List<JobAttachmentDto> attachments = new ArrayList<>();
        Set<String> seenFileNames = new LinkedHashSet<>();

        for (GenericDataContainer entry : genericDataContainerRepository.findByTypeAndId("jb", jobId)) {
            if (!"file_upload".equals(entry.getFieldName())) {
                continue;
            }

            String fileName = sanitizeFileName(entry.getContent());
            Path filePath = resolveDocumentPath(fileName);
            JobAttachmentDto dto = new JobAttachmentDto();
            dto.setId(entry.getId());
            dto.setFileName(fileName);
            dto.setRemark(extractRemark(entry.getContext()));
            dto.setSource("Upload");
            dto.setDownloadable(Files.isRegularFile(filePath));
            dto.setDeletable(true);
            fillFileMetadata(dto, filePath);
            attachments.add(dto);
            seenFileNames.add(fileName);
        }

        if (Files.isDirectory(jobDocumentsDir)) {
            try (var stream = Files.walk(jobDocumentsDir, 5)) {
                stream.filter(Files::isRegularFile)
                        .filter(path -> path.getFileName().toString().startsWith(jobId + "_"))
                        .sorted(Comparator.comparing(Path::toString))
                        .forEach(path -> {
                            String fileName = path.getFileName().toString();
                            if (seenFileNames.add(fileName)) {
                                JobAttachmentDto dto = new JobAttachmentDto();
                                dto.setFileName(fileName);
                                dto.setRemark("");
                                dto.setSource("Datei");
                                dto.setDownloadable(true);
                                dto.setDeletable(false);
                                fillFileMetadata(dto, path);
                                attachments.add(dto);
                            }
                        });
            } catch (IOException e) {
                throw new RuntimeException("Attachments could not be listed", e);
            }
        }

        attachments.sort(Comparator
                .comparing(JobAttachmentDto::getModifiedAt, Comparator.nullsLast(String::compareTo))
                .reversed()
                .thenComparing(JobAttachmentDto::getFileName, Comparator.nullsLast(String::compareToIgnoreCase)));
        return attachments;
    }

    public JobAttachmentDto saveAttachment(Long jobId, MultipartFile file, String remark) {
        requireJob(jobId);
        if (file == null || file.isEmpty()) {
            throw new RuntimeException("No file uploaded");
        }

        try {
            Files.createDirectories(jobDocumentsDir);
            String sanitizedFileName = sanitizeFileName(Optional.ofNullable(file.getOriginalFilename()).orElse("upload.bin"));
            String storedFileName = jobId + "_" + System.currentTimeMillis() + "_" + sanitizedFileName;
            Path target = resolveDocumentPath(storedFileName);
            Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);

            GenericDataContainer entry = new GenericDataContainer();
            entry.setObjectType("jb");
            entry.setObjectId(jobId);
            entry.setFieldName("file_upload");
            entry.setContent(storedFileName);
            entry.setContext(remark != null ? remark.trim() : "");
            GenericDataContainer savedEntry = genericDataContainerRepository.save(entry);

            updateDocumentCounter(jobId);

            JobAttachmentDto dto = new JobAttachmentDto();
            dto.setId(savedEntry.getId());
            dto.setFileName(storedFileName);
            dto.setRemark(entry.getContext());
            dto.setSource("Upload");
            dto.setDownloadable(true);
            dto.setDeletable(true);
            fillFileMetadata(dto, target);
            return dto;
        } catch (IOException e) {
            throw new RuntimeException("Attachment could not be stored", e);
        }
    }

    public void deleteAttachment(Long jobId, Long attachmentId) {
        requireJob(jobId);
        GenericDataContainer entry = genericDataContainerRepository.findById(Objects.requireNonNull(attachmentId))
                .orElseThrow(() -> new RuntimeException("Attachment not found: " + attachmentId));

        if (!"jb".equals(entry.getObjectType()) || !jobId.equals(entry.getObjectId()) || !"file_upload".equals(entry.getFieldName())) {
            throw new RuntimeException("Attachment does not belong to job: " + jobId);
        }

        Path filePath = resolveDocumentPath(entry.getContent());
        try {
            Files.deleteIfExists(filePath);
        } catch (IOException e) {
            throw new RuntimeException("Attachment file could not be deleted", e);
        }

        genericDataContainerRepository.delete(entry);
        updateDocumentCounter(jobId);
    }

    @Transactional(readOnly = true)
    public byte[] loadAttachment(Long jobId, String fileName) {
        requireJob(jobId);
        String sanitizedFileName = sanitizeFileName(fileName);

        if (!sanitizedFileName.startsWith(jobId + "_")) {
            throw new RuntimeException("Invalid attachment access");
        }

        Path filePath = resolveDocumentPath(sanitizedFileName);
        if (!Files.isRegularFile(filePath)) {
            throw new RuntimeException("Attachment not found: " + sanitizedFileName);
        }

        try {
            return Files.readAllBytes(filePath);
        } catch (IOException e) {
            throw new RuntimeException("Attachment could not be read", e);
        }
    }

    @Transactional(readOnly = true)
    public List<JobPhotoDto> getPhotos(Long jobId) {
        requireJob(jobId);

        Map<String, JobPhotoDto> photos = new LinkedHashMap<>();

        genericDataContainerRepository.findByTypeAndIdAndField("jb", jobId, "mobile_photos")
                .ifPresent(entry -> {
                    for (String fileName : splitFileList(entry.getContent())) {
                        JobPhotoDto dto = new JobPhotoDto();
                        dto.setFileName(fileName);
                        dto.setSource("Mobile");
                        dto.setDownloadable(Files.isRegularFile(resolveMobilePhotoPath(fileName)));
                        photos.putIfAbsent(fileName, dto);
                    }
                });

        List<Tour> tours = tourRepository.findByJobIdOrderBySort(jobId);
        if (Files.isDirectory(stationPhotosDir)) {
            for (Tour tour : tours) {
                if (tour.getId() == null) {
                    continue;
                }
                try (var stream = Files.list(stationPhotosDir)) {
                    stream.filter(Files::isRegularFile)
                            .filter(path -> belongsToTourPhoto(path.getFileName().toString(), tour.getId()))
                            .sorted(Comparator.comparing(path -> path.getFileName().toString()))
                            .forEach(path -> {
                                String fileName = path.getFileName().toString();
                                JobPhotoDto dto = new JobPhotoDto();
                                dto.setFileName(fileName);
                                dto.setSource("Station");
                                dto.setDownloadable(true);
                                photos.putIfAbsent(fileName, dto);
                            });
                } catch (IOException e) {
                    throw new RuntimeException("Station photos could not be listed", e);
                }
            }
        }

        return photos.values().stream()
                .sorted(Comparator.comparing(JobPhotoDto::getSource).thenComparing(JobPhotoDto::getFileName))
                .toList();
    }

    @Transactional(readOnly = true)
    public byte[] loadPhoto(Long jobId, String fileName) {
        requireJob(jobId);
        String sanitizedFileName = sanitizeFileName(fileName);

        Path mobilePath = resolveMobilePhotoPath(sanitizedFileName);
        if (Files.isRegularFile(mobilePath) && containsPhotoName(jobId, sanitizedFileName)) {
            try {
                return Files.readAllBytes(mobilePath);
            } catch (IOException e) {
                throw new RuntimeException("Mobile photo could not be read", e);
            }
        }

        Path stationPath = resolveStationPhotoPath(sanitizedFileName);
        if (Files.isRegularFile(stationPath) && belongsToJobTour(jobId, sanitizedFileName)) {
            try {
                return Files.readAllBytes(stationPath);
            } catch (IOException e) {
                throw new RuntimeException("Station photo could not be read", e);
            }
        }

        throw new RuntimeException("Photo not found: " + sanitizedFileName);
    }

    private Job requireJob(Long jobId) {
        return jobRepository.findById(Objects.requireNonNull(jobId))
                .orElseThrow(() -> new RuntimeException("Job not found: " + jobId));
    }

    private void updateDocumentCounter(Long jobId) {
        long documentCount = getAttachments(jobId).stream()
                .filter(JobAttachmentDto::isDownloadable)
                .count();

        GenericDataContainer counter = genericDataContainerRepository.findByTypeAndIdAndField("jb", jobId, "num_of_documents")
                .orElseGet(() -> {
                    GenericDataContainer entry = new GenericDataContainer();
                    entry.setObjectType("jb");
                    entry.setObjectId(jobId);
                    entry.setFieldName("num_of_documents");
                    return entry;
                });
        counter.setContent(String.valueOf(documentCount));
        counter.setContext("");
        genericDataContainerRepository.save(counter);
    }

    private void fillFileMetadata(JobAttachmentDto dto, Path filePath) {
        if (!Files.isRegularFile(filePath)) {
            return;
        }
        try {
            dto.setSizeBytes(Files.size(filePath));
            FileTime lastModifiedTime = Files.getLastModifiedTime(filePath);
            LocalDateTime modifiedAt = LocalDateTime.ofInstant(lastModifiedTime.toInstant(), ZoneId.systemDefault());
            dto.setModifiedAt(MODIFIED_AT_FORMAT.format(modifiedAt));
        } catch (IOException ignored) {
            // Leave metadata empty if the file system access fails.
        }
    }

    private String extractRemark(String context) {
        if (context == null || context.isBlank()) {
            return "";
        }
        int separator = context.indexOf('|');
        return separator >= 0 ? context.substring(0, separator) : context;
    }

    private List<String> splitFileList(String content) {
        if (content == null || content.isBlank()) {
            return List.of();
        }
        List<String> names = new ArrayList<>();
        for (String candidate : content.split("\\|")) {
            String sanitized = sanitizeFileName(candidate);
            if (!sanitized.isBlank()) {
                names.add(sanitized);
            }
        }
        return names;
    }

    private boolean belongsToTourPhoto(String fileName, Long tourId) {
        return fileName != null &&
                (fileName.equals("pic_tr_photo_" + tourId + ".jpg")
                        || fileName.equals("pht_pic_tr_photo_" + tourId + ".jpg")
                        || fileName.equals("pic_tr_photo_" + tourId + ".png")
                        || fileName.equals("pht_pic_tr_photo_" + tourId + ".png")
                        || fileName.equals("pic_tr_photo_" + tourId + ".jpeg")
                        || fileName.equals("pht_pic_tr_photo_" + tourId + ".jpeg"));
    }

    private boolean belongsToJobTour(Long jobId, String fileName) {
        for (Tour tour : tourRepository.findByJobIdOrderBySort(jobId)) {
            if (tour.getId() != null && belongsToTourPhoto(fileName, tour.getId())) {
                return true;
            }
        }
        return false;
    }

    private boolean containsPhotoName(Long jobId, String fileName) {
        return genericDataContainerRepository.findByTypeAndIdAndField("jb", jobId, "mobile_photos")
                .map(GenericDataContainer::getContent)
                .map(this::splitFileList)
                .orElse(List.of())
                .contains(fileName);
    }

    private Path resolveDocumentPath(String fileName) {
        return jobDocumentsDir.resolve(sanitizeFileName(fileName)).normalize();
    }

    private Path resolveMobilePhotoPath(String fileName) {
        return mobilePhotosDir.resolve(sanitizeFileName(fileName)).normalize();
    }

    private Path resolveStationPhotoPath(String fileName) {
        return stationPhotosDir.resolve(sanitizeFileName(fileName)).normalize();
    }

    private String sanitizeFileName(String fileName) {
        if (fileName == null) {
            return "";
        }
        String sanitized = Path.of(fileName).getFileName().toString();
        return sanitized.replaceAll("[^A-Za-z0-9._-]", "_");
    }
}
