package com.storlead.es.server.impl;

import cn.hutool.core.util.StrUtil;
import com.storlead.es.pojo.vo.EsGenericVO;
import com.storlead.es.pojo.vo.EsQuerySimilarityVO;
import com.storlead.es.pojo.vo.FieldConfig;
import com.storlead.es.pojo.vo.IndexFieldConfig;
import com.storlead.es.service.EsSearchCustomerService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.apache.commons.lang3.tuple.Pair;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.ScriptScoreQueryBuilder;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;

@Service
@ConditionalOnProperty(prefix = "storlead.es", name = "enabled", havingValue = "true")
public class EsSearchCustomerServiceImpl implements EsSearchCustomerService {

    @Resource
    private ElasticsearchRestTemplate elasticsearchTemplate;

    private static String similarityScript = "\n" +
            "def field = params.field;\n" +
            "if (!doc.containsKey(field) || doc[field].size() == 0 || doc[field].value == null) {\n" +
            "    return 0;\n" +
            "}\n" +
            "if (params.query == null) return 0;\n" +
            "def docValue = doc[field].value;\n" +
            "String s1 = docValue.toString().toLowerCase();\n" +
            "String s2 = params.query?.toString().toLowerCase();\n" +
            "if (s1 == null || s2 == null) return 0;\n" +
            "if (s1.equals(s2)) return 100;\n" +
            "int s1_len = s1.length();\n" +
            "int s2_len = s2.length();\n" +
            "if (s1_len == 0 || s2_len == 0) return 0;\n" +
            "if (s1_len == 0) return (int)(100 * (1 - (s2_len / Math.max(1, s2_len))));\n" +
            "if (s2_len == 0) return (int)(100 * (1 - (s1_len / Math.max(1, s1_len))));\n" +
            "int[] costs = new int[s2_len + 1];\n" +
            "for (int j = 0; j <= s2_len; j++) {\n" +
            "    costs[j] = j;\n" +
            "}\n" +
            "for (int i = 1; i <= s1_len; i++) {\n" +
            "    costs[0] = i;\n" +
            "    int prev = i - 1;\n" +
            "    for (int j = 1; j <= s2_len; j++) {\n" +
            "        int cost = (s1.charAt(i - 1) == s2.charAt(j - 1)) ? 0 : 1;\n" +
            "        int insertion = costs[j] + 1;\n" +
            "        int deletion = costs[j - 1] + 1;\n" +
            "        int substitution = prev + cost;\n" +
            "        int current = insertion;\n" +
            "        if (deletion < current) {\n" +
            "           current = deletion;\n" +
            "        } \n" +
            "        if (substitution < current) {\n" +
            "           current = substitution;\n" +
            "        } \n" +
            "        prev = costs[j];\n" +
            "        costs[j] = current;\n" +
            "    }\n" +
            "}\n" +
            "int distance = costs[s2_len];\n" +
            "int maxLen = s1_len > s2_len ? s1_len : s2_len;\n" +
            "if (maxLen == 0) return 100;\n"+
            "double similarity = 100 * (1 - ((double)distance / maxLen));\n" +
            "return (int) Math.round(similarity);";

    @Override
    public List<EsQuerySimilarityVO> listComparisonSimilarity(EsGenericVO dto) {
        List<IndexFieldConfig>  indexFields=  dto.getIndexFields();
        Map<String, IndexFieldConfig> indexFieldsMap = indexFields.stream().collect(Collectors.toMap(IndexFieldConfig::getIndexName, IndexFieldConfig -> IndexFieldConfig));
        List<Map> combinedResults = new ArrayList<>();
        float minScore = Float.parseFloat(dto.getMinScore());
        List<Pair<String, NativeSearchQuery>> indexQueries = new ArrayList<>();
        indexFieldsMap.forEach((indexName, indexField) -> {
            List<FieldConfig> fields = indexField.getFields();
            for (int i = 0; i < fields.size(); i++) {
                BoolQueryBuilder indexQuery = QueryBuilders.boolQuery();
                FieldConfig field = fields.get(i);
                Map<String, Object> scriptParams = new HashMap<>();
                scriptParams.put("field", field.getFieldName());
                scriptParams.put("query", dto.getQueryText());
                scriptParams.put("targetIndex", indexName);
                indexQuery.mustNot(QueryBuilders.termQuery("is_delete", 1));
                if(!Objects.isNull(dto.getCustomerForm())){
                    indexQuery.must(QueryBuilders.termQuery("customer_form", dto.getCustomerForm()));
                }
                ScriptScoreQueryBuilder scriptScoreQuery = QueryBuilders.scriptScoreQuery(
                        indexQuery,
                        new Script(
                                ScriptType.INLINE,
                                "painless",
                                similarityScript,
                                scriptParams
                        )
                ).setMinScore(minScore);
                NativeSearchQuery indexSearchQuery = new NativeSearchQueryBuilder()
                        .withQuery(scriptScoreQuery)
                        .withSort(SortBuilders.scoreSort().order(SortOrder.DESC))
                        .withPageable(PageRequest.of(0, 1000))
                        .build();
                indexQueries.add(Pair.of(indexName, indexSearchQuery));

                SearchHits<Map> hits = elasticsearchTemplate.search(
                        indexSearchQuery,
                        Map.class,
                        IndexCoordinates.of(indexName));

                hits.getSearchHits().stream()
                        .map(hit -> {
                            Map<String, Object> content = new HashMap<>(hit.getContent());
                            content.put("similarityScore", hit.getScore());
                            content.put("matchedIndex", hit.getIndex());
                            content.put("matchedField", field.getFieldName());
                            return content;
                        })
                        .forEach(combinedResults::add);
            }
        });

        combinedResults.sort((a, b) -> {
            float scoreA = (float) a.getOrDefault("similarityScore", 0f);
            float scoreB = (float) b.getOrDefault("similarityScore", 0f);
            return Float.compare(scoreB, scoreA);
        });

        List<EsQuerySimilarityVO> vos = convertToVoList(combinedResults,indexFieldsMap);
        return vos;
    }

    public  List<EsQuerySimilarityVO> convertToVoList(List<Map> combinedResults,Map<String, IndexFieldConfig> indexFieldsMap) {
        if (CollectionUtils.isEmpty(combinedResults)) {
            return new ArrayList<>();
        }
        return combinedResults.stream()
                .map(result -> {
                    EsQuerySimilarityVO vo = new EsQuerySimilarityVO();
                    vo.setId(Long.valueOf(result.get("id").toString()));
                    if (result.containsKey("customer_id") && Objects.nonNull(result.get("customer_id"))) {
                        vo.setCustomerId(Long.valueOf(result.get("customer_id").toString()));
                    }
                    if (result.containsKey("owner_by") && Objects.nonNull(result.get("owner_by"))) {
                        vo.setOwnerBy(Long.valueOf(result.get("owner_by").toString()));
                    }
                    vo.setObject(result);
                    Float score = (Float) result.get("similarityScore");
                    vo.setSimilarityScore(score.toString());
                    vo.setIndexName((String) result.get("matchedIndex"));
                    IndexFieldConfig indexField = indexFieldsMap.get(vo.getIndexName());
                    vo.setResourceType(indexField.getResourceType().toString());
                    findMatchedField(vo,result, vo.getIndexName(),indexFieldsMap);
                    if (StrUtil.isNotBlank(vo.getMatchedField())) {
                        vo.setDuplicateValue(result.get(vo.getMatchedField()).toString());
                    }
                    return vo;
                })
                .collect(Collectors.toList());
    }

    private void findMatchedField(EsQuerySimilarityVO vo,Map<String, Object> result, String indexName,Map<String, IndexFieldConfig> indexConfigMap) {
        IndexFieldConfig indexConfig = indexConfigMap.get(indexName);
        List<FieldConfig> fields = indexConfig.getFields();
        for (FieldConfig field : fields) {
            String cpField =  result.get("matchedField").toString();
            if (cpField.equals(field.getFieldName())) {
                String baseField = cpField.replace(".keyword", "");
                if (result.containsKey(baseField) && result.get(baseField) != null) {
                    vo.setMatchedField(baseField);
                    vo.setDuplicateField(baseField);
                    vo.setErrorMsg(field.getFieldDescribe());
                }
            }
        }
    }
}
