/*
 * 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.dromara.hmily.core.spi.repository;

import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
import com.mongodb.client.result.UpdateResult;
import org.dromara.hmily.annotation.HmilySPI;
import org.dromara.hmily.common.utils.CollectionUtils;
import org.dromara.hmily.common.bean.adapter.MongoAdapter;
import org.dromara.hmily.common.bean.entity.HmilyParticipant;
import org.dromara.hmily.common.bean.entity.HmilyTransaction;
import org.dromara.hmily.common.config.HmilyConfig;
import org.dromara.hmily.common.config.HmilyMongoConfig;
import org.dromara.hmily.common.enums.RepositorySupportEnum;
import org.dromara.hmily.common.exception.HmilyException;
import org.dromara.hmily.common.exception.HmilyRuntimeException;
import org.dromara.hmily.common.serializer.ObjectSerializer;
import org.dromara.hmily.common.utils.AssertUtils;
import org.dromara.hmily.common.utils.LogUtil;
import org.dromara.hmily.common.utils.RepositoryPathUtils;
import org.dromara.hmily.core.spi.HmilyCoordinatorRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.MongoClientFactoryBean;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;

import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

/**
 * mongo impl.
 *
 * @author xiaoyu
 */
@HmilySPI("mongodb")
public class MongoCoordinatorRepository implements HmilyCoordinatorRepository {

    /**
     * logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(MongoCoordinatorRepository.class);

    private ObjectSerializer objectSerializer;

    private MongoTemplate template;

    private String collectionName;

    @Override
    public int create(final HmilyTransaction hmilyTransaction) {
        try {
            MongoAdapter mongoBean = new MongoAdapter();
            mongoBean.setTransId(hmilyTransaction.getTransId());
            mongoBean.setCreateTime(hmilyTransaction.getCreateTime());
            mongoBean.setLastTime(hmilyTransaction.getLastTime());
            mongoBean.setRetriedCount(hmilyTransaction.getRetriedCount());
            mongoBean.setStatus(hmilyTransaction.getStatus());
            mongoBean.setRole(hmilyTransaction.getRole());
            mongoBean.setPattern(hmilyTransaction.getPattern());
            mongoBean.setTargetClass(hmilyTransaction.getTargetClass());
            mongoBean.setTargetMethod(hmilyTransaction.getTargetMethod());
            mongoBean.setConfirmMethod(hmilyTransaction.getConfirmMethod());
            mongoBean.setCancelMethod(hmilyTransaction.getCancelMethod());
            final byte[] cache = objectSerializer.serialize(hmilyTransaction.getHmilyParticipants());
            mongoBean.setContents(cache);
            template.save(mongoBean, collectionName);
        } catch (HmilyException e) {
            e.printStackTrace();
        }
        return ROWS;
    }

    @Override
    public int remove(final String id) {
        AssertUtils.notNull(id);
        Query query = new Query();
        query.addCriteria(new Criteria("transId").is(id));
        template.remove(query, collectionName);
        return ROWS;
    }

    @Override
    public int update(final HmilyTransaction hmilyTransaction) throws HmilyRuntimeException {
        Query query = new Query();
        query.addCriteria(new Criteria("transId").is(hmilyTransaction.getTransId()));
        Update update = new Update();
        update.set("lastTime", new Date());
        update.set("retriedCount", hmilyTransaction.getRetriedCount());
        update.set("version", hmilyTransaction.getVersion() + 1);
        try {
            if (CollectionUtils.isNotEmpty(hmilyTransaction.getHmilyParticipants())) {
                update.set("contents", objectSerializer.serialize(hmilyTransaction.getHmilyParticipants()));
            }
        } catch (HmilyException e) {
            e.printStackTrace();
        }
        final UpdateResult updateResult = template.updateFirst(query, update, MongoAdapter.class, collectionName);

        if (updateResult.getModifiedCount() <= 0) {
            throw new HmilyRuntimeException("update data exception!");
        }
        return ROWS;
    }

    @Override
    public int updateParticipant(final HmilyTransaction hmilyTransaction) {
        Query query = new Query();
        query.addCriteria(new Criteria("transId").is(hmilyTransaction.getTransId()));
        Update update = new Update();
        try {
            update.set("contents", objectSerializer.serialize(hmilyTransaction.getHmilyParticipants()));
        } catch (HmilyException e) {
            e.printStackTrace();
        }
        final UpdateResult updateResult = template.updateFirst(query, update, MongoAdapter.class, collectionName);
        if (updateResult.getModifiedCount() <= 0) {
            throw new HmilyRuntimeException("update data exception!");
        }
        return ROWS;
    }

    @Override
    public int updateStatus(final String id, final Integer status) {
        Query query = new Query();
        query.addCriteria(new Criteria("transId").is(id));
        Update update = new Update();
        update.set("status", status);
        final UpdateResult updateResult = template.updateFirst(query, update, MongoAdapter.class, collectionName);
        if (updateResult.getModifiedCount() <= 0) {
            throw new HmilyRuntimeException("update data exception!");
        }
        return ROWS;
    }

    @Override
    public HmilyTransaction findById(final String id) {
        Query query = new Query();
        query.addCriteria(new Criteria("transId").is(id));
        MongoAdapter cache = template.findOne(query, MongoAdapter.class, collectionName);
        return buildByCache(Objects.requireNonNull(cache));
    }

    @SuppressWarnings("unchecked")
    private HmilyTransaction buildByCache(final MongoAdapter cache) {
        try {
            HmilyTransaction hmilyTransaction = new HmilyTransaction();
            hmilyTransaction.setTransId(cache.getTransId());
            hmilyTransaction.setCreateTime(cache.getCreateTime());
            hmilyTransaction.setLastTime(cache.getLastTime());
            hmilyTransaction.setRetriedCount(cache.getRetriedCount());
            hmilyTransaction.setVersion(cache.getVersion());
            hmilyTransaction.setStatus(cache.getStatus());
            hmilyTransaction.setRole(cache.getRole());
            hmilyTransaction.setPattern(cache.getPattern());
            hmilyTransaction.setTargetClass(cache.getTargetClass());
            hmilyTransaction.setTargetMethod(cache.getTargetMethod());
            List<HmilyParticipant> hmilyParticipants = (List<HmilyParticipant>) objectSerializer.deSerialize(cache.getContents(), CopyOnWriteArrayList.class);
            hmilyTransaction.setHmilyParticipants(hmilyParticipants);
            return hmilyTransaction;
        } catch (HmilyException e) {
            LogUtil.error(LOGGER, "mongodb deSerialize exception:{}", e::getLocalizedMessage);
            return null;
        }

    }

    @Override
    public List<HmilyTransaction> listAll() {
        final List<MongoAdapter> resultList = template.findAll(MongoAdapter.class, collectionName);
        if (CollectionUtils.isNotEmpty(resultList)) {
            return resultList.stream().map(this::buildByCache).collect(Collectors.toList());
        }
        return Collections.emptyList();
    }

    @Override
    public List<HmilyTransaction> listAllByDelay(final Date date) {
        Query query = new Query();
        query.addCriteria(Criteria.where("lastTime").lt(date));
        final List<MongoAdapter> mongoBeans =
                template.find(query, MongoAdapter.class, collectionName);
        if (CollectionUtils.isNotEmpty(mongoBeans)) {
            return mongoBeans.stream().map(this::buildByCache).collect(Collectors.toList());
        }
        return Collections.emptyList();
    }

    @Override
    public void init(final String modelName, final HmilyConfig hmilyConfig) {
        collectionName = RepositoryPathUtils.buildMongoTableName(modelName);
        final HmilyMongoConfig hmilyMongoConfig = hmilyConfig.getHmilyMongoConfig();
        MongoClientFactoryBean clientFactoryBean = buildMongoClientFactoryBean(hmilyMongoConfig);
        try {
            clientFactoryBean.afterPropertiesSet();
            template = new MongoTemplate(Objects.requireNonNull(clientFactoryBean.getObject()), hmilyMongoConfig.getMongoDbName());
        } catch (Exception e) {
            LogUtil.error(LOGGER, "mongo init error please check you config:{}", e::getMessage);
            throw new HmilyRuntimeException(e);
        }
    }

    private MongoClientFactoryBean buildMongoClientFactoryBean(final HmilyMongoConfig hmilyMongoConfig) {
        MongoClientFactoryBean clientFactoryBean = new MongoClientFactoryBean();
        MongoCredential credential = MongoCredential.createScramSha1Credential(hmilyMongoConfig.getMongoUserName(),
                hmilyMongoConfig.getMongoDbName(),
                hmilyMongoConfig.getMongoUserPwd().toCharArray());
        clientFactoryBean.setCredentials(new MongoCredential[]{credential});

        List<String> urls = Lists.newArrayList(Splitter.on(",").trimResults().split(hmilyMongoConfig.getMongoDbUrl()));
        ServerAddress[] sds = new ServerAddress[urls.size()];
        for (int i = 0; i < sds.length; i++) {
            List<String> adds = Lists.newArrayList(Splitter.on(":").trimResults().split(urls.get(i)));
            InetSocketAddress address = new InetSocketAddress(adds.get(0), Integer.parseInt(adds.get(1)));
            sds[i] = new ServerAddress(address);
        }
        clientFactoryBean.setReplicaSetSeeds(sds);
        return clientFactoryBean;
    }

    @Override
    public String getScheme() {
        return RepositorySupportEnum.MONGODB.getSupport();
    }

    @Override
    public void setSerializer(final ObjectSerializer objectSerializer) {
        this.objectSerializer = objectSerializer;
    }
}