/*
 * Copyright (C) 2016-2022 ActionTech.
 * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher.
 */

package com.actiontech.dble.singleton;

import com.actiontech.dble.services.mysqlsharding.ShardingService;
import com.actiontech.dble.util.TimeUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

public final class DDLTraceHelper {

    private static final Logger LOGGER = LoggerFactory.getLogger("DDL_TRACE");
    private static final long DDL_EXECUTE_LIMIT = 30 * 60 * 60;

    // prefix
    private static final String NOTIFIED_PREFIX = "[DDL_NOTIFIED]";

    private static final DDLTraceHelper INSTANCE = new DDLTraceHelper();
    private final AtomicInteger index;
    private final Map<ShardingService, DDLTraceInfo> traceMap;

    private DDLTraceHelper() {
        index = new AtomicInteger();
        traceMap = new ConcurrentHashMap<>();
    }

    public static void init(ShardingService s, String sql) {
        DDLTraceInfo traceInfo = new DDLTraceInfo(s, sql);
        INSTANCE.getTraceMap().put(s, traceInfo);
    }

    public static void log(ShardingService s, Consumer<DDLTraceInfo> consumer) {
        try {
            if (s != null) {
                Optional.ofNullable(INSTANCE.getTraceMap().get(s)).ifPresent(consumer);
            }
        } catch (Exception e) {
            LOGGER.warn("DDL traceLog an error occurs: {}", e.getMessage());
        }
    }

    public static void log2(ShardingService s, Stage stage0, String... context0) {
        if (s != null) {
            log(s, d -> d.info(stage0, context0));
        } else {
            String context = getContext(context0);
            LOGGER.info(NOTIFIED_PREFIX + " <{}> {}", stage0, context);
        }
    }

    public static void log2(ShardingService s, Stage stage0, Status status0, String... context0) {
        if (s != null) {
            log(s, d -> d.info(stage0, status0, context0));
        } else {
            String context = getContext(context0);
            if (status0 == Status.fail) {
                LOGGER.warn(NOTIFIED_PREFIX + " <{}.{}> {}", stage0, status0, context);
            } else {
                LOGGER.info(NOTIFIED_PREFIX + " <{}.{}> {}", stage0, status0, context);
            }

        }
    }

    public static void finish(ShardingService s) {
        log(s, d -> d.end());
        INSTANCE.getTraceMap().remove(s);
    }

    public static void printDDLOutOfLimit() {
        for (Map.Entry<ShardingService, DDLTraceInfo> entry : INSTANCE.getTraceMap().entrySet()) {
            if ((TimeUtil.currentTimeMillis() - entry.getValue().getStartTime()) > DDL_EXECUTE_LIMIT) {
                LOGGER.warn("[DDL_{}] <scheduler> this ddl{{}} execute for too long.", entry.getValue().getId(), entry.getValue().getSql());
            }
        }
    }

    public AtomicInteger getIndex() {
        return index;
    }

    public Map<ShardingService, DDLTraceInfo> getTraceMap() {
        return traceMap;
    }

    private static String getContext(String... params) { // Just take the first parameter
        if (params.length > 0) {
            return params[0];
        }
        return "";
    }

    public static class DDLTraceInfo {
        private final int id;
        private final String sql;
        private final long startTime;

        private volatile String fail = null;

        public DDLTraceInfo(ShardingService s, String sql) {
            this.id = INSTANCE.getIndex().incrementAndGet();
            this.sql = sql;
            this.startTime = System.currentTimeMillis();
            LOGGER.info("================ {} [DDL_{}] ================", Stage.init_ddl_trace, this.id);
            LOGGER.info("[DDL_{}] <{}> Routes end and Start ddl{{}} execution stage. In {}", this.id, Stage.init_ddl_trace, this.sql, s.getConnection()); // TODO 希望打印前端的事务状态，如autocommit、isStartTx
        }

        public void info(Stage stage0, String... context0) {
            info0(null, stage0, null, context0);
        }

        public void info(Stage stage0, Status status0, String... context0) {
            info0(null, stage0, status0, context0);
        }

        public void infoByNode(String node0, Stage stage0, Status status0, String... context0) {
            info0(node0, stage0, status0, context0);
        }

        public void end() {
            if (fail == null) {
                LOGGER.info("[DDL_{}] <{}> Execute success", this.id, Stage.finish_ddl_trace);
            } else {
                LOGGER.warn("[DDL_{}] <{}> Execute fail, cause: {}", this.id, Stage.finish_ddl_trace, fail);
            }
            LOGGER.info("================ {} [DDL_{}] ================", Stage.finish_ddl_trace, this.id);
        }

        private void info0(String node0, Stage stage0, Status status0, String... context0) {
            String prefix;
            if (node0 == null) {
                prefix = "[DDL_" + this.id + "]";
            } else {
                prefix = "[DDL_" + this.id + "." + node0 + "]";
            }
            String context = getContext(context0);
            if (status0 == null) {
                LOGGER.info("{} <{}> {}", prefix, stage0, context);
            } else {
                if (status0 == Status.fail) {
                    this.fail = context;
                    LOGGER.warn("{} <{}.{}> {}", prefix, stage0, status0, context);
                } else {
                    LOGGER.info("{} <{}.{}> {}", prefix, stage0, status0, context);
                }
            }
        }

        public int getId() {
            return id;
        }

        public String getSql() {
            return sql;
        }

        public long getStartTime() {
            return startTime;
        }
    }

    public enum Stage {
        // self
        init_ddl_trace,
        notice_cluster_ddl_prepare,
        add_table_lock,
        test_ddl_conn,
        exec_ddl_sql,
        update_table_metadata,
        notice_cluster_ddl_complete,
        release_table_lock,
        finish_ddl_trace,

        // cluster
        receive_ddl_prepare,
        receive_ddl_complete,
    }

    public enum Status {
        start, get_conn, succ, fail
    }
}
