/*
 * Copyright (c) 2011-2014, hubin (jobob@qq.com).
 * <p>
 * Licensed 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
 * <p>
 * https://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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 cn.com.ty.lift.common.sql;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Statement;
import java.time.Duration;
import java.time.Instant;
import java.util.*;

/**
 * 性能分析拦截器，用于输出每条 SQL 语句及其执行时间
 *
 * @author hubin nieqiurong TaoYu
 * @since 2016-07-07
 */
@Slf4j
@Intercepts({
        @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
public class SqlAnalysisInterceptor implements Interceptor {

    private static final String NEWLINE                        = "\n";
    private static final String SPACE                          = " ";
    private static final String DruidPooledPreparedStatement   = "com.alibaba.druid.pool.DruidPooledPreparedStatement";
    private static final String T4CPreparedStatement           = "oracle.jdbc.driver.T4CPreparedStatement";
    private static final String OraclePreparedStatementWrapper = "oracle.jdbc.driver.OraclePreparedStatementWrapper";
    private static final String h_statement                    = "h.statement";
    private static final String stmt_statement                 = "stmt.statement";
    private static final String stmt                           = "stmt";
    private static final String delegate                       = "delegate";
    private static final String getSql                         = "getSql";
    private static final String getOriginalSql                 = "getOriginalSql";

    /**
     * SQL 执行最大时长，超过自动停止运行，有助于发现问题。
     */
    @Setter
    @Getter
    @Accessors(chain = true)
    private long    maxTime  = 0;
    /**
     * SQL 是否格式化
     */
    @Setter
    @Getter
    @Accessors(chain = true)
    private boolean format   = false;
    /**
     * 是否写入日志文件
     * <p>true 写入日志文件,慢sql->ERROR,其他sql->INFO</p>
     * <p>默认false 不写入日志文件,慢sql->system.err, 同时抛出异常,其他sql->system.out</p>
     */
    @Setter
    @Getter
    @Accessors(chain = true)
    private boolean logWrite = false;
    private Method  oracleGetOriginalSqlMethod;
    private Method  druidGetSQLMethod;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // get statement from the invocation.
        Statement statement = getStatement(invocation);
        // if statement is null, just return.
        if (SqlUtils.isNull(statement)) {
            return invocation.proceed();
        }
        // get originalSql from the statement.
        String originalSql = getOriginalSql(statement);

        if (SqlUtils.isEmpty(originalSql)) {
            return invocation.proceed();
        }
        //\s用于匹配空白字符, \\s用于匹配字符串中的\和s，两个字符
        //去掉原始sql中的格式，换行，全部格式化为一行sql
        originalSql = originalSql.replaceAll("[\\s]+", SPACE);
        int index = indexOfSqlStart(originalSql);
        if (index > 0) {
            originalSql = originalSql.substring(index);
        }

        // 计算执行 SQL 耗时
        Instant start = Instant.now();
        Object result = invocation.proceed();
        Instant end = Instant.now();
        long timing = Duration.between(start, end).toMillis();

        // 格式化 SQL 打印执行结果
        Object target = SqlUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(target);
        MappedStatement ms = SqlUtils.getMappedStatement(metaObject);
        String formatSql = NEWLINE;
        formatSql += "Consume Time：" + timing + " ms - ID：" + ms.getId() + NEWLINE;
        formatSql += "Execute SQL ：" + SqlUtils.sqlFormat(originalSql, format) + NEWLINE;
        boolean slowSql = this.getMaxTime() > 0 && timing > this.getMaxTime();
        if (this.isLogWrite()) {
            if (slowSql) {
                log.error(formatSql);
            } else {
                log.info(formatSql);
            }
        }else{
            if(slowSql){
                System.err.println(formatSql);
                throw new SqlAnalysisException("The SQL execution time is too large, please optimize !");
            }else{
                System.out.println(formatSql);
            }
        }
        return result;
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties prop) {
        String maxTime = prop.getProperty("maxTime");
        String format = prop.getProperty("format");
        String logWrite = prop.getProperty("logWrite");
        if (SqlUtils.isNumeric(maxTime)) {
            this.maxTime = Long.parseLong(maxTime);
        }
        if (SqlUtils.isBoolean(format)) {
            this.format = Boolean.valueOf(format);
        }
        if(SqlUtils.isBoolean(logWrite)){
            this.logWrite = Boolean.valueOf(logWrite);
        }
    }

    /**
     * 获取此方法名的具体 Method
     *
     * @param clazz      class 对象
     * @param methodName 方法名
     * @return 方法
     */
    private Method getMethodRegular(Class<?> clazz, String methodName) {
        if (Object.class.equals(clazz)) {
            return null;
        }
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.getName().equals(methodName)) {
                return method;
            }
        }
        return getMethodRegular(clazz.getSuperclass(), methodName);
    }

    /**
     * 获取sql语句开头部分
     *
     * @param sql ignore
     * @return ignore
     */
    private int indexOfSqlStart(String sql) {
        String upperCaseSql = sql.toUpperCase();
        Set<Integer> set = new HashSet<>();
        set.add(upperCaseSql.indexOf("SELECT "));
        set.add(upperCaseSql.indexOf("UPDATE "));
        set.add(upperCaseSql.indexOf("INSERT "));
        set.add(upperCaseSql.indexOf("DELETE "));
        set.remove(-1);
        if (SqlUtils.isEmpty(set)) {
            return -1;
        }
        List<Integer> list = new ArrayList<>(set);
        list.sort(Comparator.naturalOrder());
        return list.get(0);
    }

    /**
     * get sql {@link Statement} from the {@link Invocation}
     *
     * @param invocation the invocation inject into.
     * @return the sql statement.
     */
    private Statement getStatement(Invocation invocation) {
        Statement statement;
        Object firstArg = invocation.getArgs()[0];
        if (Proxy.isProxyClass(firstArg.getClass())) {
            statement = (Statement) SystemMetaObject.forObject(firstArg).getValue(h_statement);
        } else {
            statement = (Statement) firstArg;
        }
        MetaObject stmtMetaObject = SystemMetaObject.forObject(statement);
        if (stmtMetaObject.hasGetter(stmt)) {
            try {
                statement = (Statement) stmtMetaObject.getValue(stmt_statement);
            } catch (Exception ignored) {
                // do nothing
                log.warn("Get stmt.statement from MetaObject fail.");
            }
        }
        if (stmtMetaObject.hasGetter(delegate)) {
            //Hikari
            try {
                statement = (Statement) stmtMetaObject.getValue(delegate);
            } catch (Exception ignored) {
                // do nothing
                log.warn("Get delegate statement from MetaObject fail.");
            }
        }
        return statement;
    }

    /**
     * get Original Sql with parameter from the sql {@link Statement}
     *
     * @param statement the sql statement.
     * @return the Original Sql string.
     */
    private String getOriginalSql(Statement statement) {
        Object stmtSql = null;
        String stmtClassName = statement.getClass().getName();
        if (DruidPooledPreparedStatement.equals(stmtClassName)) {
            try {
                if (SqlUtils.isNull(druidGetSQLMethod)) {
                    Class<?> clazz = Class.forName(DruidPooledPreparedStatement);
                    druidGetSQLMethod = clazz.getMethod(getSql);
                }
                if(!druidGetSQLMethod.isAccessible()){
                    druidGetSQLMethod.setAccessible(true);
                }
                stmtSql = druidGetSQLMethod.invoke(statement);
            } catch (Exception e) {
                log.error("Get original Sql from DruidPooledPreparedStatement fail.", e);
            }
        } else if (T4CPreparedStatement.equals(stmtClassName) || OraclePreparedStatementWrapper.equals(stmtClassName)) {
            try {
                if (SqlUtils.isNull(oracleGetOriginalSqlMethod)) {
                    Class<?> clazz = Class.forName(stmtClassName);
                    oracleGetOriginalSqlMethod = getMethodRegular(clazz, getOriginalSql);
                }
                if (SqlUtils.notNull(oracleGetOriginalSqlMethod)) {
                    //OraclePreparedStatementWrapper is not a public class, need set this.
                    if(!oracleGetOriginalSqlMethod.isAccessible()){
                        oracleGetOriginalSqlMethod.setAccessible(true);
                    }
                    stmtSql = oracleGetOriginalSqlMethod.invoke(statement);
                }
            } catch (Exception ignored) {
                //ignore
                log.warn("Get original Sql from T4CPreparedStatement | OraclePreparedStatementWrapper fail.");
            }
        }
        String originalSql = null;
        if (SqlUtils.isString(stmtSql)) {
            originalSql = (String) stmtSql;
        }
        if (SqlUtils.isNull(originalSql)) {
            originalSql = statement.toString();
        }
        return originalSql;
    }
}
