package com.ucar.datalink.flinker.plugin.reader.hbasereader98.util;

import com.ucar.datalink.flinker.api.exception.DataXException;
import com.ucar.datalink.flinker.api.util.Configuration;
import com.ucar.datalink.flinker.plugin.reader.hbasereader98.*;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.util.Bytes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public final class HbaseUtil {
    private static Logger LOG = LoggerFactory.getLogger(HbaseUtil.class);

    private static final String META_SCANNER_CACHING = "100";

    private static final int TETRAD_TYPE_COUNT = 4;

    public static void doPretreatment(Configuration originalConfig) {
        originalConfig.getNecessaryValue(Key.HBASE_CONFIG,
                HbaseReaderErrorCode.REQUIRED_VALUE);

        originalConfig.getNecessaryValue(Key.TABLE, HbaseReaderErrorCode.REQUIRED_VALUE);

        String mode = HbaseUtil.dealMode(originalConfig);
        originalConfig.set(Key.MODE, mode);

        String encoding = originalConfig.getString(Key.ENCODING, "utf-8");
        if (!Charset.isSupported(encoding)) {
            throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, String.format("Hbasereader 不支持您所配置的编码:[%s]", encoding));
        }
        originalConfig.set(Key.ENCODING, encoding);

        // 此处增强一个检查:isBinaryRowkey 配置不能出现在与 hbaseConfig 等配置平级地位
        Boolean isBinaryRowkey = originalConfig.getBool(Key.IS_BINARY_ROWKEY);
        if (isBinaryRowkey != null) {
            throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, String.format("%s 不能配置在此处,它应该配置在 range 里面.", Key.IS_BINARY_ROWKEY));
        }

        // 处理 range 的配置,将 range 内的配置值提取到与 hbaseConfig 等配置项平级地位,方便后续获取值
        boolean hasConfiguredRange = false;
        String startRowkey = originalConfig.getString(Constant.RANGE + "." + Key.START_ROWKEY);

        //此处判断需要谨慎:如果有 key range.startRowkey 但是没有值,得到的 startRowkey 是空字符串,而不是 null
        if (startRowkey != null && startRowkey.length() != 0) {
            hasConfiguredRange = true;
            originalConfig.set(Key.START_ROWKEY, startRowkey);
        }

        String endRowkey = originalConfig.getString(Constant.RANGE + "." + Key.END_ROWKEY);
        //此处判断需要谨慎:如果有 key range.endRowkey 但是没有值,得到的 endRowkey 是空字符串,而不是 null
        if (endRowkey != null && endRowkey.length() != 0) {
            hasConfiguredRange = true;
            originalConfig.set(Key.END_ROWKEY, endRowkey);
        }

        // 如果配置了 range, 就必须要配置 isBinaryRowkey
        if (hasConfiguredRange) {
            isBinaryRowkey = originalConfig.getBool(Constant.RANGE + "." + Key.IS_BINARY_ROWKEY);
            if (isBinaryRowkey == null) {
                throw DataXException.asDataXException(HbaseReaderErrorCode.REQUIRED_VALUE, "您需要在 range 内配置 isBinaryRowkey 项,用于告诉 DataX 把您填写的 rowkey 转换为内部的二进制时,采用那个 API(值为 true 时,采用Bytes.toBytesBinary(String rowKey),值为 false 时,采用Bytes.toBytes(String rowKey))");
            }

            originalConfig.set(Key.IS_BINARY_ROWKEY, isBinaryRowkey);
        }
    }

    /**
     * 对模式以及与模式进行配对的配置进行检查
     */
    private static String dealMode(Configuration originalConfig) {
        String mode = originalConfig.getString(Key.MODE);

        ModeType modeType = ModeType.getByTypeName(mode);
        switch (modeType) {
            case Normal: {
                // normal 模式不需要配置 maxVersion,需要配置 column,并且 column 格式为 Map 风格
                String maxVersion = originalConfig.getString(Key.MAX_VERSION);
                Validate.isTrue(maxVersion == null, "您配置的是 normal 模式读取 hbase 中的数据,所以不能配置无关项:maxVersion");

                List<Map> column = originalConfig.getList(Key.COLUMN, Map.class);

                if (column == null || column.isEmpty()) {
                    throw DataXException.asDataXException(HbaseReaderErrorCode.REQUIRED_VALUE, "您配置的是 normal 模式读取 hbase 中的数据,所以必须配置 column,其形式为:column:[{\"name\": \"cf0:column0\",\"type\": \"string\"},{\"name\": \"cf1:column1\",\"type\": \"long\"}]");
                }

                // 通过 parse 进行 column 格式的进一步检查
                HbaseUtil.parseColumnOfNormalMode(column);
                break;
            }
            case MultiVersionFixedColumn: {
                // multiVersionFixedColumn 模式需要配置 maxVersion 和 column,并且 column 格式为 List 风格
                checkMaxVersion(originalConfig, mode);

                checkTetradType(originalConfig, mode);

                List<String> columns = originalConfig.getList(Key.COLUMN, String.class);
                if (columns == null || columns.isEmpty()) {
                    throw DataXException.asDataXException(HbaseReaderErrorCode.REQUIRED_VALUE, "您配置的是 multiVersionFixedColumn 模式读取 hbase 中的数据,所以必须配置 column,其形式为: column:[\"cf0:column0\",\"cf1:column1\"]");
                }

                // 检查配置的 column 格式是否包含cf:qualifier
                for (String column : columns) {
                    if (StringUtils.isBlank(column) || column.split(":").length != 2) {
                        throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, String.format("您配置的是 multiVersionFixedColumn 模式读取 hbase 中的数据,但是您配置的列格式[%s]不正确,每一个列元素应该配置为 列族:列名  的形式, 如 column:[\"cf0:column0\",\"cf1:column1\"]", column));
                    }
                }

                // 检查多版本固定列读取时,不能配置 columnFamily
                List<String> columnFamilies = originalConfig.getList(Key.COLUMN_FAMILY, String.class);
                if (columnFamilies != null) {
                    throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, "您配置的是 multiVersionFixedColumn 模式读取 hbase 中的数据,所以不能配置 columnFamily");
                }

                break;
            }
            case MultiVersionDynamicColumn: {
                // multiVersionDynamicColumn 模式需要配置 maxVersion 和 columnFamily,并且 columnFamily 格式为 List 风格
                checkMaxVersion(originalConfig, mode);

                checkTetradType(originalConfig, mode);

                List<String> columnFamilies = originalConfig.getList(Key.COLUMN_FAMILY, String.class);
                if (columnFamilies == null || columnFamilies.isEmpty()) {
                    throw DataXException.asDataXException(HbaseReaderErrorCode.REQUIRED_VALUE, "您配置的是 multiVersionDynamicColumn 模式读取 hbase 中的数据,所以必须配置 columnFamily,其形式为:columnFamily:[\"cf0\",\"cf1\"]");
                }

                // 检查多版本动态列读取时,不能配置 column
                List<String> columns = originalConfig.getList(Key.COLUMN, String.class);
                if (columns != null) {
                    throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, "您配置的是 multiVersionDynamicColumn 模式读取 hbase 中的数据,所以不能配置 column");
                }

                break;
            }
            default:
                throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, "Hbasereader 不支持此类模式:" + modeType);
        }

        return mode;
    }

    // 检查 maxVersion 是否存在,并且值是否合法
    private static void checkMaxVersion(Configuration configuration, String mode) {
        Integer maxVersion = configuration.getInt(Key.MAX_VERSION);
        Validate.notNull(maxVersion, String.format("您配置的是 %s 模式读取 hbase 中的数据,所以必须配置:maxVersion", mode));

        boolean isMaxVersionValid = maxVersion == -1 || maxVersion > 1;
        Validate.isTrue(isMaxVersionValid, String.format("您配置的是 %s 模式读取 hbase 中的数据,但是配置的 maxVersion 值错误. maxVersion规定:-1为读取全部版本,不能配置为0或者1(因为0或者1,我们认为用户是想用 normal 模式读取数据,而非 %s 模式读取,二者差别大),大于1则表示读取最新的对应个数的版本", mode, mode));
    }

    public static org.apache.hadoop.conf.Configuration getHbaseConf(String hbaseConf) {
        if (StringUtils.isBlank(hbaseConf)) {
            throw DataXException.asDataXException(HbaseReaderErrorCode.REQUIRED_VALUE, "读 Hbase 时需要配置 hbaseConfig,其内容为 Hbase 连接信息,请联系 Hbase PE 获取该信息.");
        }
        org.apache.hadoop.conf.Configuration conf = new org.apache.hadoop.conf.Configuration();

        try {
            Map<String, String> map = JSON.parseObject(hbaseConf,
                    new TypeReference<Map<String, String>>() {
                    });

            // / 用户配置的 key-value 对 来表示 hbaseConf
            Validate.isTrue(map != null, "hbaseConfig 不能为空 Map 结构!");
            for (Map.Entry<String, String> entry : map.entrySet()) {
                conf.set(entry.getKey(), entry.getValue());
            }
            return conf;
        } catch (Exception e) {
            // 用户配置的 hbase 配置文件路径
            LOG.warn("尝试把您配置的 hbaseConfig: {} \n 当成 json 解析时遇到错误:", e);
            LOG.warn("现在尝试把您配置的 hbaseConfig: {} \n 当成文件路径进行解析.", hbaseConf);
            conf.addResource(new Path(hbaseConf));

            LOG.warn("您配置的 hbaseConfig 是文件路径, 是不推荐的行为:因为当您的这个任务迁移到其他机器运行时,很可能出现该路径不存在的错误. 建议您把此项配置改成标准的 Hbase 连接信息,请联系 Hbase PE 获取该信息.");
            return conf;
        }
    }

    private static void checkTetradType(Configuration configuration, String mode) {
        List<String> userConfiguredTetradTypes = configuration.getList(Key.TETRAD_TYPE, String.class);
        if (userConfiguredTetradTypes == null || userConfiguredTetradTypes.isEmpty()) {
            throw DataXException.asDataXException(HbaseReaderErrorCode.REQUIRED_VALUE, String.format("您配置的是 %s 模式读取 hbase 中的数据,但是缺失了 tetradType 配置项. tetradType规定:是长度为 4 的数组,用于指定四元组读取的类型,如:tetradType:[\"bytes\",\"string\",\"long\",\"bytes\"]", mode));
        }

        if (userConfiguredTetradTypes.size() != TETRAD_TYPE_COUNT) {
            throw DataXException.asDataXException(HbaseReaderErrorCode.REQUIRED_VALUE, String.format("您配置的是 %s 模式读取 hbase 中的数据,但是 tetradType 配置项元素个数错误. tetradType规定:是长度为 4 的数组,用于指定四元组读取的类型,如:tetradType:[\"bytes\",\"string\",\"long\",\"bytes\"]", mode));
        }


        String rowkeyType = userConfiguredTetradTypes.get(0);
        String columnNameType = userConfiguredTetradTypes.get(1);
        String timestampType = userConfiguredTetradTypes.get(2);
        String valueType = userConfiguredTetradTypes.get(3);

        ColumnType.getByTypeName(rowkeyType);
        ColumnType.getByTypeName(columnNameType);

        if (!"long".equalsIgnoreCase(timestampType)) {
            throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, String.format("您配置的是 %s 模式读取 hbase 中的数据,但是 tetradType 配置项元素类型错误. tetradType规定:第三项描述 timestamp 类型只能为 long,而您配置的值是:[%s]", mode, timestampType));
        }

        if ("date".equalsIgnoreCase(rowkeyType) || "date".equalsIgnoreCase(columnNameType)
                || "date".equalsIgnoreCase(valueType)) {
            throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, String.format("您配置的是 %s 模式读取 hbase 中的数据,但是 tetradType 配置项元素类型错误. tetradType规定:不支持 date 类型", mode));
        }

        ColumnType.getByTypeName(valueType);
    }

    public static byte[] convertUserStartRowkey(Configuration configuration) {
        String startRowkey = configuration.getString(Key.START_ROWKEY);
        if (StringUtils.isBlank(startRowkey)) {
            return HConstants.EMPTY_BYTE_ARRAY;
        } else {
            boolean isBinaryRowkey = configuration.getBool(Key.IS_BINARY_ROWKEY);
            return HbaseUtil.stringToBytes(startRowkey, isBinaryRowkey);
        }
    }

    public static byte[] convertUserEndRowkey(Configuration configuration) {
        String endRowkey = configuration.getString(Key.END_ROWKEY);
        if (StringUtils.isBlank(endRowkey)) {
            return HConstants.EMPTY_BYTE_ARRAY;
        } else {
            boolean isBinaryRowkey = configuration.getBool(Key.IS_BINARY_ROWKEY);
            return HbaseUtil.stringToBytes(endRowkey, isBinaryRowkey);
        }
    }

    /**
     * 注意:convertUserStartRowkey 和 convertInnerStartRowkey,前者会受到 isBinaryRowkey 的影响,只用于第一次对用户配置的 String 类型的 rowkey 转为二进制时使用。而后者约定:切分时得到的二进制的 rowkey 回填到配置中时采用
     */
    public static byte[] convertInnerStartRowkey(Configuration configuration) {
        String startRowkey = configuration.getString(Key.START_ROWKEY);
        if (StringUtils.isBlank(startRowkey)) {
            return HConstants.EMPTY_BYTE_ARRAY;
        }

        return Bytes.toBytesBinary(startRowkey);
    }

    public static byte[] convertInnerEndRowkey(Configuration configuration) {
        String endRowkey = configuration.getString(Key.END_ROWKEY);
        if (StringUtils.isBlank(endRowkey)) {
            return HConstants.EMPTY_BYTE_ARRAY;
        }

        return Bytes.toBytesBinary(endRowkey);
    }

    /**
     * 每次都获取一个新的HTable  注意:HTable 本身是线程不安全的
     */
    public static HTable initHtable(Configuration configuration) {
        String hbaseConnConf = configuration.getString(Key.HBASE_CONFIG);
        String tableName = configuration.getString(Key.TABLE);
        HBaseAdmin admin = null;
        try {
            org.apache.hadoop.conf.Configuration conf = HbaseUtil.getHbaseConf(hbaseConnConf);
            conf.set("hbase.meta.scanner.caching", META_SCANNER_CACHING);

            HTable htable = HTableManager.createHTable(conf, tableName);
            admin = HTableManager.createHBaseAdmin(conf);
            check(admin, htable);

            return htable;
        } catch (Exception e) {
            throw DataXException.asDataXException(HbaseReaderErrorCode.INIT_TABLE_ERROR, e);
        } finally {
            if (admin != null) {
                try {
                    admin.close();
                } catch (IOException e) {
                    // ignore it
                }
            }
        }
    }


    private static void check(HBaseAdmin admin, HTable htable) throws DataXException, IOException {
        if (!admin.isMasterRunning()) {
            throw new IllegalStateException("HBase master 没有运行, 请检查您的配置 或者 联系 Hbase 管理员.");
        }
        if (!admin.tableExists(htable.getTableName())) {
            throw new IllegalStateException("HBase源头表" + Bytes.toString(htable.getTableName())
                    + "不存在, 请检查您的配置 或者 联系 Hbase 管理员.");
        }
        if (!admin.isTableAvailable(htable.getTableName()) || !admin.isTableEnabled(htable.getTableName())) {
            throw new IllegalStateException("HBase源头表" + Bytes.toString(htable.getTableName())
                    + " 不可用, 请检查您的配置 或者 联系 Hbase 管理员.");
        }
    }

    private static byte[] stringToBytes(String rowkey, boolean isBinaryRowkey) {
        if (isBinaryRowkey) {
            return Bytes.toBytesBinary(rowkey);
        } else {
            return Bytes.toBytes(rowkey);
        }
    }

    public static boolean isRowkeyColumn(String columnName) {
        return Constant.ROWKEY_FLAG.equalsIgnoreCase(columnName);
    }

    /**
     * 用于解析 Normal 模式下的列配置
     */
    public static List<HbaseColumnCell> parseColumnOfNormalMode(List<Map> column) {
        List<HbaseColumnCell> hbaseColumnCells = new ArrayList<HbaseColumnCell>();

        HbaseColumnCell oneColumnCell;

        for (Map<String, String> aColumn : column) {
            ColumnType type = ColumnType.getByTypeName(aColumn.get("type"));
            String columnName = aColumn.get("name");
            String columnValue = aColumn.get("value");
            String dateformat = aColumn.get("format");

            if (type == ColumnType.DATE) {
                Validate.notNull(dateformat, "Hbasereader 在 normal 方式读取时,其列配置中,如果类型为时间,则必须指定时间格式. 形如:yyyy-MM-dd HH:mm:ss,特别注意不能随意小写时间格式,那样可能导致时间转换错误!");

                Validate.isTrue(StringUtils.isNotBlank(columnName) || StringUtils.isNotBlank(columnValue), "Hbasereader 在 normal 方式读取时则要么是 type + name + format 的组合,要么是type + value + format 的组合. 而您的配置非这两种组合,请检查并修改.");

                oneColumnCell = new HbaseColumnCell
                        .Builder(type)
                        .columnName(columnName)
                        .columnValue(columnValue)
                        .dateformat(dateformat)
                        .build();
            } else {
                Validate.isTrue(dateformat == null, "Hbasereader 在 normal 方式读取时, 其列配置中,如果类型不为时间,则不需要指定时间格式.");

                Validate.isTrue(StringUtils.isNotBlank(columnName) || StringUtils.isNotBlank(columnValue), "Hbasereader 在 normal 方式读取时,其列配置中,如果类型不是时间,则要么是 type + name 的组合,要么是type + value 的组合. 而您的配置非这两种组合,请检查并修改.");

                oneColumnCell = new HbaseColumnCell
                        .Builder(type)
                        .columnName(columnName)
                        .columnValue(columnValue)
                        .build();
            }

            hbaseColumnCells.add(oneColumnCell);
        }

        return hbaseColumnCells;
    }
}