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

import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import org.opensearch.action.get.GetResponse;
import org.opensearch.action.get.MultiGetItemResponse;
import org.opensearch.action.get.MultiGetResponse;
import org.opensearch.common.document.DocumentField;
import org.opensearch.core.common.bytes.BytesReference;
import org.opensearch.core.common.text.Text;
import org.opensearch.data.client.orhlc.ResponseConverter;
import org.opensearch.index.get.GetResult;
import org.opensearch.search.SearchHit;
import org.opensearch.search.SearchHits;
import org.springframework.data.elasticsearch.core.MultiGetItem;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.document.Explanation;
import org.springframework.data.elasticsearch.core.document.NestedMetaData;
import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.mapping.MappingException;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Custom DocumentAdapters - 배열 필드 처리를 위한 커스텀 어댑터
 * 원본: org.opensearch.data.client.orhlc.DocumentAdapters
 *
 * 변경사항:
 * - DocumentFieldAdapter.get() 메서드에서 다중 값(배열)을 List로 반환하도록 수정
 */
public final class CustomDocumentAdapters {

    private CustomDocumentAdapters() {}

    @Nullable
    public static Document from(GetResponse getResponse) {

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

        if (!getResponse.isExists()) {
            return null;
        }

        if (getResponse.isSourceEmpty()) {
            return fromDocumentFields(
                    getResponse,
                    getResponse.getIndex(),
                    getResponse.getId(),
                    getResponse.getVersion(),
                    getResponse.getSeqNo(),
                    getResponse.getPrimaryTerm());
        }

        Document document = Document.from(getResponse.getSourceAsMap());
        document.setIndex(getResponse.getIndex());
        document.setId(getResponse.getId());
        document.setVersion(getResponse.getVersion());
        document.setSeqNo(getResponse.getSeqNo());
        document.setPrimaryTerm(getResponse.getPrimaryTerm());

        return document;
    }

    @Nullable
    public static Document from(GetResult source) {

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

        if (!source.isExists()) {
            return null;
        }

        if (source.isSourceEmpty()) {
            return fromDocumentFields(
                    source,
                    source.getIndex(),
                    source.getId(),
                    source.getVersion(),
                    source.getSeqNo(),
                    source.getPrimaryTerm());
        }

        Document document = Document.from(source.getSource());
        document.setIndex(source.getIndex());
        document.setId(source.getId());
        document.setVersion(source.getVersion());
        document.setSeqNo(source.getSeqNo());
        document.setPrimaryTerm(source.getPrimaryTerm());

        return document;
    }

    public static List<MultiGetItem<Document>> from(MultiGetResponse source) {

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

        return Arrays.stream(source.getResponses())
                .map(CustomDocumentAdapters::from)
                .collect(Collectors.toList());
    }

    public static MultiGetItem<Document> from(MultiGetItemResponse itemResponse) {

        MultiGetItem.Failure failure = ResponseConverter.getFailure(itemResponse);
        return MultiGetItem.of(
                itemResponse.isFailed() ? null : CustomDocumentAdapters.from(itemResponse.getResponse()), failure);
    }

    /**
     * Create a {@link SearchDocument} from {@link SearchHit}.
     * 배열 필드 처리를 위해 CustomDocumentFieldAdapter 사용
     */
    public static SearchDocument from(SearchHit source) {

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

        Map<String, List<String>> highlightFields = new HashMap<>(source.getHighlightFields().entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, entry -> Arrays.stream(
                                entry.getValue().getFragments())
                        .map(Text::string)
                        .collect(Collectors.toList()))));

        Map<String, SearchDocumentResponse> innerHits = new LinkedHashMap<>();
        Map<String, SearchHits> sourceInnerHits = source.getInnerHits();

        if (sourceInnerHits != null) {
            sourceInnerHits.forEach((name, searchHits) -> innerHits.put(
                    name,
                    CustomSearchDocumentResponseBuilder.from(searchHits, null, null, 0, null, null,
                        searchDocument -> CompletableFuture.completedFuture(null))));
        }

        NestedMetaData nestedMetaData = from(source.getNestedIdentity());
        Explanation explanation = from(source.getExplanation());
        List<String> matchedQueries = from(source.getMatchedQueries());

        BytesReference sourceRef = source.getSourceRef();
        Map<String, DocumentField> sourceFields = source.getFields();

        if (sourceRef == null || sourceRef.length() == 0) {
            // docValueFields를 _source와 동일한 nested Map 구조로 변환
            Map<String, Object> nestedMap = convertFieldsToNestedMap(sourceFields);
            Document document = Document.from(nestedMap);
            document.setIndex(source.getIndex());
            document.setId(source.getId());
            if (source.getVersion() >= 0) {
                document.setVersion(source.getVersion());
            }
            document.setSeqNo(source.getSeqNo());
            document.setPrimaryTerm(source.getPrimaryTerm());

            return new SearchDocumentAdapter(
                    document,
                    source.getScore(),
                    source.getSortValues(),
                    sourceFields,
                    highlightFields,
                    innerHits,
                    nestedMetaData,
                    explanation,
                    matchedQueries);
        }

        Document document = Document.from(source.getSourceAsMap());
        document.setIndex(source.getIndex());
        document.setId(source.getId());

        if (source.getVersion() >= 0) {
            document.setVersion(source.getVersion());
        }
        document.setSeqNo(source.getSeqNo());
        document.setPrimaryTerm(source.getPrimaryTerm());

        return new SearchDocumentAdapter(
                document,
                source.getScore(),
                source.getSortValues(),
                sourceFields,
                highlightFields,
                innerHits,
                nestedMetaData,
                explanation,
                matchedQueries);
    }

    @Nullable
    private static Explanation from(@Nullable org.apache.lucene.search.Explanation explanation) {

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

        List<Explanation> details = new ArrayList<>();
        for (org.apache.lucene.search.Explanation detail : explanation.getDetails()) {
            details.add(from(detail));
        }

        return new Explanation(
                explanation.isMatch(), explanation.getValue().doubleValue(), explanation.getDescription(), details);
    }

    @Nullable
    private static NestedMetaData from(@Nullable SearchHit.NestedIdentity nestedIdentity) {

        if (nestedIdentity == null) {
            return null;
        }
        NestedMetaData child = from(nestedIdentity.getChild());
        return NestedMetaData.of(nestedIdentity.getField().string(), nestedIdentity.getOffset(), child);
    }

    @Nullable
    private static List<String> from(@Nullable String[] matchedQueries) {
        return matchedQueries == null ? null : Arrays.asList(matchedQueries);
    }

    @SuppressWarnings("unchecked")
    private static Map<String, Object> convertFieldsToNestedMap(Map<String, DocumentField> fields) {
        Map<String, Object> result = new LinkedHashMap<>();

        for (Map.Entry<String, DocumentField> entry : fields.entrySet()) {
            String fieldKey = entry.getKey();
            DocumentField documentField = entry.getValue();

            // .keyword suffix 제거
            if (fieldKey.endsWith(".keyword")) {
                fieldKey = fieldKey.substring(0, fieldKey.length() - 8);
            }

            // 값 추출 (단일 값 또는 배열)
            Object value = extractFieldValue(documentField);
            if (value == null) {
                continue;
            }

            // nested 구조로 변환 (예: "assignee.assignee_displayName" → nested map)
            String[] parts = fieldKey.split("\\.");
            if (parts.length == 1) {
                // flat 필드 (예: "key", "pdServiceId")
                result.put(fieldKey, value);
            } else {
                // nested 필드 (예: "assignee.assignee_displayName")
                Map<String, Object> current = result;
                for (int i = 0; i < parts.length - 1; i++) {
                    String part = parts[i];
                    if (!current.containsKey(part)) {
                        current.put(part, new LinkedHashMap<String, Object>());
                    }
                    Object nested = current.get(part);
                    if (nested instanceof Map) {
                        current = (Map<String, Object>) nested;
                    } else {
                        // 기존 값이 Map이 아닌 경우 (충돌), 새 Map으로 덮어쓰기
                        Map<String, Object> newMap = new LinkedHashMap<>();
                        current.put(part, newMap);
                        current = newMap;
                    }
                }
                current.put(parts[parts.length - 1], value);
            }
        }

        return result;
    }

    /**
     * DocumentField에서 값 추출 (단일 값/배열 처리 + Integer→Long 변환)
     */
    @Nullable
    private static Object extractFieldValue(DocumentField documentField) {
        List<Object> values = documentField.getValues();
        if (values.isEmpty()) {
            return null;
        }

        if (values.size() == 1) {
            return convertNumber(documentField.getValue());
        }

        // 다중 값은 List로 반환
        return values.stream()
                .map(CustomDocumentAdapters::convertNumber)
                .collect(Collectors.toCollection(ArrayList::new));
    }

    /**
     * Integer를 Long으로 변환 (OpenSearch docValueFields 타입 호환성)
     */
    private static Object convertNumber(Object value) {
        if (value instanceof Integer) {
            return ((Integer) value).longValue();
        }
        return value;
    }

    public static Document fromDocumentFields(
            Iterable<DocumentField> documentFields,
            String index,
            String id,
            long version,
            long seqNo,
            long primaryTerm) {

        if (documentFields instanceof Collection) {
            return new DocumentFieldAdapter(
                    (Collection<DocumentField>) documentFields, index, id, version, seqNo, primaryTerm);
        }

        List<DocumentField> fields = new ArrayList<>();
        for (DocumentField documentField : documentFields) {
            fields.add(documentField);
        }

        return new DocumentFieldAdapter(fields, index, id, version, seqNo, primaryTerm);
    }

    /**
     * Custom DocumentFieldAdapter - 배열 필드 처리 지원
     * 원본 대비 변경사항: get() 메서드에서 다중 값을 List로 반환
     */
    static class DocumentFieldAdapter implements Document {

        private final Collection<DocumentField> documentFields;
        private final Map<String, DocumentField> documentFieldMap;
        private final String index;
        private final String id;
        private final long version;
        private final long seqNo;
        private final long primaryTerm;

        DocumentFieldAdapter(
                Collection<DocumentField> documentFields,
                String index,
                String id,
                long version,
                long seqNo,
                long primaryTerm) {
            this.documentFields = documentFields;
            this.documentFieldMap = new LinkedHashMap<>(documentFields.size());
            documentFields.forEach(documentField -> documentFieldMap.put(documentField.getName(), documentField));
            this.index = index;
            this.id = id;
            this.version = version;
            this.seqNo = seqNo;
            this.primaryTerm = primaryTerm;
        }

        @Override
        public String getIndex() {
            return index;
        }

        @Override
        public boolean hasId() {
            return StringUtils.hasLength(id);
        }

        @Override
        public String getId() {
            return this.id;
        }

        @Override
        public boolean hasVersion() {
            return this.version >= 0;
        }

        @Override
        public long getVersion() {

            if (!hasVersion()) {
                throw new IllegalStateException("No version associated with this Document");
            }

            return this.version;
        }

        @Override
        public boolean hasSeqNo() {
            return true;
        }

        @Override
        public long getSeqNo() {

            if (!hasSeqNo()) {
                throw new IllegalStateException("No seq_no associated with this Document");
            }

            return this.seqNo;
        }

        @Override
        public boolean hasPrimaryTerm() {
            return true;
        }

        @Override
        public long getPrimaryTerm() {

            if (!hasPrimaryTerm()) {
                throw new IllegalStateException("No primary_term associated with this Document");
            }

            return this.primaryTerm;
        }

        @Override
        public int size() {
            return documentFields.size();
        }

        @Override
        public boolean isEmpty() {
            return documentFields.isEmpty();
        }

        @Override
        public boolean containsKey(Object key) {
            return documentFieldMap.containsKey(key);
        }

        @Override
        public boolean containsValue(Object value) {

            for (DocumentField documentField : documentFields) {

                Object fieldValue = getValue(documentField);
                if (fieldValue != null && fieldValue.equals(value) || value == fieldValue) {
                    return true;
                }
            }

            return false;
        }

        /**
         * 배열 필드 처리를 위해 수정된 get() 메서드
         * 다중 값이 있는 경우 List로 반환
         * Integer → Long 변환 지원 (OpenSearch가 Integer로 반환하는 경우 대응)
         * .keyword suffix fallback 지원 (OpenSearch가 .keyword 없이 반환하는 경우 대응)
         */
        @Override
        @Nullable
        public Object get(Object key) {
            DocumentField documentField = documentFieldMap.get(key);

            // .keyword suffix fallback 처리
            // OpenSearch docValueFields 응답에서 .keyword 없이 반환되는 경우 대응
            if (documentField == null && key instanceof String) {
                String keyStr = (String) key;
                if (keyStr.endsWith(".keyword")) {
                    documentField = documentFieldMap.get(keyStr.substring(0, keyStr.length() - 8));
                }
            }

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

            List<Object> values = documentField.getValues();
            if (values.isEmpty()) {
                return null;
            }

            // 다중 값인 경우 List로 반환, 단일 값인 경우 단일 값 반환
            if (values.size() > 1) {
                return values.stream()
                        .map(this::convertNumberToLong)
                        .collect(Collectors.toCollection(ArrayList::new));
            }
            return convertNumberToLong(documentField.getValue());
        }

        /**
         * Integer를 Long으로 변환 (OpenSearch docValueFields 타입 호환성)
         */
        private Object convertNumberToLong(Object value) {
            if (value instanceof Integer) {
                return ((Integer) value).longValue();
            }
            return value;
        }

        @Override
        public Object put(String key, Object value) {
            throw new UnsupportedOperationException();
        }

        @Override
        public Object remove(Object key) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void putAll(Map<? extends String, ?> m) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void clear() {
            throw new UnsupportedOperationException();
        }

        @Override
        public Set<String> keySet() {
            return documentFieldMap.keySet();
        }

        @Override
        public Collection<Object> values() {
            return documentFieldMap.values().stream()
                    .map(DocumentFieldAdapter::getValue)
                    .collect(Collectors.toList());
        }

        @Override
        public Set<Entry<String, Object>> entrySet() {
            return documentFieldMap.entrySet().stream()
                    .collect(Collectors.toMap(Entry::getKey, entry -> DocumentFieldAdapter.getValue(entry.getValue())))
                    .entrySet();
        }

        @Override
        public void forEach(BiConsumer<? super String, ? super Object> action) {

            Objects.requireNonNull(action);

            documentFields.forEach(field -> action.accept(field.getName(), getValue(field)));
        }

        @Override
        @SuppressWarnings("java:S2095")
        public String toJson() {

            JsonFactory nodeFactory = new JsonFactory();
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            try(JsonGenerator generator = nodeFactory.createGenerator(stream, JsonEncoding.UTF8)) {
                generator.writeStartObject();
                for (DocumentField value : documentFields) {
                    if (value.getValues().size() > 1) {
                        generator.writeArrayFieldStart(value.getName());
                        for (Object val : value.getValues()) {
                            generator.writeObject(val);
                        }
                        generator.writeEndArray();
                    } else {
                        generator.writeObjectField(value.getName(), value.getValue());
                    }
                }
                generator.writeEndObject();
                generator.flush();
                return new String(stream.toByteArray(), StandardCharsets.UTF_8);
            } catch (IOException e) {
                throw new MappingException("Cannot render JSON", e);
            }
        }

        @Override
        public String toString() {
            return getClass().getSimpleName() + '@' + this.id + '#' + this.version + ' ' + toJson();
        }

        @Nullable
        private static Object getValue(DocumentField documentField) {

            if (documentField.getValues().isEmpty()) {
                return null;
            }

            if (documentField.getValues().size() == 1) {
                return documentField.getValue();
            }

            return documentField.getValues();
        }
    }

    /**
     * Adapter for a {@link SearchDocument}.
     */
    static class SearchDocumentAdapter implements SearchDocument {

        private final float score;
        private final Object[] sortValues;
        private final Map<String, List<Object>> fields = new HashMap<>();
        private final Document delegate;
        private final Map<String, List<String>> highlightFields = new HashMap<>();
        private final Map<String, SearchDocumentResponse> innerHits = new HashMap<>();

        @Nullable
        private final NestedMetaData nestedMetaData;

        @Nullable
        private final Explanation explanation;

        @Nullable
        private final List<String> matchedQueries;

        SearchDocumentAdapter(
                Document delegate,
                float score,
                Object[] sortValues,
                Map<String, DocumentField> fields,
                Map<String, List<String>> highlightFields,
                Map<String, SearchDocumentResponse> innerHits,
                @Nullable NestedMetaData nestedMetaData,
                @Nullable Explanation explanation,
                @Nullable List<String> matchedQueries) {

            this.delegate = delegate;
            this.score = score;
            this.sortValues = sortValues;
            fields.forEach((name, documentField) -> this.fields.put(name, documentField.getValues()));
            this.highlightFields.putAll(highlightFields);
            this.innerHits.putAll(innerHits);
            this.nestedMetaData = nestedMetaData;
            this.explanation = explanation;
            this.matchedQueries = matchedQueries;
        }

        @Override
        public SearchDocument append(String key, Object value) {
            delegate.append(key, value);

            return this;
        }

        @Override
        public float getScore() {
            return score;
        }

        @Override
        public Map<String, List<Object>> getFields() {
            return fields;
        }

        @Override
        public Object[] getSortValues() {
            return sortValues;
        }

        @Override
        public Map<String, List<String>> getHighlightFields() {
            return highlightFields;
        }

        @Override
        public String getIndex() {
            return delegate.getIndex();
        }

        @Override
        public boolean hasId() {
            return delegate.hasId();
        }

        @Override
        public String getId() {
            return delegate.getId();
        }

        @Override
        public void setId(String id) {
            delegate.setId(id);
        }

        @Override
        public boolean hasVersion() {
            return delegate.hasVersion();
        }

        @Override
        public long getVersion() {
            return delegate.getVersion();
        }

        @Override
        public void setVersion(long version) {
            delegate.setVersion(version);
        }

        @Override
        public boolean hasSeqNo() {
            return delegate.hasSeqNo();
        }

        @Override
        public long getSeqNo() {
            return delegate.getSeqNo();
        }

        @Override
        public void setSeqNo(long seqNo) {
            delegate.setSeqNo(seqNo);
        }

        @Override
        public boolean hasPrimaryTerm() {
            return delegate.hasPrimaryTerm();
        }

        @Override
        public long getPrimaryTerm() {
            return delegate.getPrimaryTerm();
        }

        @Override
        public void setPrimaryTerm(long primaryTerm) {
            delegate.setPrimaryTerm(primaryTerm);
        }

        @Override
        public Map<String, SearchDocumentResponse> getInnerHits() {
            return innerHits;
        }

        @Override
        @Nullable
        public NestedMetaData getNestedMetaData() {
            return nestedMetaData;
        }

        @Override
        @Nullable
        public <T> T get(Object key, Class<T> type) {
            return delegate.get(key, type);
        }

        @Override
        public String toJson() {
            return delegate.toJson();
        }

        @Override
        public int size() {
            return delegate.size();
        }

        @Override
        public boolean isEmpty() {
            return delegate.isEmpty();
        }

        @Override
        public boolean containsKey(Object key) {
            return delegate.containsKey(key);
        }

        @Override
        public boolean containsValue(Object value) {
            return delegate.containsValue(value);
        }

        @Override
        public Object get(Object key) {

            if (delegate.containsKey(key)) {
                return delegate.get(key);
            }

            // fallback to fields
            return fields.get(key);
        }

        @Override
        public Object put(String key, Object value) {
            return delegate.put(key, value);
        }

        @Override
        public Object remove(Object key) {
            return delegate.remove(key);
        }

        @Override
        public void putAll(Map<? extends String, ?> m) {
            delegate.putAll(m);
        }

        @Override
        public void clear() {
            delegate.clear();
        }

        @Override
        public Set<String> keySet() {
            return delegate.keySet();
        }

        @Override
        public Collection<Object> values() {
            return delegate.values();
        }

        @Override
        public Set<Entry<String, Object>> entrySet() {
            return delegate.entrySet();
        }

        @Override
        @Nullable
        public Explanation getExplanation() {
            return explanation;
        }

        @Override
        @Nullable
        public List<String> getMatchedQueries() {
            return matchedQueries;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof SearchDocumentAdapter)) {
                return false;
            }
            SearchDocumentAdapter that = (SearchDocumentAdapter) o;
            return Float.compare(that.score, score) == 0 && delegate.equals(that.delegate);
        }

        @Override
        public int hashCode() {
            return delegate.hashCode();
        }

        @Override
        public void forEach(BiConsumer<? super String, ? super Object> action) {
            delegate.forEach(action);
        }

        @Override
        public boolean remove(Object key, Object value) {
            return delegate.remove(key, value);
        }

        @Override
        public String toString() {

            String id = hasId() ? getId() : "?";
            String version = hasVersion() ? Long.toString(getVersion()) : "?";

            return getClass().getSimpleName() + '@' + id + '#' + version + ' ' + toJson();
        }
    }
}