<?php
/**
 * ===========================================================================
 * Veitool 快捷开发框架系统
 * Author: Niaho 26843818@qq.com
 * Copyright (c)2019-2023 www.veitool.com All rights reserved.
 * Licensed: 这不是一个自由软件，不允许对程序代码以任何形式任何目的的再发行
 * ---------------------------------------------------------------------------
 */
namespace addons\vtool\tool;

use tool\Menus;
use think\Exception;
use think\facade\Db;
use think\facade\Cache;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
use PhpZip\ZipFile;
use PhpZip\Exception\ZipException;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException;

/**
 * 构建服务类
 */
class Build
{
    /**
     * 构建单例盒子
     * @var array 
     */
    private static $build_instance = [];

    /**
     * 微库构建
     * @param   string    $name      构建名称
     * @param   array     $extend    扩展参数
     * @return  bool
     * @throws  Exception
     */
    public static function build(string $name, array $extend = [])
    {
        $repeat = $extend['repeat'] ?? 0; //是否检查冲突文件
        $reback = $extend['reback'] ?? 0; //是否备份冲突文件
        $do_build = $extend['do_build'] ?? 0; //数据生成选择
        $menu_pid = $extend['menu_pid'] ?? 0; //上级菜单ID
        unset($extend['repeat'], $extend['reback'], $extend['do_build'], $extend['menu_pid']);
        //远程构建下载
        $tmpFile  = self::download($name, $extend);
        //获取指定的构建目录
        $buildDir = self::getBuildDir($name);
        try{
            //解压构建压缩包到构建目录
            self::unzip($name);
            //是否检查文件冲突
            if($repeat) self::noConflict($name);
            //检查构建目录是否存在
            self::check($name);
        }catch(Exception $e){
            //异常 删除解压后的构建目录
            @rmdirs($buildDir);
            throw new Exception($e->getMessage());
        }finally{
            //最后移除临时文件
            @unlink($tmpFile);
        }
        //导入菜单数据、安装SQL 和 配置数据
        self::importsql($name,$do_build,$menu_pid);
        //启用构建
        return self::enable($name, $repeat, $reback);
    }

    /**
     * 卸载构建
     * @param   string   $name   构建名
     * @return  bool
     * @throws  Exception
     */
    public static function uninstall(string $name)
    {
        if(!$name || !is_dir(ADDON_PATH . '/vtool/data/' . $name)){
            throw new Exception('Build not exists');
        }

        /*移除文件处理*/
        $dirs = [];
        $list = self::aJson($name);
        $list = isset($list['files']) && is_array($list['files']) ? str_replace('@','',$list['files']) : [];
        //移除插件全局文件
        foreach($list as $v){
            $file = ROOT_PATH . $v;
            $dirs[] = dirname($file);
            @unlink($file);
        }
        /*移除插件空目录*/
        $dirs = array_filter(array_unique($dirs));
        foreach($dirs as $v){
            remove_empty_folder($v);
        }/**/

        //执行卸载脚本
        try{
            $build = self::getBuildInstance($name);
            $build->uninstall();
        }catch(Exception $e){
            throw new Exception($e->getMessage());
        }
        //移除构建目录
        @rmdirs(ADDON_PATH . '/vtool/data/' . $name);
        return true;
    }

    /**
     * 下载构建
     * @param   string   $name     构建名称
     * @param   array    $extend   扩展参数
     * @return  string   返回下载后的构建临时路径
     * @throws  Exception
     */
    public static function download(string $name, array $extend = [])
    {
        $addonsTempDir = self::getBuildBackupDir();
        $tmpFile = $addonsTempDir . $name . ".zip";


        try{
            $client = self::getClient();
            $response = $client->post('/api/build/vtool', ['form_params' => $extend]); //GET请求用: 'query'=>[]; POST请求用 'form_params'=>[]

            $body = $response->getBody();
            $content = $body->getContents();
            if(substr($content, 0, 1) === '{'){
                $json = (array)json_decode($content, true);
                //如果传回的是一个下载链接,则再次下载
                if($json['data'] && isset($json['data']['url'])){
                    $response = $client->get($json['data']['url']);
                    $body = $response->getBody();
                    $content = $body->getContents();
                }else{
                    throw new Exception($json['msg'],$json['code']);
                }
            }
        }catch(TransferException $e){
            throw new Exception("Build package download failed");
        }
        if($write = fopen($tmpFile, 'w')){
            fwrite($write, $content);
            fclose($write);
            return $tmpFile;
        }
        throw new Exception("No permission to write temporary files");
    }

    /**
     * 启用构建
     * @param   string   $name     构建名称
     * @param   bool     $repeat   是否检查冲突文件
     * @param   bool     $reback   是否备份冲突文件
     * @return  bool
     * @throws  Exception
     */
    public static function enable(string $name, bool $repeat = false, bool $reback = false)
    {
        if($repeat) self::noConflict($name); //检查冲突文件

        if($reback){ //备份冲突文件
            $conflictFiles = self::getGlobalFiles($name,true);
            if($conflictFiles){
                $zip = new ZipFile();
                try{
                    foreach($conflictFiles as $v){
                        $v = ltrim($v,'@');
                        $zip->addFile(ROOT_PATH . $v, $v);
                    }
                    //\think\facade\Log::write(self::getBuildBackupDir() . $name.'000');
                    $zip->saveAsFile(self::getBuildBackupDir() . $name . "-conflict-enable-" . date("YmdHis") . ".zip");
                }catch(Exception $e){
                    throw new Exception($e->getMessage());
                }finally{
                    $zip->close();
                }
            }
        }
        $files = self::getGlobalFiles($name);
        $onDir = self::getCheckDirs();
        $buildDir = self::getBuildDir($name);
        //更新构建资源文件记录
        if($files){
            self::aJson($name, ['files' => $files]);
        }
        //复制 可动资源 文件到全局
        foreach($onDir as $dir){
            if(is_dir($buildDir . $dir)){
                copydirs($buildDir . $dir, ROOT_PATH . ltrim($dir,'@'));
            }
        }
        //删除构建原可动资源目录
        foreach($onDir as $dir){
            @rmdirs($buildDir . $dir);
        }
        return true;
    }

    /**
     * 解压构建到构建目录
     * @param   string   $name   构建名称
     * @return  string
     * @throws  Exception
     */
    public static function unzip(string $name)
    {
        if(!$name){
            throw new Exception('Invalid parameters');
        }
        $file = self::getBuildBackupDir() . $name . '.zip';
        //打开构建压缩包
        $zip = new ZipFile();
        try{
            $zip->openFile($file);
        }catch(ZipException $e){
            $zip->close();
            throw new Exception('Unable to open the zip file');
        }
        $dir = self::getBuildDir($name);
        if(!is_dir($dir)){
            @mkdir($dir, 0755);
        }
        //解压构建压缩包到构建目录
        try{
            $zip->extractTo($dir);
        }catch(ZipException $e){
            throw new Exception('Unable to extract the file');
        }finally{
            $zip->close();
        }
        return $dir;
    }

    /**
     * 检测构建是否完整
     * @param   string  $name  构建名称
     * @return  bool
     * @throws  Exception
     */
    public static function check(string $name)
    {
        if(!$name || !is_dir(ADDON_PATH .'vtool'. VT_DS .'data'. VT_DS . $name)){
            throw new Exception('Build not exists');
        }
        return true;
    }

    /**
     * 是否有冲突
     * @param   string  $name  构建名称
     * @return  bool
     * @throws  Exception
     */
    public static function noConflict(string $name)
    {
        //检测冲突文件
        $list = self::getGlobalFiles($name, true);
        if($list){
            //发现冲突文件，抛出异常
            throw new Exception("Conflicting file found:". implode('|', $list));
        }
        return true;
    }

    /**
     * 导入SQL 菜单 配置
     * @param   string  $name  构建名称
     * @param   int     $flag  导入配置 0:都不生成 1:只生成数据表 2:只生成菜单 3:全部生成
     * @param   int     $pid   上级菜单ID
     * @return  bool
     * @throws  Exception
     */
    public static function importsql(string $name, int $flag = 3, int $pid = 0)
    {
        if($flag){
            //开启事务
            Db::startTrans();
            try{
                $buildDir = self::getBuildDir($name);
                if($flag == 1 || $flag == 3){
                    $sqlFile  = $buildDir . 'data/install.sql';
                    if(is_file($sqlFile)){
                        $prefix = config('database.connections.mysql.prefix');
                        $lines = file($sqlFile);
                        $sql = '';
                        $db = Db::connect();
                        foreach($lines as $line){
                            if(substr($line, 0, 2) == '--' || $line == '' || substr($line, 0, 2) == '/*'){
                                continue;
                            }
                            $sql .= $line;
                            if(substr(trim($line), -1, 1) == ';'){
                                $sql = str_ireplace('__PREFIX__', $prefix, $sql);
                                $sql = str_ireplace('INSERT INTO ', 'INSERT IGNORE INTO ', $sql);
                                $db->execute($sql);
                                $sql = '';
                            }
                        }
                    }
                }
                if($flag > 1){
                    $menusFile = $buildDir .'data/menus.php';
                    if(is_file($menusFile)){
                        $data = include_once $menusFile;
                        $data = $pid > 0 ? $data[0]['sublist'] : $data;
                        Menus::create($data, $pid);
                    }
                    $configFile = $buildDir . 'data/config.php';
                    if(is_file($configFile)){
                        $data = include_once $configFile;
                        if(is_array($data) && $data){
                            Db::name('system_setting')->insertAll($data);
                            Cache::delete('VSETTING');
                        }
                    }
                }
                Db::commit();
            }catch(Exception $e){
                @rmdirs($buildDir);
                Db::rollback();
                throw new Exception($e->getMessage());
            }
        }
        return true;
    }

    /**
     * 获取构建在全局的文件
     * @param   string    $name    构建名称
     * @param   bool      $flag    是否只返回冲突文件（重名文件内容不同时为冲突文件）
     * @return  array
     */
    public static function getGlobalFiles(string $name, bool $flag = false)
    {
        $list = [];
        $addonDir = self::getBuildDir($name);
        $checkDirList = self::getCheckDirs();
        //扫描构建目录是否有覆盖的文件
        foreach($checkDirList as $dirName){
            //检测目录是否存在
            if(!is_dir($addonDir . $dirName)){
                continue;
            }
            //匹配出所有的文件
            $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($addonDir . $dirName, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
            foreach($files as $fileinfo){
                if($fileinfo->isFile()){
                    $filePath = $fileinfo->getPathName();
                    $path = str_replace($addonDir, '', $filePath);
                    if($flag){
                        $destPath = ROOT_PATH . ltrim($path,'@');
                        if(is_file($destPath)){
                            if(filesize($filePath) != filesize($destPath) || md5_file($filePath) != md5_file($destPath)){
                                $list[] = $path;
                            }
                        }
                    }else{
                        $list[] = $path;
                    }
                }
            }
        }
        $list = array_filter(array_unique($list));
        return $list;
    }

    /**
     * 获取构建类的类名
     * @param  string  $name  构建名
     * @param  string  $type  获取类型 controller,hook
     * @param  string  $class 当前类名
     * @return string
     */
    public static function getBuildClass(string $name, string $type = '', string $class = null)
    {
        $name = parse_name($name);
        //处理多级控制器情况
        if(!is_null($class) && strpos($class, '.')){
            $class = explode('.', $class);
            $class[count($class) - 1] = parse_name(end($class), 1);
            $class = implode('\\', $class);
        }else{
            $class = parse_name(is_null($class) ? 'Main' : $class, 1);
        }
        $namespace = "\\addons\\vtool\\data\\" . $name . ($type ? "\\$type\\" : "\\") . $class;
        return class_exists($namespace) ? $namespace : '';
    }

    /**
     * 获取可变动的全局文件夹目录
     * @return  array
     */
    protected static function getCheckDirs()
    {
        return ['app','addons','extend','public','@view'];
    }

    /**
     * 获取远程服务器
     * @return  string
     */
    protected static function getServerUrl()
    {
        return config('veitool.api_url');
    }

    /**
     * 获取构建包临时目录
     * @return  string
     */
    public static function getBuildBackupDir()
    {
        $dir = RUNTIME_PATH . 'build' . VT_DS;
        if(!is_dir($dir)){
            @mkdir($dir, 0755, true);
        }
        return $dir;
    }

    /**
     * 获取指定构建的目录
     * @param  string  $name  构建名
     * @return string
     */
    public static function getBuildDir(string $name)
    {
        return ADDON_PATH. 'vtool' . VT_DS .'data'. VT_DS . $name . VT_DS;
    }

    /**
     * 获取构建的单例
     * @param  string  $name  构建名
     * @return obj|null
     */
    public static function getBuildInstance(string $name)
    {
        if(isset(self::$build_instance[$name])){
            return self::$build_instance[$name];
        }
        $class = self::getBuildClass($name);
        if(class_exists($class)){
            self::$build_instance[$name] = new $class();
            return self::$build_instance[$name];
        }else{
            throw new Exception("The build file does not exist");
        }
    }

    /**
     * 读取或修改构建资源记录
     * @param  string   $name   构建名
     * @param  array    $news   新数据
     * @return array
     */
    public static function aJson(string $name, array $news = [])
    {
        $buildDir = self::getBuildDir($name);
        $file = $buildDir . 'data' . VT_DS . '.ajson';
        $data = [];
        if(is_file($file)){
            $data = (array)json_decode(file_get_contents($file), true);
        }
        $data = array_merge($data, $news);
        if($news){
            file_put_contents($file, json_encode($data, JSON_UNESCAPED_UNICODE));
        }
        return $data;
    }

    /**
     * 获取请求对象
     * @return Client
     */
    protected static function getClient()
    {
        $options = [
            'base_uri'        => self::getServerUrl(),
            'timeout'         => 30,
            'connect_timeout' => 30,
            'verify'          => false,
            'http_errors'     => false,
            'headers'         => [
                'X-REQUESTED-WITH' => 'XMLHttpRequest',
                'Referer'          => request()->root(true),
                'User-Agent'       => 'VeitoolBuild for Vetool',
            ]
        ];
        static $client;
        return empty($client) ? $client = new Client($options) : $client;
    }

    /**
     * 备份冲突文件
     * @param   string  $name  构建名称
     * @return  bool
     * @throws  Exception
     */
    public static function backup(string $name)
    {
        $addonsBackupDir = self::getBuildBackupDir();
        $file = $addonsBackupDir . $name . '-backup-' . date("YmdHis") . '.zip';
        $zip = new ZipFile();
        try{
            $zip->addDirRecursive(self::getBuildDir($name))->saveAsFile($file)->close();
        }catch(ZipException $e){
            throw new Exception($e->getMessage());
        }finally{
            $zip->close();
        }
        return true;
    }

}