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

package com.actiontech.dble.services.manager.response;

import com.actiontech.dble.backend.mysql.PacketUtil;
import com.actiontech.dble.btrace.provider.StatisticProvider;
import com.actiontech.dble.config.ErrorCode;
import com.actiontech.dble.config.Fields;
import com.actiontech.dble.config.model.SystemConfig;
import com.actiontech.dble.memory.unsafe.utils.JavaUtils;
import com.actiontech.dble.net.mysql.*;
import com.actiontech.dble.route.parser.util.Pair;
import com.actiontech.dble.services.manager.ManagerService;
import com.actiontech.dble.services.manager.handler.WriteDynamicBootstrap;
import com.actiontech.dble.services.manager.information.ManagerSchemaInfo;
import com.actiontech.dble.services.manager.information.tables.statistic.AssociateTablesByEntryByUser;
import com.actiontech.dble.services.manager.information.tables.statistic.FrontendByBackendByEntryByUser;
import com.actiontech.dble.services.manager.information.tables.statistic.SqlLog;
import com.actiontech.dble.services.manager.information.tables.statistic.TableByUserByEntry;
import com.actiontech.dble.statistic.sql.StatisticManager;
import com.actiontech.dble.statistic.sql.UsageData;
import com.actiontech.dble.statistic.sql.UsageDataBlock;
import com.actiontech.dble.util.DateUtil;
import com.actiontech.dble.util.StringUtil;
import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.ByteBuffer;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class StatisticCf {
    private static final Logger LOGGER = LoggerFactory.getLogger(StatisticCf.class);
    private static final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();

    public static class OnOff {
        public OnOff() {
        }

        public static void execute(ManagerService service, boolean isOn) {
            LOCK.writeLock().lock();
            try {
                StatisticProvider.onOffStatistic();
                String onOffStatus = isOn ? "enable" : "disable";
                try {
                    WriteDynamicBootstrap.getInstance().changeValue("enableStatistic", isOn ? "1" : "0");
                } catch (Exception ex) {
                    LOGGER.warn("rollback enableStatistic failed, exception：", ex);
                    service.writeErrMessage(ErrorCode.ER_YES, onOffStatus + " enableStatistic failed");
                    return;
                }
                StatisticManager.getInstance().setEnable(isOn);
                OkPacket ok = new OkPacket();
                ok.setPacketId(1);
                ok.setAffectedRows(1);
                ok.setServerStatus(2);
                ok.write(service.getConnection());
            } finally {
                LOCK.writeLock().unlock();
            }
        }
    }

    public static class SamplingSwitch {
        public SamplingSwitch() {
        }

        public static void execute(ManagerService service, int samplingRate) {
            if (samplingRate < 0 || samplingRate > 100) {
                service.writeErrMessage(ErrorCode.ER_YES, "value of samplingRate is incorrect, the value is integer between 0 and 100.");
                return;
            }

            try {
                WriteDynamicBootstrap.getInstance().changeValue("samplingRate", samplingRate + "");
            } catch (Exception ex) {
                LOGGER.warn("write samplingRate error", ex);
                service.writeErrMessage(ErrorCode.ER_YES, "fail to reload samplingRate");
                return;
            }

            LOCK.writeLock().lock();
            try {
                StatisticManager.getInstance().setSamplingRate(samplingRate);
            } finally {
                LOCK.writeLock().unlock();
            }

            service.writeOkPacket();
        }
    }

    public static class SetTableMaxSize {
        // reload @@statistic_table_size = 96
        //reload @@statistic_table_size = 96 where table=table1;
        //reload @@statistic_table_size = 96 where table in(schema1.table1,...)
        static final Pattern PATTERN_IN = Pattern.compile("^([^\\s]+)(\\s+where\\s+table\\s+in\\s*\\(([^\\s]+)\\))*", Pattern.CASE_INSENSITIVE);
        static final Pattern PATTERN_EQUAL = Pattern.compile("^([^\\s]+)(\\s+where\\s+table\\s*=\\s*'([^\\s]+)')*", Pattern.CASE_INSENSITIVE);
        static final Map<String, String> STATISTIC_TABLES = new HashMap<>(8);

        static {
            STATISTIC_TABLES.put(AssociateTablesByEntryByUser.TABLE_NAME, "associateTablesByEntryByUserTableSize");
            STATISTIC_TABLES.put(FrontendByBackendByEntryByUser.TABLE_NAME, "frontendByBackendByEntryByUserTableSize");
            STATISTIC_TABLES.put(TableByUserByEntry.TABLE_NAME, "tableByUserByEntryTableSize");
            STATISTIC_TABLES.put(SqlLog.TABLE_NAME, "sqlLogTableSize");
        }

        public static void execute(ManagerService service, String value) {
            LOCK.writeLock().lock();
            try {
                StatisticProvider.updateTableMaxSize();

                Matcher matcher1 = PATTERN_IN.matcher(value);
                Matcher matcher2 = PATTERN_EQUAL.matcher(value);
                int size = 0;
                boolean haveCondition = false;
                String tableStr = null;
                try {
                    if (matcher1.matches()) {
                        size = Integer.parseInt(matcher1.group(1));
                        if ((tableStr = matcher1.group(3)) != null) {
                            haveCondition = true;
                        }
                    } else if (matcher2.matches()) {
                        size = Integer.parseInt(matcher2.group(1));
                        if ((tableStr = matcher2.group(3)) != null) {
                            haveCondition = true;
                        }
                    } else {
                        service.writeErrMessage(ErrorCode.ER_YES, "Usage: reload @@statistic_table_size = 1024 [where table='?' | where table in (dble_information.tableA,...)]");
                        return;
                    }
                } catch (NumberFormatException e) {
                    service.writeErrMessage(ErrorCode.ER_UNKNOWN_ERROR, "tableSize setting is not correct");
                    return;
                }
                if (size < 1) {
                    service.writeErrMessage(ErrorCode.ER_YES, "tableSize must be greater than 0");
                    return;
                }

                Set<String> tables;
                if (haveCondition) {
                    tables = new HashSet<>();
                    String[] tableArray = StringUtil.removeApostropheOrBackQuote(tableStr).split(",");
                    for (String table : tableArray) {
                        String[] arr = table.split("\\.");
                        if (arr.length == 1) {
                            String tableTmp;
                            if (!STATISTIC_TABLES.containsKey((tableTmp = StringUtil.removeApostropheOrBackQuote(arr[0]).toLowerCase()))) {
                                service.writeErrMessage(ErrorCode.ER_YES, "Table `" + ManagerSchemaInfo.SCHEMA_NAME + "`.`" + arr[0] + "` don't belong to statistic tables");
                                return;
                            } else {
                                tables.add(tableTmp);
                            }
                        } else if (arr.length == 2) {
                            String tableTmp;
                            if (!StringUtil.removeApostropheOrBackQuote(arr[0]).equalsIgnoreCase(ManagerSchemaInfo.SCHEMA_NAME) ||
                                    !STATISTIC_TABLES.containsKey((tableTmp = StringUtil.removeApostropheOrBackQuote(arr[1])))) {
                                service.writeErrMessage(ErrorCode.ER_YES, "Table `" + arr[0] + "`.`" + arr[1] + "` don't belong to statistic tables");
                                return;
                            } else {
                                tables.add(tableTmp);
                            }
                        } else {
                            service.writeErrMessage(ErrorCode.ER_YES, "Please check table name is correct");
                            return;
                        }
                    }
                } else {
                    tables = new HashSet<>(STATISTIC_TABLES.keySet());
                }

                try {
                    List<Pair<String, String>> props = new ArrayList<>();
                    for (String t : tables) {
                        props.add(new Pair<>(STATISTIC_TABLES.get(t), String.valueOf(size)));
                    }
                    WriteDynamicBootstrap.getInstance().changeValue(props);
                } catch (Exception ex) {
                    LOGGER.warn("set statistic table size failed", ex);
                    service.writeErrMessage(ErrorCode.ER_YES, "set statistic table size failed");
                    return;
                }
                for (String t : tables) {
                    switch (t.toLowerCase()) {
                        case AssociateTablesByEntryByUser.TABLE_NAME:
                            StatisticManager.getInstance().setAssociateTablesByEntryByUserTableSize(size);
                            break;
                        case FrontendByBackendByEntryByUser.TABLE_NAME:
                            StatisticManager.getInstance().setFrontendByBackendByEntryByUserTableSize(size);
                            break;
                        case TableByUserByEntry.TABLE_NAME:
                            StatisticManager.getInstance().setTableByUserByEntryTableSize(size);
                            break;
                        case SqlLog.TABLE_NAME:
                            StatisticManager.getInstance().setSqlLogSize(size);
                            break;
                        default:
                            break;
                    }
                    /*Method method = StatisticManager.getInstance().getClass().getMethod("",new Class[]{int.class});
                       method.invoke(StatisticManager.getInstance(),size);*/
                }

                OkPacket ok = new OkPacket();
                ok.setPacketId(1);
                ok.setAffectedRows(1);
                ok.setServerStatus(2);
                ok.write(service.getConnection());
            } finally {
                LOCK.writeLock().unlock();
            }
        }
    }

    public static class Show {

        public Show() {
        }

        private static final int FIELD_COUNT = 2;
        private static final ResultSetHeaderPacket HEADER = PacketUtil.getHeader(FIELD_COUNT);
        private static final FieldPacket[] FIELDS = new FieldPacket[FIELD_COUNT];
        private static final EOFPacket EOF = new EOFPacket();

        static {
            int i = 0;
            byte packetId = 0;
            HEADER.setPacketId(++packetId);

            FIELDS[i] = PacketUtil.getField("NAME", Fields.FIELD_TYPE_VAR_STRING);
            FIELDS[i++].setPacketId(++packetId);

            FIELDS[i] = PacketUtil.getField("VALUE", Fields.FIELD_TYPE_VAR_STRING);
            FIELDS[i].setPacketId(++packetId);

            EOF.setPacketId(++packetId);
        }

        public static void execute(ManagerService service) {
            LOCK.readLock().lock();
            try {
                StatisticProvider.showStatistic();
                ByteBuffer buffer = service.allocate();
                // write header
                buffer = HEADER.write(buffer, service, true);

                // write fields
                for (FieldPacket field : FIELDS) {
                    buffer = field.write(buffer, service, true);
                }

                // write eof
                buffer = EOF.write(buffer, service, true);

                // write rows
                byte packetId = EOF.getPacketId();

                RowDataPacket row1 = new RowDataPacket(FIELD_COUNT);
                row1.add(StringUtil.encode("statistic", service.getCharset().getResults()));
                row1.add(StringUtil.encode(StatisticManager.getInstance().isEnable() ? "ON" : "OFF", service.getCharset().getResults()));
                row1.setPacketId(++packetId);
                buffer = row1.write(buffer, service, true);

                RowDataPacket row2 = new RowDataPacket(FIELD_COUNT);
                row2.add(StringUtil.encode("associateTablesByEntryByUserTableSize", service.getCharset().getResults()));
                row2.add(StringUtil.encode(String.valueOf(StatisticManager.getInstance().getAssociateTablesByEntryByUserTableSize()), service.getCharset().getResults()));
                row2.setPacketId(++packetId);
                buffer = row2.write(buffer, service, true);

                RowDataPacket row3 = new RowDataPacket(FIELD_COUNT);
                row3.add(StringUtil.encode("frontendByBackendByEntryByUserTableSize", service.getCharset().getResults()));
                row3.add(StringUtil.encode(String.valueOf(StatisticManager.getInstance().getFrontendByBackendByEntryByUserTableSize()), service.getCharset().getResults()));
                row3.setPacketId(++packetId);
                buffer = row3.write(buffer, service, true);

                RowDataPacket row4 = new RowDataPacket(FIELD_COUNT);
                row4.add(StringUtil.encode("tableByUserByEntryTableSize", service.getCharset().getResults()));
                row4.add(StringUtil.encode(String.valueOf(StatisticManager.getInstance().getTableByUserByEntryTableSize()), service.getCharset().getResults()));
                row4.setPacketId(++packetId);
                buffer = row4.write(buffer, service, true);

                RowDataPacket row5 = new RowDataPacket(FIELD_COUNT);
                row5.add(StringUtil.encode("sqlLogTableSize", service.getCharset().getResults()));
                row5.add(StringUtil.encode(String.valueOf(StatisticManager.getInstance().getSqlLogSize()), service.getCharset().getResults()));
                row5.setPacketId(++packetId);
                buffer = row5.write(buffer, service, true);

                RowDataPacket row6 = new RowDataPacket(FIELD_COUNT);
                row6.add(StringUtil.encode("samplingRate", service.getCharset().getResults()));
                row6.add(StringUtil.encode(String.valueOf(StatisticManager.getInstance().getSamplingRate()), service.getCharset().getResults()));
                row6.setPacketId(++packetId);
                buffer = row6.write(buffer, service, true);

                RowDataPacket row7 = new RowDataPacket(FIELD_COUNT);
                row7.add(StringUtil.encode("queueMonitor", service.getCharset().getResults()));
                row7.add(StringUtil.encode(StatisticManager.getInstance().isMonitoring() ? "monitoring" : "-", service.getCharset().getResults()));
                row7.setPacketId(++packetId);
                buffer = row7.write(buffer, service, true);

                // write last eof
                EOFRowPacket lastEof = new EOFRowPacket();
                lastEof.setPacketId(++packetId);

                lastEof.write(buffer, service);
            } finally {
                LOCK.readLock().unlock();
            }
        }
    }

    public static class Queue {
        private static final int FIELD_COUNT = 2;
        private static final ResultSetHeaderPacket HEADER = PacketUtil.getHeader(FIELD_COUNT);
        private static final FieldPacket[] FIELDS = new FieldPacket[FIELD_COUNT];
        private static final EOFPacket EOF = new EOFPacket();
        private static final Pattern PATTERN = Pattern.compile("(\\s+observeTime\\s*=\\s*(([^\\s])+)(\\s+and\\s+intervalTime\\s*=\\s*([^']+))?)?", Pattern.CASE_INSENSITIVE);

        private static final ImmutableMap<String, TimeUnit> TIME_SUFFIXES =
                ImmutableMap.<String, TimeUnit>builder().
                        put("s", TimeUnit.SECONDS).
                        put("m", TimeUnit.MINUTES).
                        put("min", TimeUnit.MINUTES).
                        put("h", TimeUnit.HOURS).
                        build();

        static {
            int i = 0;
            byte packetId = 0;
            HEADER.setPacketId(++packetId);

            FIELDS[i] = PacketUtil.getField("TIME", Fields.FIELD_TYPE_VAR_STRING);
            FIELDS[i++].setPacketId(++packetId);

            FIELDS[i] = PacketUtil.getField("USAGE", Fields.FIELD_TYPE_VAR_STRING);
            FIELDS[i].setPacketId(++packetId);

            EOF.setPacketId(++packetId);
        }

        // start @@statistic_queue_monitor observeTime = 0 and intervalTime = 0;
        public static void start(ManagerService service, String stmt, int offset) {
            String option = stmt.substring(offset);
            Matcher ma = PATTERN.matcher(option);
            if (!ma.matches()) {
                service.writeErrMessage(ErrorCode.ER_PARSE_ERROR, "The sql does not match: start @@statistic_queue_monitor observeTime = ? and intervalTime = ?");
                return;
            }
            long observeTime = 60000L;
            long intervalTime = 5000L;
            try {
                if (null != ma.group(2)) {
                    observeTime = JavaUtils.timeStringAsMs(ma.group(2), TIME_SUFFIXES);
                }
                if (null != ma.group(5)) {
                    intervalTime = JavaUtils.timeStringAsMs(ma.group(5), TIME_SUFFIXES);
                }
                if (observeTime < intervalTime) {
                    throw new NumberFormatException();
                }
            } catch (NumberFormatException e) {
                service.writeErrMessage(ErrorCode.ER_PARSE_ERROR, "Rule: must be a positive integer, observeTime > intervalTime, Unit: (s,m/min,h)");
                return;
            }
            if (!StatisticManager.getInstance().isEnable() && StatisticManager.getInstance().getSamplingRate() == 0) {
                service.writeErrMessage(ErrorCode.ER_YES, "Statistic is disabled and samplingRate value is 0");
                return;
            }
            if (StatisticManager.getInstance().isMonitoring()) {
                service.writeErrMessage(ErrorCode.ER_YES, "In the monitoring..., can use 'stop @@statistic_queue_monitor' to interrupt monitoring");
                return;
            }

            StatisticManager.getInstance().resetUsageData();
            Timer timer = StatisticManager.getInstance().getQueueMonitor();
            if (null == timer) {
                service.writeErrMessage(ErrorCode.ER_YES, "Check the sql statistics is disabled or samplingRate value is 0");
                return;
            }
            timer.schedule(
                    new MonitorTask(
                            observeTime,
                            intervalTime),
                    0,
                    intervalTime);
            service.writeOkPacket();
        }

        // stop @@statistic_queue_monitor;
        public static void stop(ManagerService service) {
            StatisticManager.getInstance().cancelMonitoring();
            service.writeOkPacket();
        }

        // show @@statistic_queue.usage;
        public static void show(ManagerService service) {
            ByteBuffer buffer = service.allocate();
            buffer = HEADER.write(buffer, service, true);
            for (FieldPacket field : FIELDS) {
                buffer = field.write(buffer, service, true);
            }
            buffer = EOF.write(buffer, service, true);
            byte packetId = EOF.getPacketId();
            RowDataPacket row;
            UsageData usageData;

            for (UsageDataBlock block : StatisticManager.getInstance().getUsageData()) {
                if ((usageData = block.get()) != null) {
                    row = new RowDataPacket(FIELD_COUNT);
                    row.add(StringUtil.encode(usageData.getDataTime(), service.getCharset().getResults()));
                    row.add(StringUtil.encode(usageData.getUsage(), service.getCharset().getResults()));
                    row.setPacketId(++packetId);
                    buffer = row.write(buffer, service, true);
                }
            }
            EOFRowPacket lastEof = new EOFRowPacket();
            lastEof.setPacketId(++packetId);
            lastEof.write(buffer, service);
        }

        public static void drop(ManagerService service) {
            StatisticManager.getInstance().getUsageData().clear();
            service.writeOkPacket();
        }

        public static class MonitorTask extends TimerTask {
            private static final DecimalFormat DF = new DecimalFormat("0.00%");
            private double queueSize = SystemConfig.getInstance().getStatisticQueueSize();
            private long interval;
            private long observe;
            private long count = 0L;

            public MonitorTask(long observe, long interval) {
                super();
                this.interval = interval;
                this.observe = observe;
            }

            @Override
            public void run() {
                if (count > observe) {
                    StatisticManager.getInstance().cancelMonitoring();
                    return;
                }
                StatisticManager.getInstance().
                        getUsageData().add(new UsageDataBlock(DateUtil.parseStr(System.currentTimeMillis(), DateUtil.DEFAULT_DATE_PATTERN),
                        DF.format(1 - (StatisticManager.getInstance().getDisruptorRemaining() / queueSize))));
                count += interval;
            }
        }
    }
}
