<?php

namespace app\service;

use Exception;
use think\facade\Db;
use app\lib\CertHelper;
use app\utils\CertDnsUtils;

/**
 * SSL证书订单处理
 */
class CertOrderService
{
    private static $retry_interval = [60, 180, 300, 600, 600];

    private $client;
    private $aid;
    private $atype;
    private $order;
    private $info;
    private $dnsList;
    private $domainList;
    private $cnameDomainList = [];

    // 订单状态：0:待提交 1:待验证 2:正在验证 3:已签发 4:已吊销 -1:购买证书失败 -2:创建订单失败 -3:添加DNS失败 -4:验证DNS失败 -5:验证订单失败 -6:订单验证未通过 -7:签发证书失败
    public function __construct($oid)
    {
        $order = Db::name('cert_order')->where('id', $oid)->find();
        if (!$order) throw new Exception('该证书订单不存在', 102);
        $this->order = $order;

        $this->aid = $order['aid'];
        $account = Db::name('cert_account')->where('id', $this->aid)->find();
        if (!$account) throw new Exception('该证书账户不存在', 102);
        $config = json_decode($account['config'], true);
        $ext = $account['ext'] ? json_decode($account['ext'], true) : null;
        $this->atype = $account['type'];
        $this->client = CertHelper::getModel2($account['type'], $config, $ext);
        if (!$this->client) throw new Exception('该证书类型不存在', 102);

        $domainList = Db::name('cert_domain')->where('oid', $oid)->order('sort', 'asc')->column('domain');
        if (!$domainList) throw new Exception('该证书订单没有绑定域名', 102);
        $this->domainList = $domainList;
        $this->info = $order['info'] ? json_decode($order['info'], true) : null;
        $this->dnsList = $order['dns'] ? json_decode($order['dns'], true) : null;
    }

    //执行证书申请
    public function process($isManual = false)
    {
        if ($this->order['status'] >= 3) return 3;
        if ($this->order['retry2'] >= 3 && !$isManual) {
            throw new Exception('已超出最大重试次数('.$this->order['error'].')', 103);
        }
        if ($this->order['status'] != 1 && $this->order['status'] != 2 && $this->order['retry'] >= 3 && !$isManual) {
            if ($this->order['status'] == -2 || $this->order['status'] == -5 || $this->order['status'] == -6 || $this->order['status'] == -7) {
                $this->cancel();
                if($this->order['status'] <= -5) $this->delDns();
                Db::name('cert_order')->where('id', $this->order['id'])->data(['status' => 0, 'retry' => 0, 'retrytime' => null, 'updatetime' => date('Y-m-d H:i:s')])->inc('retry2')->update();
                $this->order['status'] = 0;
                $this->order['retry'] = 0;
            } else {
                throw new Exception('已超出最大重试次数('.$this->order['error'].')', 103);
            }
        }

        $cname = CertHelper::$cert_config[$this->atype]['cname'];
        foreach($this->domainList as $domain){
            $mainDomain = getMainDomain($domain);
            $drow = Db::name('domain')->where('name', $mainDomain)->find();
            if (!$drow && preg_match('/^xn--/', $mainDomain)) {
                $drow = Db::name('domain')->where('name', idn_to_utf8($mainDomain))->find();
            }
            if (!$drow) {
                if (substr($domain, 0, 2) == '*.') $domain = substr($domain, 2);
                $cname_row = Db::name('cert_cname')->where('domain', $domain)->where('status', 1)->find();
                if (!$cname || !$cname_row) {
                    $errmsg = '域名'.$domain.'未在本系统添加';
                    Db::name('cert_order')->where('id', $this->order['id'])->data(['error'=>$errmsg]);
                    throw new Exception($errmsg, 103);
                } else {
                    $this->cnameDomainList[] = $cname_row['id'];
                }
            }
        }

        $this->lockOrder();
        try {
            return $this->processOrder($isManual);
        } finally {
            $this->unlockOrder();
            if (($this->order['status'] == -2 || $this->order['status'] == -5 || $this->order['status'] == -6 || $this->order['status'] == -7) && $this->order['retry'] >= 3) {
                Db::name('cert_order')->where('id', $this->order['id'])->data(['retrytime' => date('Y-m-d H:i:s', time() + 3600)])->update();
            }
        }
    }

    private function processOrder($isManual = false)
    {
        $this->client->setLogger(function ($txt) {
            $this->saveLog($txt);
        });
        // step1: 购买证书
        if ($this->order['status'] == 0 || $this->order['status'] == -1) {
            $this->saveLog(date('Y-m-d H:i:s').' - 开始购买证书');
            $this->buyCert();
        }
        // step2: 创建订单
        if ($this->order['status'] == 0 || $this->order['status'] == -2) {
            $this->saveLog(date('Y-m-d H:i:s').' - 开始创建订单');
            $this->createOrder();
        }
        // step3: 添加DNS
        if ($isManual && $this->order['status'] == -3 && CertDnsUtils::verifyDns($this->dnsList)) {
            $this->saveResult(1);
            $this->saveLog('检测到DNS记录已添加成功');
            return 1;
        }
        if ($this->order['status'] == 0 || $this->order['status'] == -3) {
            $this->saveLog(date('Y-m-d H:i:s').' - 开始添加DNS记录');
            $this->addDns();
            $this->saveLog('添加DNS记录成功，请等待生效后进行验证...');
            if (CertHelper::$cert_config[$this->atype]['cname']) {
                Db::name('cert_order')->where('id', $this->order['id'])->update(['retrytime' => date('Y-m-d H:i:s', time() + 180)]);
            }
            return 1;
        }
        // step4: 查询DNS
        if ($this->order['status'] == 1 || $this->order['status'] == -4) {
            $this->verifyDns();
        }
        // step5: 验证订单
        if ($this->order['status'] == 1 || $this->order['status'] == -5) {
            $this->saveLog(date('Y-m-d H:i:s').' - 开始验证订单');
            $this->authOrder();
        }
        // step6: 查询验证结果
        if ($this->order['status'] == 2 || $this->order['status'] == -6) {
            $this->saveLog(date('Y-m-d H:i:s').' - 开始查询验证结果');
            $this->getAuthStatus();
        }
        // step7: 签发证书
        if ($this->order['status'] == 2 || $this->order['status'] == -7) {
            $this->saveLog(date('Y-m-d H:i:s').' - 开始签发证书');
            $this->finalizeOrder();
        }
        $this->delDns();
        $this->resetRetry2();
        $this->saveLog('[Success] 证书签发成功');
        Db::name('cert_deploy')->where('oid', $this->order['id'])->data(['status' => 0, 'retry' => 0, 'retrytime' => null, 'issend' => 0])->update();
        return 3;
    }

    private function lockOrder()
    {
        Db::startTrans();
        try {
            $isLock = Db::name('cert_order')->where('id', $this->order['id'])->lock(true)->value('islock');
            if ($isLock == 1 && time() - strtotime($this->order['locktime']) < 3600) {
                throw new Exception('订单正在处理中，请稍后再试', 102);
            }
            $update = ['islock' => 1, 'locktime' => date('Y-m-d H:i:s')];
            if (empty($this->order['processid'])) $this->order['processid'] = $update['processid'] = getSid();
            Db::name('cert_order')->where('id', $this->order['id'])->update($update);
            Db::commit();
        } catch (Exception $e) {
            Db::rollback();
            throw $e;
        }
    }

    private function unlockOrder()
    {
        Db::name('cert_order')->where('id', $this->order['id'])->update(['islock' => 0]);
    }

    private function saveResult($status, $error = null, $retrytime = null)
    {
        $this->order['status'] = $status;
        if (!empty($error) && strlen($error) > 300) {
            $error = mb_strcut($error, 0, 300);
        }
        $update = ['status' => $status, 'error' => $error, 'updatetime' => date('Y-m-d H:i:s'), 'retrytime' => $retrytime];
        $res = Db::name('cert_order')->where('id', $this->order['id'])->data($update);
        if ($status < 0 || $retrytime) {
            $this->order['retry']++;
            $res->inc('retry');
        }
        $res->update();
        if ($error) {
            $this->saveLog('[Error] ' . $error);
        }
    }

    private function resetRetry()
    {
        if ($this->order['retry'] > 0) {
            $this->order['retry'] = 0;
            Db::name('cert_order')->where('id', $this->order['id'])->update(['retry' => 0, 'retrytime' => null]);
        }
    }

    private function resetRetry2()
    {
        if ($this->order['retry2'] > 0) {
            $this->order['retry2'] = 0;
            Db::name('cert_order')->where('id', $this->order['id'])->update(['retry2' => 0, 'retrytime' => null]);
        }
    }

    //重置订单
    public function reset()
    {
        Db::name('cert_order')->where('id', $this->order['id'])->data(['status' => 0, 'retry' => 0, 'retry2' => 0, 'retrytime' => null, 'processid' => null, 'updatetime' => date('Y-m-d H:i:s'), 'issend' => 0, 'islock' => 0])->update();
        $file_name = app()->getRuntimePath().'log/'.$this->order['processid'].'.log';
        if (file_exists($file_name)) unlink($file_name);
        $this->order['status'] = 0;
        $this->order['retry'] = 0;
        $this->order['retry2'] = 0;
        $this->order['processid'] = null;
    }

    //购买证书
    public function buyCert()
    {
        try {
            $this->client->buyCert($this->domainList, $this->info);
        } catch (Exception $e) {
            $this->saveResult(-1, $e->getMessage());
            throw $e;
        }
        if($this->info){
            Db::name('cert_order')->where('id', $this->order['id'])->update(['info' => json_encode($this->info)]);
        }
        $this->order['status'] = 0;
        $this->resetRetry();
    }

    //创建订单
    public function createOrder()
    {
        try {
            if (!empty($this->cnameDomainList)) {
                foreach($this->cnameDomainList as $cnameId){
                    $this->checkDomainCname($cnameId);
                }
            }
            try {
                $this->dnsList = $this->client->createOrder($this->domainList, $this->info, $this->order['keytype'], $this->order['keysize']);
            } catch (Exception $e) {
                if (strpos($e->getMessage(), 'KeyID header contained an invalid account URL') !== false) {
                    $ext = $this->client->register();
                    Db::name('cert_account')->where('id', $this->aid)->update(['ext' => json_encode($ext)]);
                    $this->dnsList = $this->client->createOrder($this->domainList, $this->info, $this->order['keytype'], $this->order['keysize']);
                } else {
                    throw $e;
                }
            }
        } catch (Exception $e) {
            $this->saveResult(-2, $e->getMessage());
            throw $e;
        }
        Db::name('cert_order')->where('id', $this->order['id'])->update(['info' => json_encode($this->info), 'dns' => json_encode($this->dnsList)]);

        if (!empty($this->dnsList)) {
            $dns_txt = '需验证的DNS记录如下:';
            foreach ($this->dnsList as $mainDomain => $list) {
                foreach ($list as $row) {
                    $domain = $row['name'] . '.' . $mainDomain;
                    $dns_txt .= PHP_EOL.'主机记录: '.$domain.' 类型: '.$row['type'].' 记录值: '.$row['value'];
                }
            }
            $this->saveLog($dns_txt);
        }
        $this->order['status'] = 0;
        $this->resetRetry();
    }

    //验证DNS记录
    public function verifyDns()
    {
        $verify = CertDnsUtils::verifyDns($this->dnsList);
        if (!$verify) {
            if ($this->order['retry'] >= 10) {
                $this->saveResult(-4, '未查询到DNS解析记录');
            } else {
                $this->saveLog('未查询到DNS解析记录(尝试第'.($this->order['retry']+1).'次)');
                $this->saveResult(1, null, date('Y-m-d H:i:s', time() + (array_key_exists($this->order['retry'], self::$retry_interval) ? self::$retry_interval[$this->order['retry']] : 1800)));
            }
            throw new Exception('未查询到DNS解析记录(尝试第'.($this->order['retry']).'次)，请稍后再试');
        }
        if($this->order['retry'] == 0 && time() - strtotime($this->order['updatetime']) < 10){
            throw new Exception('请等待'.(10 - (time() - strtotime($this->order['updatetime']))).'秒后再试');
        }
        $this->order['status'] = 1;
        $this->resetRetry();
    }

    //验证订单
    public function authOrder()
    {
        try {
            $this->client->authOrder($this->domainList, $this->info);
        } catch (Exception $e) {
            $this->saveResult(-5, $e->getMessage());
            throw $e;
        }
        $this->saveResult(2);
        $this->resetRetry();
    }

    //查询验证结果
    public function getAuthStatus()
    {
        try {
            $status = $this->client->getAuthStatus($this->domainList, $this->info);
        } catch (Exception $e) {
            $this->saveResult(-6, $e->getMessage());
            throw $e;
        }
        if(!$status){
            if ($this->order['retry'] >= 10) {
                $this->saveResult(-6, '订单验证未通过');
            } else {
                $this->saveLog('订单验证未通过(尝试第'.($this->order['retry']+1).'次)');
                $this->saveResult(2, null, date('Y-m-d H:i:s', time() + (array_key_exists($this->order['retry'], self::$retry_interval) ? self::$retry_interval[$this->order['retry']] : 1800)));
            }
            throw new Exception('订单验证未通过(尝试第'.($this->order['retry']).'次)，请稍后再试');
        }
        $this->order['status'] = 2;
        $this->resetRetry();
    }

    //签发证书
    public function finalizeOrder()
    {
        try {
            $result = $this->client->finalizeOrder($this->domainList, $this->info, $this->order['keytype'], $this->order['keysize']);
        } catch (Exception $e) {
            $this->saveResult(-7, $e->getMessage());
            throw $e;
        }
        $this->order['issuer'] = $result['issuer'];
        Db::name('cert_order')->where('id', $this->order['id'])->update(['fullchain' => $result['fullchain'], 'privatekey' => $result['private_key'], 'issuer' => $result['issuer'], 'issuetime' => date('Y-m-d H:i:s', $result['validFrom']), 'expiretime' => date('Y-m-d H:i:s', $result['validTo'])]);
        $this->saveResult(3);
        $this->resetRetry();
    }

    //吊销证书
    public function revoke()
    {
        $this->client->setLogger(function ($txt) {
            $this->saveLog($txt);
        });
        try {
            $this->client->revoke($this->info, $this->order['fullchain']);
        } catch (Exception $e) {
            throw $e;
        }
        $this->saveResult(4);
    }

    //取消证书订单
    public function cancel(){
        $this->client->setLogger(function ($txt) {
            $this->saveLog($txt);
        });
        if($this->order['status'] == 1 || $this->order['status'] == 2 || $this->order['status'] < -2){
            try {
                $this->client->cancel($this->info);
            } catch (Exception $e) {
            }
        }
    }

    //添加DNS记录
    public function addDns()
    {
        if (empty($this->dnsList)) {
            $this->saveResult(1);
            return;
        }
        try {
            CertDnsUtils::addDns($this->dnsList, function ($txt) {
                $this->saveLog($txt);
            }, !empty($this->cnameDomainList));
        } catch (Exception $e) {
            $this->saveResult(-3, $e->getMessage());
            throw $e;
        }
        $this->saveResult(1);
        $this->resetRetry();
    }

    //删除DNS记录
    public function delDns()
    {
        if (empty($this->dnsList)) return;
        try {
            CertDnsUtils::delDns($this->dnsList, function ($txt) {
                $this->saveLog($txt);
            }, true);
        } catch (Exception $e) {
            $this->saveLog('[Error] ' . $e->getMessage());
        }
    }

    //检查域名CNAME代理记录
    private function checkDomainCname($id)
    {
        $row = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id')->where('A.id', $id)->field('A.*,B.name cnamedomain')->find();
        $domain = '_acme-challenge.' . $row['domain'];
        $record = $row['rr'] . '.' . $row['cnamedomain'];
        $result = \app\utils\DnsQueryUtils::get_dns_records($domain, 'CNAME');
        if (!$result || !in_array($record, $result)) {
            $result = \app\utils\DnsQueryUtils::query_dns_doh($domain, 'CNAME');
            if (!$result || !in_array($record, $result)) {
                if ($row['status'] == 1) {
                    Db::name('cert_cname')->where('id', $id)->update(['status' => 0]);
                }
                throw new Exception('域名' . $row['domain'] . '的CNAME代理记录未验证通过');
            }
        }
    }

    private function saveLog($txt)
    {
        if (empty($this->order['processid'])) return;
        if (!is_dir(app()->getRuntimePath() . 'log')) mkdir(app()->getRuntimePath() . 'log');
        $file_name = app()->getRuntimePath().'log/'.$this->order['processid'].'.log';
        $file_exists = file_exists($file_name);
        file_put_contents($file_name, $txt . PHP_EOL, FILE_APPEND);
        if (!$file_exists) {
            @chmod($file_name, 0777);
        }
        if(php_sapi_name() == 'cli'){
            echo $txt . PHP_EOL;
        }
    }
}
