package com.skr.mdm.authentication.user.sms;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.skr.mdm.authentication.user.qr.NumberLimitException;
import com.skr.mdm.bean.dto.UserLoginCallbackDTO;
import com.skr.mdm.bean.qo.GaodeTerminalQO;
import com.skr.mdm.dao.AuCompanyDao;
import com.skr.mdm.dao.DeviceEquipmentDao;
import com.skr.mdm.dto.LoginUserDTO;
import com.skr.mdm.entity.AuCompanyDO;
import com.skr.mdm.entity.DeviceEquipmentDO;
import com.skr.mdm.enums.DeleteStateEnum;
import com.skr.mdm.enums.EquipmentTypeEnum;
import com.skr.mdm.rabbitmq.RabbitMQProvider;
import com.skr.mdm.util.RegisterEquipmentUtil;
import com.skr.mdm.util.SmUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;

import javax.net.ssl.*;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;

@Slf4j
public class SmsAuthenticationProvider implements AuthenticationProvider {

    private static final Integer COUNT_EQUIPMENT = 9;

    private SmsDetailsServiceImpl userDetailsService;
    private DeviceEquipmentDao equipmentDao;
    private AuCompanyDao companyDao;
    private RabbitMQProvider rabbitMQProvider;

    private final String MOB_TECH_URL = "https://webapi.sms.mob.com/sms/verify";

    private final String MOB_TECH_APP_KEY = "appkey=36269a90b7fe4";

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
        String phone = (String) authenticationToken.getPrincipal();
        Object smsCode = authenticationToken.getCredentials();
        String imei = (String) authenticationToken.getImei();
        String oaid = (String) authenticationToken.getOaid();
        String uuid = (String) authenticationToken.getUuid();
        String companyId = (String) authenticationToken.getCompanyId();

        // mob teach 短信验证
        String codeCheck = requestData(MOB_TECH_URL, MOB_TECH_APP_KEY + "&phone=" + phone + "&zone=86" + "&code=" + smsCode);
        if (!StringUtils.equalsIgnoreCase(codeCheck, HttpURLConnection.HTTP_OK + "")) {
            throw new SMSCodeLoginException(codeCheck);
        }
        UserDetails userDetails = null;
        DeviceEquipmentDO equipmentDO = null;
        String loginType = "imei";
        if (StringUtils.isNotBlank(imei)) {
            LambdaQueryChainWrapper<DeviceEquipmentDO> queryChainWrapper = new LambdaQueryChainWrapper<>(equipmentDao);
            equipmentDO = queryChainWrapper
                    .eq(DeviceEquipmentDO::getImei, imei)
                    .eq(DeviceEquipmentDO::getIsDelete, DeleteStateEnum.NON_DELETE.getState())
                    .eq(DeviceEquipmentDO::getCompanyId, Integer.valueOf(companyId))
                    .one();
        }
        if (Objects.isNull(equipmentDO) && StringUtils.isNotBlank(oaid)) {
            loginType = "oaid";
            equipmentDO = new LambdaQueryChainWrapper<>(equipmentDao)
                    .eq(DeviceEquipmentDO::getOaid, oaid)
                    .eq(DeviceEquipmentDO::getIsDelete, DeleteStateEnum.NON_DELETE.getState())
                    .eq(DeviceEquipmentDO::getCompanyId, Integer.valueOf(companyId))
                    .one();
        }
        if (Objects.isNull(equipmentDO) && StringUtils.isNotBlank(uuid)) {
            loginType = "uuid";
            equipmentDO = new LambdaQueryChainWrapper<>(equipmentDao)
                    .eq(DeviceEquipmentDO::getUuid, uuid)
                    .eq(DeviceEquipmentDO::getIsDelete, DeleteStateEnum.NON_DELETE.getState())
                    .eq(DeviceEquipmentDO::getCompanyId, Integer.valueOf(companyId))
                    .one();
        }
        // imei oaid uuid 有一个唯一标识即可
        // 校验用户名
        userDetailsService.setCompanyId(Integer.valueOf(companyId));
        userDetails = userDetailsService.loadUserByUsername(phone);
        if (userDetails == null) {
            throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null");
        }
        LoginUserDTO loginUserDTO = (LoginUserDTO) userDetails;
        if (Objects.nonNull(equipmentDO) && !equipmentDO.getUserId().equals(loginUserDTO.getUserId())) {
            throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation " + loginType + "  already exist!");
        } else if (Objects.isNull(equipmentDO)) {
            // 注册设备
            equipmentDO = new DeviceEquipmentDO();
            equipmentDO.setUserId(loginUserDTO.getUserId());
            equipmentDO.setUserName(loginUserDTO.getUserName());
            equipmentDO.setDepartmentId(loginUserDTO.getDepartmentId());
            equipmentDO.setDepartmentName(loginUserDTO.getDepartmentName());
            equipmentDO.setCompanyId(loginUserDTO.getCompanyId());
            equipmentDO.setEquipmentType(EquipmentTypeEnum.CELL_PHONE.getType());
            equipmentDO.setImei(imei);
            equipmentDO.setOaid(oaid);
            equipmentDO.setUuid(uuid);
            equipmentDO.setActivationTime(new Date());
            String equipmentName = getEquipmentName(equipmentDO.getUserName(), Integer.valueOf(companyId));
            equipmentDO.setEquipmentName(equipmentName);
            AuCompanyDO companyDO = companyDao.selectById(loginUserDTO.getCompanyId());
            // 设备数量限制
            String allowUseNumber = companyDO.getAllowUseNumber();
            SM2 sm2 = SmUtils.sm2();
            String authorizationNumber = StrUtil.utf8Str(sm2.decryptFromBcd(allowUseNumber, KeyType.PrivateKey));

            Long equipmentCount = new LambdaQueryChainWrapper<>(equipmentDao)
                    .eq(DeviceEquipmentDO::getCompanyId, loginUserDTO.getCompanyId())
                    .eq(DeviceEquipmentDO::getIsDelete, DeleteStateEnum.NON_DELETE.getState())
                    .count();
            if (equipmentCount >= Integer.parseInt(authorizationNumber)) {
                throw new NumberLimitException("设备数量超出限制，无法注册，请联系管理员!");
            }
            // 创建猎鹰终端
            if (companyDO.getSid() != null) {
                GaodeTerminalQO gaodeTerminalQO = GaodeTerminalQO.builder()
                        .sid(companyDO.getSid())
                        .phoneNumber(loginUserDTO.getPhoneNumber())
                        .equipmentName(equipmentDO.getEquipmentName())
                        .userName(equipmentDO.getUserName())
                        .departmentName(equipmentDO.getDepartmentName())
                        .companyName(companyDO.getCompanyName())
                        .imei(equipmentDO.getImei())
                        .oaid(equipmentDO.getOaid())
                        .uuid(equipmentDO.getUuid())
                        .build();
                RegisterEquipmentUtil.registerGaodeTerminal(gaodeTerminalQO, equipmentDO);
            }
            equipmentDao.insert(equipmentDO);
            BeanUtils.copyProperties(equipmentDO, loginUserDTO);
            loginUserDTO.setId(equipmentDO.getId());
            // 异步拉取全局指令
            UserLoginCallbackDTO userLoginCallbackDTO = UserLoginCallbackDTO.builder().deviceIdList(ListUtil.toList(equipmentDO.getId())).companyId(loginUserDTO.getCompanyId()).build();
            rabbitMQProvider.publishRegisterEventWithDelayed(userLoginCallbackDTO);
        } else {
            // 退出登录 再次登录直接响应
            BeanUtils.copyProperties(equipmentDO, loginUserDTO);
        }
        SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails.getAuthorities(), userDetails, imei, oaid, uuid, companyId);
        authenticationResult.setDetails(authentication.getDetails());
        return authenticationResult;
    }

    private String getEquipmentName(String userName, Integer companyId) {
        List<DeviceEquipmentDO> equipmentDOList = new LambdaQueryChainWrapper<>(equipmentDao)
                .likeRight(DeviceEquipmentDO::getEquipmentName, EquipmentTypeEnum.CELL_PHONE.getName())
                .eq(DeviceEquipmentDO::getUserName, userName)
                .eq(DeviceEquipmentDO::getCompanyId, companyId)
                .eq(DeviceEquipmentDO::getIsDelete, DeleteStateEnum.NON_DELETE.getState())
                .list();
        if (CollectionUtils.isEmpty(equipmentDOList)) {
            return EquipmentTypeEnum.CELL_PHONE.getName() + "01";
        }
        List<Integer> numberList = new ArrayList<>(equipmentDOList.size());
        equipmentDOList.forEach(equipmentDO -> {
            if (equipmentDO.getEquipmentName().length() == 4) {
                String number = StringUtils.substringAfter(equipmentDO.getEquipmentName(), EquipmentTypeEnum.CELL_PHONE.getName());
                if (StringUtils.isNumeric(number)) {
                    numberList.add(Integer.valueOf(number));
                }
            }
        });
        if (CollectionUtils.isEmpty(numberList)) {
            return EquipmentTypeEnum.CELL_PHONE.getName() + "01";
        }
        Integer max = CollectionUtil.max(numberList);
        if (max < COUNT_EQUIPMENT) {
            max++;
            return EquipmentTypeEnum.CELL_PHONE.getName() + "0" + max;
        }
        max++;
        return EquipmentTypeEnum.CELL_PHONE.getName() + max;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (SmsAuthenticationToken.class.isAssignableFrom(authentication));
    }

    public void setUserDetailsService(SmsDetailsServiceImpl userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    public void setEquipmentDao(DeviceEquipmentDao equipmentDao) {
        this.equipmentDao = equipmentDao;
    }

    public void setCompanyDao(AuCompanyDao companyDao) {
        this.companyDao = companyDao;
    }

    public void setRabbitMQProvider(RabbitMQProvider rabbitMQProvider) {
        this.rabbitMQProvider = rabbitMQProvider;
    }

    /**
     * 发起https 请求
     *
     * @param address
     * @param params
     * @return
     */
    public static String requestData(String address, String params) {

        HttpURLConnection conn = null;
        try {
            // Create a trust manager that does not validate certificate chains
            TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }

                @Override
                public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] certs, String authType) {
                }
            }};

            // Install the all-trusting trust manager
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null, trustAllCerts, new SecureRandom());

            //ip host verify
            HostnameVerifier hv = new HostnameVerifier() {
                @Override
                public boolean verify(String urlHostName, SSLSession session) {
                    return urlHostName.equals(session.getPeerHost());
                }
            };

            //set ip host verify
            HttpsURLConnection.setDefaultHostnameVerifier(hv);

            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

            URL url = new URL(address);
            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("POST");// POST
            conn.setConnectTimeout(3000);
            conn.setReadTimeout(3000);
            // set params ;post params
            if (params != null) {
                conn.setDoOutput(true);
                DataOutputStream out = new DataOutputStream(conn.getOutputStream());
                out.write(params.getBytes(StandardCharsets.UTF_8));
                out.flush();
                out.close();
            }
            conn.connect();
            //get result
//            200 验证成功
//            405 AppKey为空
//            406 AppKey无效
//            456 国家代码或手机号码为空
//            457 手机号码格式错误
//            466 请求校验的验证码为空
//            467 请求校验验证码频繁（5分钟内同一个appkey
//            的同一个号码最多只能校验三次）
//            468 验证码错误
//            474 没有打开服务端验证开关
            if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
                return parsRtn(conn.getInputStream());
            } else {
                log.warn("验证码校验失败: 编码 {}  报文{}", conn.getResponseCode() + "", conn.getResponseMessage());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
        return null;
    }

    /**
     * 将从服务器获得的流is转换为字符串
     *
     * @param is
     * @return
     * @throws IOException
     */
    private static String parsRtn(InputStream is) throws IOException {

        //初始值，起标志位作用
        int len = -1;
        //缓冲区
        byte[] buf = new byte[128];
        //捕获内存缓冲区的数据转换为字节数组
        ByteArrayOutputStream bas = new ByteArrayOutputStream();
        //循环读取内容,将输入流的内容放进缓冲区中
        while ((len = is.read(buf)) != -1) {
            //将缓冲区内容写进输出流，0是从起始偏移量，len是指定的字符个数
            bas.write(buf, 0, len);
        }
        //最终结果，将字节数组转换成字符
        return JSONUtil.parseObj(bas.toString()).get("status") + "";
    }
}