/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.seatunnel.connectors.seatunnel.http.source;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.ReadContext;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONUtil;
import org.apache.seatunnel.api.serialization.DeserializationSchema;
import org.apache.seatunnel.api.source.Boundedness;
import org.apache.seatunnel.api.source.Collector;
import org.apache.seatunnel.api.table.type.SeaTunnelRow;
import org.apache.seatunnel.common.utils.JsonUtils;
import org.apache.seatunnel.connectors.seatunnel.common.source.AbstractSingleSplitReader;
import org.apache.seatunnel.connectors.seatunnel.common.source.SingleSplitReaderContext;
import org.apache.seatunnel.connectors.seatunnel.http.client.HttpClientProvider;
import org.apache.seatunnel.connectors.seatunnel.http.client.HttpResponse;
import org.apache.seatunnel.connectors.seatunnel.http.config.HttpParameter;
import org.apache.seatunnel.connectors.seatunnel.http.config.JsonField;
import org.apache.seatunnel.connectors.seatunnel.http.config.PageCondition;
import org.apache.seatunnel.connectors.seatunnel.http.config.PageParam;
import org.apache.seatunnel.connectors.seatunnel.http.enums.PageConditionType;
import org.apache.seatunnel.connectors.seatunnel.http.enums.PageRelationType;
import org.apache.seatunnel.connectors.seatunnel.http.exception.HttpConnectorErrorCode;
import org.apache.seatunnel.connectors.seatunnel.http.exception.HttpConnectorException;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TimeZone;

import static org.apache.seatunnel.connectors.seatunnel.http.enums.PageType.OFFSET;
import static org.apache.seatunnel.connectors.seatunnel.http.enums.PageType.PAGE_NUM;

@Slf4j
public class HttpSourceReader extends AbstractSingleSplitReader<SeaTunnelRow> {
    protected final SingleSplitReaderContext context;
    protected final HttpParameter httpParameter;
    protected HttpClientProvider httpClient;
    private final DeserializationCollector deserializationCollector;
    private static final Option[] DEFAULT_OPTIONS = {
        Option.SUPPRESS_EXCEPTIONS, Option.ALWAYS_RETURN_LIST, Option.DEFAULT_PATH_LEAF_TO_NULL
    };
    private JsonPath[] jsonPaths;
    private final JsonField jsonField;
    private final String contentJson;
    private final Configuration jsonConfiguration =
            Configuration.defaultConfiguration().addOptions(DEFAULT_OPTIONS);

    public HttpSourceReader(
            HttpParameter httpParameter,
            SingleSplitReaderContext context,
            DeserializationSchema<SeaTunnelRow> deserializationSchema,
            JsonField jsonField,
            String contentJson) {
        this.context = context;
        this.httpParameter = httpParameter;
        this.deserializationCollector = new DeserializationCollector(deserializationSchema);
        this.jsonField = jsonField;
        this.contentJson = contentJson;
    }

    @Override
    public void open() {
        httpClient = new HttpClientProvider(httpParameter);
    }

    @Override
    public void close() throws IOException {
        if (Objects.nonNull(httpClient)) {
            httpClient.close();
        }
    }

    @Override
    public void pollNext(Collector<SeaTunnelRow> output) throws Exception {
        //判断是否需要分页查询数据
        int times = 1, httpCode = 200;
        String dataJson = null;
        Object currPageVal = null;
        //初始化分页
        initPageParam();
        do{
            try {
                //获取当前分页数据
                currPageVal = getCurrPageVal();
                HttpResponse response =
                        httpClient.execute(
                                this.httpParameter.getUrl(),
                                this.httpParameter.getMethod().getMethod(),
                                this.httpParameter.getHeaders(),
                                this.httpParameter.getParams(),
                                this.httpParameter.getBody());
                //获取HttpCode
                httpCode = response.getCode();
                if (HttpResponse.STATUS_OK == response.getCode()) {
                    String content = response.getContent();
                    if (!Strings.isNullOrEmpty(content)) {
                        if (this.httpParameter.isEnableMultilines()) {
                            try(StringReader stringReader = new StringReader(content);
                                BufferedReader bufferedReader = new BufferedReader(stringReader);){
                                String lineStr;
                                while ((lineStr = bufferedReader.readLine()) != null) {
                                    collect(output, lineStr);
                                }
                            } finally{ }
                        } else {
                            dataJson = collect(output, content);
                        }
                    }
                }else {
                    log.error(
                            "http client execute exception, http response status code:[{}], content:[{}]",
                            response.getCode(),
                            response.getContent());
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            } finally {
                //循环查询次数++
                ++times;
                //设置下次分页数据参数
                setNextPageVal(dataJson, currPageVal);
                if (Boundedness.BOUNDED.equals(context.getBoundedness())) {
                    // signal to the source that we have reached the end of the data.
                    log.info("Closed the bounded http source");
                    context.signalNoMoreElement();
                } else {
                    if (httpParameter.getPollIntervalMillis() > 0) {
                        Thread.sleep(httpParameter.getPollIntervalMillis());
                    }
                }
            }
        } while(!checkEndQuery(httpCode, dataJson, currPageVal, times));
    }

    private String collect(Collector<SeaTunnelRow> output, String data) throws IOException {
        //前置操作：把包含json字符串的value值规整
        String orgJson = data = dataNeaten(data);
        if (contentJson != null) {
            data = JsonUtils.stringToJsonNode(getPartOfJson(data)).toString();
        }
        if (jsonField != null) {
            this.initJsonPath(jsonField);
            data = JsonUtils.toJsonNode(parseToMap(decodeJSON(data), jsonField)).toString();
        }
        deserializationCollector.collect(data.getBytes(), output);
        return orgJson;
    }

    private List<Map<String, String>> parseToMap(List<List<String>> datas, JsonField jsonField) {
        List<Map<String, String>> decodeDatas = new ArrayList<>(datas.size());
        String[] keys = jsonField.getFields().keySet().toArray(new String[] {});

        for (List<String> data : datas) {
            Map<String, String> decodeData = new HashMap<>(jsonField.getFields().size());
            final int[] index = {0};
            data.forEach(
                    field -> {
                        decodeData.put(keys[index[0]], field);
                        index[0]++;
                    });
            decodeDatas.add(decodeData);
        }

        return decodeDatas;
    }

    private List<List<String>> decodeJSON(String data) {
        ReadContext jsonReadContext = JsonPath.using(jsonConfiguration).parse(data);
        List<List<String>> results = new ArrayList<>(jsonPaths.length);
        for (JsonPath path : jsonPaths) {
            List<String> result = jsonReadContext.read(path);
            results.add(result);
        }
        for (int i = 1; i < results.size(); i++) {
            List<?> result0 = results.get(0);
            List<?> result = results.get(i);
            if (result0.size() != result.size()) {
                throw new HttpConnectorException(
                        HttpConnectorErrorCode.FIELD_DATA_IS_INCONSISTENT,
                        String.format(
                                "[%s](%d) and [%s](%d) the number of parsing records is inconsistent.",
                                jsonPaths[0].getPath(),
                                result0.size(),
                                jsonPaths[i].getPath(),
                                result.size()));
            }
        }

        return dataFlip(results);
    }

    private String getPartOfJson(String data) {
        ReadContext jsonReadContext = JsonPath.using(jsonConfiguration).parse(data);
        return JsonUtils.toJsonString(jsonReadContext.read(JsonPath.compile(contentJson)));
    }

    private List<List<String>> dataFlip(List<List<String>> results) {

        List<List<String>> datas = new ArrayList<>();
        for (int i = 0; i < results.size(); i++) {
            List<String> result = results.get(i);
            if (i == 0) {
                for (Object o : result) {
                    String val = o == null ? null : o.toString();
                    List<String> row = new ArrayList<>(jsonPaths.length);
                    row.add(val);
                    datas.add(row);
                }
            } else {
                for (int j = 0; j < result.size(); j++) {
                    Object o = result.get(j);
                    String val = o == null ? null : o.toString();
                    List<String> row = datas.get(j);
                    row.add(val);
                }
            }
        }
        return datas;
    }

    private void initJsonPath(JsonField jsonField) {
        jsonPaths = new JsonPath[jsonField.getFields().size()];
        for (int index = 0; index < jsonField.getFields().keySet().size(); index++) {
            jsonPaths[index] =
                    JsonPath.compile(
                            jsonField.getFields().values().toArray(new String[] {})[index]);
        }
    }

    private static final ObjectMapper OBJECT_MAPPER =
            new ObjectMapper()
                    .setTimeZone(TimeZone.getDefault());

    private String dataNeaten(String data) {
        if(httpParameter.getDeepParseFields() != null && !httpParameter.getDeepParseFields().isEmpty()) {
            for(String deepParseFieldJsonPath : httpParameter.getDeepParseFields()){
                Object _object = JsonPath.read(data, deepParseFieldJsonPath);
                if(!(_object instanceof String)) {
                    throw new UnsupportedOperationException(String.format("jsonpath:[%s]对应的值必须是字符串类型"));
                }
                try {
                    data = JsonPath.parse(data).set(deepParseFieldJsonPath, OBJECT_MAPPER.readValue(_object.toString(), Object.class)).jsonString();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return data;
    }

    /**
     * 初始化分页设置
     * 1. 查找并设置pageParam.paramName
     * 2. 设置初始值
     */
    private void initPageParam(){
        PageParam pageParam = this.httpParameter.getPageParam();
        if(pageParam.getPageEnabled()) {
            String paramName = null;
            //根据value值去查找paramName
            Optional<Map.Entry<String, String>> optionalEntry = Optional.ofNullable(this.httpParameter.getParams()).orElse(new HashMap<>())
                    .entrySet().stream().filter(item -> item.getValue().equals(pageParam.getPageType().getParamNamePlaceholder())).findFirst();
            if(optionalEntry.isPresent()) {
                paramName = optionalEntry.get().getKey();
                this.httpParameter.getParams().put(paramName, pageParam.getCurrVal());
            }
            if(Strings.isNullOrEmpty(paramName)) {
                Optional<Map.Entry<String, String>> headerOptionalEntry = Optional.ofNullable(this.httpParameter.getHeaders()).orElse(new HashMap<>())
                        .entrySet().stream().filter(item -> item.getValue().equals(pageParam.getPageType().getParamNamePlaceholder())).findFirst();
                if(headerOptionalEntry.isPresent()) {
                    paramName = headerOptionalEntry.get().getKey();
                    this.httpParameter.getHeaders().put(paramName, pageParam.getCurrVal());
                }
            }
            if(!Strings.isNullOrEmpty(paramName)) {
                this.httpParameter.getPageParam().setParamName(paramName);
            }else{
                throw new RuntimeException(String.format(
                        "[%s] can't find page parameters.",
                        pageParam.getPageType()));
            }
        }
    }

    /**
     * 获取分页参数值
     * @return
     */
    private Object getCurrPageVal() {
        PageParam pageParam = this.httpParameter.getPageParam();
        Object currPageVal = null;
        if(pageParam.getPageEnabled()) {
            String paramName = pageParam.getParamName();
            String value = Optional.ofNullable(this.httpParameter.getParams()).orElse(new HashMap<>()).get(paramName);
            if (Strings.isNullOrEmpty(value)) {
                value = Optional.ofNullable(this.httpParameter.getHeaders()).orElse(new HashMap<>()).get(paramName);
            }
            if (Strings.isNullOrEmpty(value)) {
                throw new RuntimeException("未找到分页参数｜pageParamName:" + paramName);
            }
            currPageVal = value;
            if (pageParam.getPageType() == PAGE_NUM || pageParam.getPageType() == OFFSET) {
                currPageVal = Integer.valueOf(value);
            }
            log.info("分页类型:{}|分页查询数据|pageNum/cursor={}", pageParam.getPageType(), currPageVal);
        }
        return currPageVal;
    }

    /**
     * 设置下一次分页数据
     * @param currPageVal
     * @return
     */
    private void setNextPageVal(String dataJson, Object currPageVal) {
        PageParam pageParam = this.httpParameter.getPageParam();
        if(pageParam.getPageEnabled()) {
            String paramName = pageParam.getParamName();
            Integer stepVal = pageParam.getStepVal();
            String updateFieldPath = pageParam.getUpdateFieldPath();
            Map<String, String> paramsMap = Optional.ofNullable(this.httpParameter.getParams()).orElse(new HashMap<>());
            String nextVal = null;
            if (pageParam.getPageType() == PAGE_NUM || pageParam.getPageType() == OFFSET) {
                nextVal = String.valueOf(((Integer)currPageVal) + stepVal);
            }else {// cursor方式，通过jsonpath获取值并更新
                nextVal = JsonPath.read(dataJson, updateFieldPath);
            }
            if(paramsMap.containsKey(paramName)) {
                this.httpParameter.getParams().put(paramName, nextVal);
            }else{
                this.httpParameter.getHeaders().put(paramName, nextVal);
            }
        }
    }

    /**
     * 校验分页查询是否结束
     * @param dataJson
     * @return
     */
    private boolean checkEndQuery(int httpCode, String dataJson, Object currPageVal, int times) {
        PageParam pageParam = this.httpParameter.getPageParam();
        if(pageParam.getPageEnabled()) {
            try {
                List<PageCondition> endConditions = Optional.ofNullable(pageParam.getEndConditions()).orElse(new ArrayList<>());
                PageRelationType endConditionRelationType = pageParam.getEndConditionRelationType();
                List<Boolean> conditionVals = new ArrayList<>();
                for(PageCondition pageCondition : endConditions){
                    PageConditionType conditionType = pageCondition.getConditionType();
                    Object _currentValue = null;
                    switch (conditionType){
                        case HTTP_STATUS: {
                            _currentValue = httpCode; break;
                        }
                        case REQUEST_TIMES: {
                            _currentValue = times; break;
                        }
                        case JSON_PATH: {
                            _currentValue = JsonPath.read(dataJson, pageCondition.getFieldPath()); break;
                        }
                        case CURRENT_VALUE: {
                            _currentValue = currPageVal; break;
                        }
                    }
                    conditionVals.add(pageCondition.getFunction().compare(_currentValue, pageCondition.getValue()));
                }
                //或者 达到最大循环次数
                return pageParam.getRequestMaxTimes() <= times || (endConditionRelationType == PageRelationType.OR ? conditionVals.contains(true) : !conditionVals.contains(false));
            } catch (Exception e) {
                log.error("接口返回数据:{}", dataJson);
                log.error(e.getMessage(), e);
            }
        }
        return true;
    }
}
