package ru.soune.nocopy.service.search;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.configuration.search.SearchProperties;
import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.search.GlobalSearchResult;
import ru.soune.nocopy.entity.search.GlobalSearchTask;
import ru.soune.nocopy.entity.search.SearchStatus;
import ru.soune.nocopy.entity.tokenoperation.OperationType;
import ru.soune.nocopy.exception.TaskCancelledException;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.GlobalSearchResultRepository;
import ru.soune.nocopy.repository.GlobalSearchTaskRepository;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.tariff.TariffConstants;
import ru.soune.nocopy.service.tariff.TariffInfoService;
import ru.soune.nocopy.service.violation.ViolationService;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;

@Slf4j
@Component
@RequiredArgsConstructor
public class GlobalSearchAsyncProcessor {

    private final GlobalSearchResultRepository globalSearchResultRepository;

    private final SearchImageService searchImageService;

    private final TariffInfoService tariffInfoService;

    private final GlobalSearchTaskRepository globalSearchTaskRepository;

    private final ViolationService violationService;

    private final SearchProperties searchProperties;

    private final NotificationService notificationService;

    private final FileEntityRepository fileEntityRepository;

//    @Async
//    public void processFilesAsync(String taskId, List<String> filesToProcess, Long userId) {
//        try {
//            for (int i = 0; i < filesToProcess.size(); i++) {
//                String uuid = filesToProcess.get(i);
//                log.info("UUID: " + uuid);
//                FileEntity file = fileEntityRepository.findById(uuid).orElse(null);
//
//                if (file == null) continue;
//
//                tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH,
//                        OperationType.GLOBAL_SEARCH);
//                GlobalSearchResult result = processFile(file, taskId, userId);
//                globalSearchResultRepository.save(result);
//
//                task.setProcessedFiles(i + 1);
//                task.setUpdatedAt(LocalDateTime.now());
//                globalSearchTaskRepository.save(task);
//            }
//
//            task.setStatus(SearchStatus.COMPLETED.name());
//            task.setUpdatedAt(LocalDateTime.now());
//            GlobalSearchTask save = globalSearchTaskRepository.save(task);
//
//            addSearchResultNotification(save, filesToProcess, userId);
//        } catch (Exception e) {
//            task.setStatus(SearchStatus.FAILED.name());
//            task.setUpdatedAt(LocalDateTime.now());
//            globalSearchTaskRepository.save(task);
//        }
//    }

    @Async
    public void processFilesAsync(String taskId, List<String> filesToProcess, Long userId) {
        try {
            for (int i = 0; i < filesToProcess.size(); i++) {
                processOneFile(taskId, filesToProcess.get(i), userId, i);
            }
            completeTask(taskId, filesToProcess, userId);
        } catch (TaskCancelledException e) {
            log.info("Task {} was cancelled", taskId);
        } catch (Exception e) {
            log.error("Task {} failed", taskId, e);
            failTask(taskId);
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processOneFile(String taskId, String fileId, Long userId, int index) throws MessagingException, IOException {
        GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow();

        if (SearchStatus.FAILED.name().equals(task.getStatus())) {
            throw new TaskCancelledException("Task " + taskId + " was cancelled");
        }

        FileEntity file = fileEntityRepository.findById(fileId).orElse(null);
        if (file == null) {
            log.warn("File not found: {}", fileId);
            return;
        }

        tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH, OperationType.GLOBAL_SEARCH);

        GlobalSearchResult result = processFile(file, taskId, userId);
        globalSearchResultRepository.save(result);

        task.setProcessedFiles(index + 1);
        task.setUpdatedAt(LocalDateTime.now());
        globalSearchTaskRepository.save(task);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void completeTask(String taskId, List<String> filesToProcess, Long userId) {
        GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow();
        task.setStatus(SearchStatus.COMPLETED.name());
        task.setUpdatedAt(LocalDateTime.now());

        GlobalSearchTask saved = globalSearchTaskRepository.save(task);

        addSearchResultNotification(saved, filesToProcess, userId);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void failTask(String taskId) {
        try {
            GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow();
            task.setStatus(SearchStatus.FAILED.name());
            task.setUpdatedAt(LocalDateTime.now());

            globalSearchTaskRepository.save(task);
        } catch (Exception ex) {
            log.error("Could not mark task {} as failed", taskId, ex);
        }
    }

    private void addSearchResultNotification(GlobalSearchTask globalSearchTask, List<String> files, Long userId) {
        ObjectMapper mapper = new ObjectMapper();
        ObjectNode context = mapper.createObjectNode();

        context.put("start_date",
                globalSearchTask.getCreatedAt().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")));
        context.put("end_date",
                LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")));
        context.put("search_type", globalSearchTask.getSearchType());
        context.put("files_id", files.toString());
        context.put("count", files.toString());

        notificationService.addNotification(NotificationType.SEARCH_RESULT, userId, context.toString());
    }

    private GlobalSearchResult processFile(FileEntity file, String taskId, Long userId) {
        GlobalSearchResult result = new GlobalSearchResult();
        result.setTaskId(taskId);
        result.setFileId(file.getId());
        result.setFileName(file.getOriginalFileName());

        result = globalSearchResultRepository.save(result);

        boolean useGoogle = searchProperties.getEngines().getOrDefault("google",
                new SearchProperties.EngineConfig()).isEnabled();
        boolean useYandex = searchProperties.getEngines().getOrDefault("yandex",
                new SearchProperties.EngineConfig()).isEnabled();

        log.info("Global search settings for file {}: useGoogle={}, useYandex={}",
                file.getId(), useGoogle, useYandex);

        List<YandexSearchResponse.ImageResult> googleImages = new ArrayList<>();
        List<YandexSearchResponse.ImageResult> yandexImages = new ArrayList<>();
        List<YandexSearchResponse.ImageResult> allResults = new ArrayList<>();
        boolean hasTimeout = false;

        if (useGoogle) {
            try {
                String searchResponseGoogle = searchImageService.searchReverseByPublicUrl(
                        file, "google_lens", "exact_matches");

                googleImages = searchImageService.getAllImagesWithoutPagination(searchResponseGoogle,
                        "exact_matches");

                allResults.addAll(googleImages);
                log.info("Google search OK for file {}, found {} results", file.getId(), googleImages.size());

            } catch (TimeoutException e) {
                log.warn("Google search timeout for file {}", file.getId());
                hasTimeout = true;
            } catch (IOException e) {
                log.error("Google search failed for file {}", file.getId(), e);
            }
        } else {
            log.info("Google search is disabled by settings for file {}", file.getId());
        }

        if (useYandex) {
            try {
                String searchResponseYandex = searchImageService.searchReverseByPublicUrl(
                        file, "yandex_reverse_image", "visual_matches");

                yandexImages = searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches");

                allResults.addAll(yandexImages);
                log.info("Yandex search OK for file {}, found {} results", file.getId(), yandexImages.size());

            } catch (TimeoutException e) {
                log.warn("Yandex search timeout for file {}", file.getId());
                hasTimeout = true;
            } catch (IOException e) {
                log.error("Yandex search failed for file {}", file.getId(), e);
            }
        } else {
            log.info("Yandex search is disabled by settings for file {}", file.getId());
        }

        List<YandexSearchResponse.ImageResult> allUniqueImages;
        if (useGoogle && useYandex) {
            allUniqueImages = searchImageService.removeDuplicateUrls(
                    yandexImages.stream()
                            .filter(img -> img.getUrl() != null)
                            .collect(Collectors.toList()),
                    googleImages.stream()
                            .filter(img -> img.getUrl() != null)
                            .collect(Collectors.toList())
            );
            log.info("Merged results from both engines: {} unique images", allUniqueImages.size());
        } else if (useGoogle) {
            allUniqueImages = googleImages.stream()
                    .filter(img -> img.getUrl() != null)
                    .collect(Collectors.toList());
            log.info("Results only from Google: {} images", allUniqueImages.size());
        } else if (useYandex) {
            allUniqueImages = yandexImages.stream()
                    .filter(img -> img.getUrl() != null)
                    .collect(Collectors.toList());
            log.info("Results only from Yandex: {} images", allUniqueImages.size());
        } else {
            allUniqueImages = new ArrayList<>();
            log.warn("All search engines are disabled for file {}", file.getId());
        }

        for (YandexSearchResponse.ImageResult imageResult : allUniqueImages) {
            violationService.processViolation(imageResult, file, result);
        }

        if (allUniqueImages.isEmpty() && hasTimeout) {
            result.setFileStatus(SearchStatus.TIMEOUT.name());
        } else if (allUniqueImages.isEmpty() && (!useGoogle && !useYandex)) {
            result.setFileStatus(SearchStatus.FAILED.name());
            log.warn("No results for file {} because all engines are disabled", file.getId());
        } else {
            result.setFileStatus(SearchStatus.SUCCESS.name());
        }

        return result;
    }
}
