package at.procon.dip.search.service;

import at.procon.dip.search.api.SearchExecutionContext;
import at.procon.dip.search.api.SearchExecutionPlan;
import at.procon.dip.search.dto.SearchDebugResponse;
import at.procon.dip.search.dto.SearchEngineDebugResult;
import at.procon.dip.search.dto.SearchEngineType;
import at.procon.dip.search.dto.SearchHit;
import at.procon.dip.search.dto.SearchRequest;
import at.procon.dip.search.dto.SearchResponse;
import at.procon.dip.search.engine.SearchEngine;
import at.procon.dip.search.plan.SearchPlanner;
import at.procon.dip.search.rank.SearchResultFusionService;
import at.procon.dip.search.spi.SearchDocumentScope;
import at.procon.dip.search.config.DipSearchProperties;
import at.procon.dip.runtime.condition.ConditionalOnRuntimeMode;
import at.procon.dip.runtime.config.RuntimeMode;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@ConditionalOnRuntimeMode(RuntimeMode.NEW)
@RequiredArgsConstructor
public class DefaultSearchOrchestrator implements SearchOrchestrator {

    private final DipSearchProperties properties;
    private final SearchPlanner planner;
    private final List<SearchEngine> engines;
    private final SearchResultFusionService fusionService;
    private final SearchMetricsService metricsService;

    @Override
    public SearchResponse search(SearchRequest request, SearchDocumentScope scope) {
        SearchExecution execution = executeInternal(request, scope);
        SearchResponse response = fusionService.fuse(execution.context(), execution.plan(), execution.engineResults());
        metricsService.recordSearch(execution.engineResults(), response.getHits().size(), false);
        return response;
    }

    @Override
    public SearchDebugResponse debug(SearchRequest request, SearchDocumentScope scope) {
        SearchExecution execution = executeInternal(request, scope);
        SearchResponse fused = fusionService.fuse(execution.context(), execution.plan(), execution.engineResults());
        metricsService.recordSearch(execution.engineResults(), fused.getHits().size(), true);

        List<SearchEngineDebugResult> debugResults = new ArrayList<>();
        int topLimit = properties.getDebugTopHitsPerEngine();
        execution.engineResults().forEach((engine, hits) -> debugResults.add(SearchEngineDebugResult.builder()
                .engineType(engine)
                .hitCount(hits.size())
                .topHits(hits.stream().limit(topLimit).toList())
                .build()));

        return SearchDebugResponse.builder()
                .request(request)
                .plan(execution.plan())
                .engineResults(debugResults)
                .fusedResponse(fused)
                .build();
    }

    @Override
    public at.procon.dip.search.dto.SearchMetricsResponse metrics() {
        return metricsService.snapshot();
    }

    private SearchExecution executeInternal(SearchRequest request, SearchDocumentScope scope) {
        int page = request.getPage() == null || request.getPage() < 0 ? 0 : request.getPage();
        int requestedSize = request.getSize() == null || request.getSize() <= 0
                ? properties.getDefaultPageSize()
                : request.getSize();
        int size = Math.min(requestedSize, properties.getMaxPageSize());

        SearchExecutionContext context = SearchExecutionContext.builder()
                .request(request)
                .scope(scope)
                .page(page)
                .size(size)
                .build();

        SearchExecutionPlan plan = planner.plan(context);
        Map<SearchEngineType, List<SearchHit>> engineResults = new LinkedHashMap<>();
        for (SearchEngine engine : engines) {
            if (plan.getEngines().contains(engine.type()) && engine.supports(context)) {
                engineResults.put(engine.type(), engine.execute(context));
            }
        }
        return new SearchExecution(context, plan, engineResults);
    }

    private record SearchExecution(
            SearchExecutionContext context,
            SearchExecutionPlan plan,
            Map<SearchEngineType, List<SearchHit>> engineResults
    ) {
    }
}
