package com.arms.egovframework.javaservice.esframework.custom.client;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.search.TotalHits;
import org.opensearch.action.search.SearchResponse;
import org.opensearch.action.search.ShardSearchFailure;
import org.opensearch.core.common.text.Text;
import org.opensearch.data.client.orhlc.OpenSearchAggregations;
import org.opensearch.search.SearchHit;
import org.opensearch.search.SearchHits;
import org.opensearch.search.aggregations.Aggregations;
import org.springframework.data.elasticsearch.core.SearchShardStatistics;
import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.suggest.response.CompletionSuggestion;
import org.springframework.data.elasticsearch.core.suggest.response.PhraseSuggestion;
import org.springframework.data.elasticsearch.core.suggest.response.SortBy;
import org.springframework.data.elasticsearch.core.suggest.response.Suggest;
import org.springframework.data.elasticsearch.core.suggest.response.TermSuggestion;
import org.springframework.data.elasticsearch.ElasticsearchErrorCause;
import org.springframework.data.elasticsearch.support.ScoreDoc;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
 * Custom SearchDocumentResponseBuilder - CustomDocumentAdapters 사용
 * 원본: org.opensearch.data.client.orhlc.SearchDocumentResponseBuilder
 *
 * 변경사항:
 * - DocumentAdapters.from(searchHit) 대신 CustomDocumentAdapters.from(searchHit) 사용
 */
public class CustomSearchDocumentResponseBuilder {

    private static final Log LOGGER = LogFactory.getLog(SearchDocumentResponse.class);

    public static <T> SearchDocumentResponse from(
            SearchResponse searchResponse, SearchDocumentResponse.EntityCreator<T> entityCreator) {

        Assert.notNull(searchResponse, "searchResponse must not be null");

        SearchHits searchHits = searchResponse.getHits();
        String scrollId = searchResponse.getScrollId();
        Aggregations aggregations = searchResponse.getAggregations();
        org.opensearch.search.suggest.Suggest suggest = searchResponse.getSuggest();
        SearchShardStatistics shardStatistics = shardsFrom(searchResponse);
        var executionDurationInMillis = searchResponse.getTook().millis();

        return from(searchHits, shardStatistics, scrollId, executionDurationInMillis,
            aggregations, suggest, entityCreator);
    }

    public static <T> SearchDocumentResponse from(
            SearchHits searchHits,
            @Nullable SearchShardStatistics shardStatistics,
            @Nullable String scrollId,
            long executionDurationInMillis,
            @Nullable Aggregations aggregations,
            @Nullable org.opensearch.search.suggest.Suggest suggestOS,
            SearchDocumentResponse.EntityCreator<T> entityCreator) {

        TotalHits responseTotalHits = searchHits.getTotalHits();

        long totalHits;
        String totalHitsRelation;

        if (responseTotalHits != null) {
            totalHits = responseTotalHits.value;
            totalHitsRelation = responseTotalHits.relation.name();
        } else {
            totalHits = searchHits.getHits().length;
            totalHitsRelation = "OFF";
        }

        float maxScore = searchHits.getMaxScore();
        final Duration executionDuration = Duration.ofMillis(executionDurationInMillis);

        List<SearchDocument> searchDocuments = new ArrayList<>();
        for (SearchHit searchHit : searchHits) {
            if (searchHit != null) {
                // CustomDocumentAdapters 사용 - 배열 필드 처리 지원
                searchDocuments.add(CustomDocumentAdapters.from(searchHit));
            }
        }

        OpenSearchAggregations aggregationsContainer =
                aggregations != null ? new OpenSearchAggregations(aggregations) : null;
        Suggest suggest = suggestFrom(suggestOS, entityCreator);

        return new SearchDocumentResponse(
                totalHits,
                totalHitsRelation,
                maxScore,
                executionDuration,
                scrollId,
                null,
                searchDocuments,
                aggregationsContainer,
                suggest,
                shardStatistics);
    }

    private static SearchShardStatistics shardsFrom(SearchResponse response) {
        ShardSearchFailure[] failures = response.getShardFailures();
        if (failures == null) {
            return SearchShardStatistics.of(response.getFailedShards(), response.getSuccessfulShards(), response.getTotalShards(), response.getSkippedShards(), List.of());
        }

        List<SearchShardStatistics.Failure> searchFailures = Arrays
            .stream(failures)
            .map(f -> SearchShardStatistics.Failure
                .of(f.index(), null, f.status().name(), f.shardId(), null, toErrorCause(f.reason(), f.getCause())))
            .toList();

        return SearchShardStatistics.of(response.getFailedShards(), response.getSuccessfulShards(), response.getTotalShards(), response.getSkippedShards(),
                searchFailures);
    }

    /**
     * Throwable을 ElasticsearchErrorCause로 변환
     * 원본: org.opensearch.data.client.orhlc.ResponseConverter.toErrorCause (package-private)
     */
    @Nullable
    private static ElasticsearchErrorCause toErrorCause(String reason, @Nullable Throwable errorCause) {
        if (errorCause == null) {
            return null;
        }

        StringWriter sw = new StringWriter();
        try (PrintWriter writer = new PrintWriter(sw)) {
            errorCause.printStackTrace(writer);
            writer.flush();

            return new ElasticsearchErrorCause(
                    null,
                    reason,
                    sw.toString(),
                    toErrorCause(reason, errorCause.getCause()),
                    List.of(),
                    Arrays.stream(errorCause.getSuppressed())
                            .map(s -> toErrorCause(reason, s))
                            .collect(Collectors.toList())
            );
        }
    }

    @Nullable
    private static <T> Suggest suggestFrom(
            @Nullable org.opensearch.search.suggest.Suggest suggestES,
            SearchDocumentResponse.EntityCreator<T> entityCreator) {

        if (suggestES == null) {
            return null;
        }

        List<Suggest.Suggestion<? extends Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>>>
                suggestions = new ArrayList<>();

        for (org.opensearch.search.suggest.Suggest.Suggestion<
                        ? extends
                                org.opensearch.search.suggest.Suggest.Suggestion.Entry<
                                        ? extends org.opensearch.search.suggest.Suggest.Suggestion.Entry.Option>>
                suggestionES : suggestES) {

            if (suggestionES instanceof org.opensearch.search.suggest.term.TermSuggestion) {
                org.opensearch.search.suggest.term.TermSuggestion termSuggestionES =
                        (org.opensearch.search.suggest.term.TermSuggestion) suggestionES;

                List<TermSuggestion.Entry> entries = new ArrayList<>();
                for (org.opensearch.search.suggest.term.TermSuggestion.Entry entryES : termSuggestionES) {

                    List<TermSuggestion.Entry.Option> options = new ArrayList<>();
                    for (org.opensearch.search.suggest.term.TermSuggestion.Entry.Option optionES : entryES) {
                        options.add(new TermSuggestion.Entry.Option(
                                textToString(optionES.getText()),
                                textToString(optionES.getHighlighted()),
                                optionES.getScore(),
                                optionES.collateMatch(),
                                optionES.getFreq()));
                    }

                    entries.add(new TermSuggestion.Entry(
                            textToString(entryES.getText()), entryES.getOffset(), entryES.getLength(), options));
                }

                suggestions.add(new TermSuggestion(
                        termSuggestionES.getName(),
                        termSuggestionES.getSize(),
                        entries,
                        suggestFrom(termSuggestionES.getSort())));
            }

            if (suggestionES instanceof org.opensearch.search.suggest.phrase.PhraseSuggestion) {
                org.opensearch.search.suggest.phrase.PhraseSuggestion phraseSuggestionES =
                        (org.opensearch.search.suggest.phrase.PhraseSuggestion) suggestionES;

                List<PhraseSuggestion.Entry> entries = new ArrayList<>();
                for (org.opensearch.search.suggest.phrase.PhraseSuggestion.Entry entryES : phraseSuggestionES) {

                    List<PhraseSuggestion.Entry.Option> options = new ArrayList<>();
                    for (org.opensearch.search.suggest.phrase.PhraseSuggestion.Entry.Option optionES : entryES) {
                        options.add(new PhraseSuggestion.Entry.Option(
                                textToString(optionES.getText()),
                                textToString(optionES.getHighlighted()),
                                (double) optionES.getScore(),
                                optionES.collateMatch()));
                    }

                    entries.add(new PhraseSuggestion.Entry(
                            textToString(entryES.getText()),
                            entryES.getOffset(),
                            entryES.getLength(),
                            options,
                            entryES.getCutoffScore()));
                }

                suggestions.add(
                        new PhraseSuggestion(phraseSuggestionES.getName(), phraseSuggestionES.getSize(), entries));
            }

            if (suggestionES instanceof org.opensearch.search.suggest.completion.CompletionSuggestion) {
                org.opensearch.search.suggest.completion.CompletionSuggestion completionSuggestionES =
                        (org.opensearch.search.suggest.completion.CompletionSuggestion) suggestionES;

                List<CompletionSuggestion.Entry<T>> entries = new ArrayList<>();
                for (org.opensearch.search.suggest.completion.CompletionSuggestion.Entry entryES :
                        completionSuggestionES) {

                    List<CompletionSuggestion.Entry.Option<T>> options = new ArrayList<>();
                    for (org.opensearch.search.suggest.completion.CompletionSuggestion.Entry.Option optionES :
                            entryES) {
                        // CustomDocumentAdapters 사용
                        SearchDocument searchDocument =
                                optionES.getHit() != null ? CustomDocumentAdapters.from(optionES.getHit()) : null;
                        T hitEntity = null;

                        if (searchDocument != null) {
                            try {
                                hitEntity = entityCreator.apply(searchDocument).get();
                            } catch (InterruptedException | ExecutionException e) {
                                if (LOGGER.isWarnEnabled()) {
                                    LOGGER.warn("Error creating entity from SearchDocument");
                                }
                                Thread.currentThread().interrupt();
                            }
                        }

                        options.add(new CompletionSuggestion.Entry.Option<>(
                                textToString(optionES.getText()),
                                textToString(optionES.getHighlighted()),
                                (double) optionES.getScore(),
                                optionES.collateMatch(),
                                optionES.getContexts(),
                                scoreDocFrom(optionES.getDoc()),
                                searchDocument,
                                hitEntity));
                    }

                    entries.add(new CompletionSuggestion.Entry<>(
                            textToString(entryES.getText()), entryES.getOffset(), entryES.getLength(), options));
                }

                suggestions.add(new CompletionSuggestion<>(
                        completionSuggestionES.getName(), completionSuggestionES.getSize(), entries));
            }
        }

        return new Suggest(suggestions, suggestES.hasScoreDocs());
    }

    private static SortBy suggestFrom(org.opensearch.search.suggest.SortBy sort) {
        return SortBy.valueOf(sort.name().toUpperCase());
    }

    @Nullable
    private static ScoreDoc scoreDocFrom(@Nullable org.apache.lucene.search.ScoreDoc scoreDoc) {

        if (scoreDoc == null) {
            return null;
        }

        return new ScoreDoc(scoreDoc.score, scoreDoc.doc, scoreDoc.shardIndex);
    }

    private static String textToString(@Nullable Text text) {
        return text != null ? text.string() : "";
    }
}