<?php

namespace Service;

use Event\Event;
use Lib\Imap\Fun;
use Lib\Imap\Imap;
use Lib\Imap\ImapConfig;
use Lib\Imap\ImapPool;
use Lib\Imap\ImapSearch;
use Lib\Imap\Parse\Body;
use Lib\Imap\Parse\Folder\Folder;
use Lib\Imap\Parse\MessageItem;
use Lib\Log;
use Model\bodySql;
use Model\emailSql;
use Model\folderSql;
use Model\listsSql;

/**
 * 同步邮件
 * @author:dc
 * @time 2024/9/26 9:31
 * Class SyncMail
 * @package Service
 */
class SyncMail {

    /**
     * @var \Lib\Db|\Lib\DbPool
     */
    protected $db;

    /**
     * @var \Lib\Imap\Imap
     */
    protected $imap;

    /**
     * @var array
     */
    protected $email;


    protected $isStop = false;

    /**
     * @var 搜索规则
     */
    protected $search = null;

    /**
     * 是否强制更新
     * @var bool
     */
    protected $isForceUpdate = false;

    /**
     * 是否输出日志
     * @var bool
     */
    protected $echoLog = false;


    /**
     * SyncMail constructor.
     * @param int|string|array $email
     * @throws \Exception
     */
    public function __construct(int|string|array $email,\Lib\Imap\Imap|null $imap = null)
    {
        $this->db = db();

        if(!is_array($email)){
            $email = $this->db->cache(3600)->first(emailSql::first($email));
            if(!$email){
                abort('未查询到邮箱');
            }
        }
        $this->email = $email;
        // 实例一个imap类
        if($imap instanceof \Lib\Imap\Imap){
            $this->imap = $imap;
        }else{
            $this->imap = new Imap(
                (new ImapConfig())
                    ->setHost($email['imap'])
                    ->setEmail($email['email'])
                    ->setPassword(base64_decode($email['password']))
//                ->debug()
            );

            $this->login();

        }
    }

    public function stop(){
        $this->isStop = true;
    }

    /**
     * @param bool $echoLog
     */
    public function setEchoLog()
    {
        $this->echoLog = true;
        return $this;
    }

    /**
     * 输出日志
     * @param $msg
     * @author:dc
     * @time 2025/3/20 11:03
     */
    protected function eLog($msg){
        if($this->echoLog){
            _echo($msg);
        }
    }

    /**
     * 搜索
     * @param ImapSearch $search
     * @return $this
     * @author:dc
     * @time 2024/11/11 17:29
     */
    public function search(ImapSearch $search){
        $this->search = $search;
        return $this;
    }

    // 查找uid之后的数据
    public $isUid = 0;

    /**
     * @param $uid 2 表示没有数据 则执行下一个条件  1表示没有数据就没有数据
     * @return $this
     * @author:dc
     * @time 2025/3/7 14:53
     */
    public function isUidAfter($uid=1){
        $this->isUid = $uid;
        return $this;
    }


    protected function emailId(){
        return $this->email['id'];
    }

    /**
     * 强制更新本地数据
     */
    public function forceUpdate(): static
    {
        $this->isForceUpdate = true;
        return $this;
    }

    /**
     * 登录imap
     * @throws \Exception
     * @author:dc
     * @time 2024/9/26 9:58
     */
    private function login(){
        $login = $this->imap->login();
        if(!$login->isOk()){
            foreach ([
                         '[ALERT] Invalid credentials (Failure)',// 登录失败
                         '[AUTHENTICATIONFAILED] Invalid credentials (Failure)',// 登录失败
                         '[AUTHENTICATIONFAILED] Authentication failed.',// 登录失败 权限
                         'LOGIN Login error',// 登录失败
                         'LOGIN auth error',// 登录失败
                         'ERR.LOGIN.PASSERR',// 登录失败 密码错误
                         'Login fail.',// 登录失败
                         'LOGIN failed.', // 登录失败
//                    'NO ERR.LOGIN.REQCODE', // 未知错误
                         '[ALERT] Application-specific password', // 这个错误是没有提供特定的授权码
                         'password error',
                         'ERR.LOGIN.REQCODE',
                         'authentication failure'
                     ] as $em){
                if(str_contains($login->getMessage(), $em)){
                    $this->db->update(
                        \Model\emailSql::$table,
                        ['pwd_error'=>1],
                        dbWhere(['id'=> $this->emailId()])
                    );
                }
            }

            abort($login->getMessage()?:'连接服务器异常');
        }
    }

    /**
     * @param $folder
     * @param int $pid
     * @return array
     * @throws \Exception
     * @author:dc
     * @time 2024/9/26 10:46
     */
    protected function folder($folder,$pid = 0){
        $uuids = [];

        foreach ($folder as $item){
            /** @var Folder $item*/

            $uuid = md5($this->emailId().$item->folder);
            $uuids[$uuid] = $uuid;

            $folder_name = '';
            if($item->flags){
                // 有些邮箱是把公共的文件夹标记在flag里面的,识别出来
                foreach ($item->flags as $flag){
                    if(in_array($flag,['Send','Drafts','Junk','Trash'])){
                        $folder_name = folderAlias($flag);
                    }
                }
            }
            if(!$folder_name){
                $fn = explode('/',$item->getParseFolder());
                $folder_name = folderAlias(end($fn));
            }

            // 是否存在
            $id = $this->db->value(folderSql::has(['email_id'=>$this->emailId(),'uuid'=>$uuid]));
            $data = [
                'email_id' => $this->emailId(),
                'folder' => $folder_name,
                'origin_folder' => $item->folder,
                'uuid'  =>  $uuid,
                'pid'   =>  $pid
            ];
            if ($id){
                $this->db->update(folderSql::$table,$data,dbWhere(['id'=>$id]),false);
            }else{
                $id = $this->db->insert(folderSql::$table,$data,false);
                if(!$id) abort('文件夹写入异常 '.json_encode($data,JSON_UNESCAPED_UNICODE));
            }
            // 是否有子级目录
            if($id && $item->getChild()){
                $uuids = array_merge($uuids,$this->folder($item->getChild(),$id));
            }
        }

        return $uuids;
    }

    /**
     * @param bool $syncMail
     * @return bool|void|array
     * @throws \Exception
     * @author:dc
     * @time 2024/10/18 17:53
     */
    public function sync($syncMail = true){
        $this->isStop = false;
        /*********************************** 同步文件夹 ***************************************/
        // 获取文件夹
        $folders = $this->imap->getFolders();
        $uuids = $this->folder($folders->getTopFolder());
        if($uuids){
            // 删除以前的
            $this->db->delete(folderSql::$table,['uuid.notin'=>$uuids,'email_id'=>$this->emailId()]);
        }

        if (!$syncMail) return true;

//        _echo($this->emailId().' ===> 文件夹同步成功');
        if($this->isStop) return;


        /********************* 同步邮件 **********************/
        $syncNum = [];
        // 循环文件夹
        $startTime = time();
        foreach ($folders->all() as $f){
            if($this->isStop) return;

            if($f->isSelect){ // 是否可以选择 只有可以选中的文件夹才有邮件
                $folder = $this->imap->folder($f); // 选择文件夹后,有状态

                // 是否有邮件 有邮件才继续
                if ($folder->getTotal()){
                    $num = $this->mail($folder);
                    if($num){
                        $syncNum[$folder->getName()] = $num;
//                        _echo($this->emailId().' ===> '.$folder->getName().' ===> '.$num);
                    }
                }

            }
        }
        _echo($this->emailId()."=>runTime:".(time()-$startTime).'s => total: '.(array_sum($syncNum)));
        return $syncNum;
    }

    /**
     * 当前 目录的id
     * @param string $name
     * @return mixed|null
     * @author:dc
     * @time 2024/10/12 17:44
     */
    private function getFolderId(string $name){
        return $this->db->cache(120)->value(folderSql::first([
            'email_id'=>$this->emailId(),
            'uuid'  =>  md5($this->emailId().$name)
        ],'`id`'));
    }

    /**
     * 过滤uid
     * @param array $uids
     * @return array
     * @author:dc
     * @time 2024/11/12 14:17
     */
    private function getFilterUid(array $uids, int $folder_id):array {
        // 强制更新本地数据,不进行过滤
        if($this->isForceUpdate){
            return $uids;
        }
        foreach ($uids as $k=>$uid){

            $num = redis()->get('h_'.$folder_id.'_'.$uid,function () use ($folder_id,$uid){
                $num = $this->db->value(listsSql::first(dbWhere(['email_id'=>$this->emailId(),'folder_id'=>$folder_id,'uid'=>$uid]),'count(*) as c'));
                // 查询lists_hot表
                if(!$num){
                    $num = $this->db->value("select count(*) as c from lists_hot where `email_id` = ".$this->emailId()." and `folder_id` = {$folder_id} and `uid` = {$uid}");
                }
                if($num){
                    redis()->set('h_'.$folder_id.'_'.$uid,1,86400);
                }
                return $num;
            });

            if($num){
                unset($uids[$k]);
            }
        }

        return array_values($uids);
    }

    /**同步邮件
     *
     * @param string|\Lib\Imap\Request\Folder $folder
     * @param array $uids 固定的uid
     * @param false $isBody 是否同时同步body
     * @author:dc
     * @time 2024/9/26 11:10
     */
    public function mail(string|\Lib\Imap\Request\Folder $folder, array $uids = [],$isBody = false):int {
        $sync_number = 0;
        if(is_string($folder)){
            $folder = $this->imap->folder($folder)->exec();
        }
        $this->eLog("正在同步 ".$folder->getName());
        $folder_id = $this->getFolderId($folder->getName());
        if($folder->getName() == 'INBOX'){
            $this->folder_inbox_id =     $folder_id;
        }
        if(in_array($folder->getName(),['INBOX','[Gmail]/Important','[Gmail]/Starred','星标邮件','Important'])){
            $isBody = true;
        }
        // 选择成功
        if($folder->isOk()){
            $msg = $folder->msg();

            if($uids){
                $this->saveMail($folder_id,$msg->uid($uids)->get()->all(),$isBody);
            }else{
                $p=1;
                while (1){
                    if($this->isStop) return $sync_number;

                    if($this->isUid){
                        $maxUid = $this->db->value(listsSql::first(dbWhere([
                            'email_id'=>$this->emailId(),
                            'folder_id'=>$folder_id,
                        ]),'max(uid)'));
                        $maxUid = $maxUid?$maxUid:0;
                        if($this->isUid==1&&!$maxUid){
                            return 0;
                        }
                        if($maxUid){
                            $this->eLog("找到最大的uid ".$maxUid);
                            $lists = $msg->uid(1)->get(($maxUid+1).':*')->all();
                            if($lists){
                                $this->saveMail($folder_id,$lists,$isBody);
                                return count($lists);
                            }
                        }
                    }

                    // 是否搜索
                    if ($this->search instanceof ImapSearch){
                        $uids = $msg->search($this->search)->getUids();
                    }else{
                        $uids = $msg->forPage($p)->getOriginUids();
                    }

                    if(!$uids){
                        break;
                    }

                    $uids = $this->getFilterUid($uids,$folder_id);
                    $p++;
                    if($uids){
                        $lists = $msg->uid($uids)->get()->all();
                        $sync_number += count($lists);
                        // 没有数据就跳出
                        if($lists){
                            $this->saveMail($folder_id,$lists,$isBody);
                        }
                    }
                    // 只需要执行一次
                    if ($this->search instanceof ImapSearch){
                        break;
                    }
                }
            }

        }

        return  $sync_number;
    }

    /**
     * 保存邮件列表
     * @param int $folder_id
     * @param MessageItem[] $lists
     * @param bool $isBody
     * @author:dc
     * @time 2024/9/29 15:14
     */
    protected function saveMail(int $folder_id, array $lists, bool $isBody=false){
        foreach ($lists as $item){
            try {
                $data   =   [
                    'uid'   =>  $item->uid,
                    'subject'   =>  mb_substr($item->header->getSubject(),0,1000),// 控制下,有的蛋疼,整tm多长
                    'cc'    =>  $item->header->getCc(true),
                    'bcc'    =>  $item->header->getBcc(true),
                    'from'   =>  $item->header->getFrom()->email,
                    'from_name'   =>  mb_substr($item->header->getFrom()->name,0,200),
                    'to'   =>  implode(',',array_column($item->header->getTo(true),'email')),
                    'to_name'   =>  $item->header->getTo(true),
                    // 这个是 邮件的时间 就是header里面带的 一般情况就是发件时间
//                'date'   =>  strtotime($item->header->getDate()),
                    'udate'   =>  strtotime($item->date), // 有这个时间就够了,内部时间,就是收到邮件的时间
                    'size'   =>  $item->size,
                    'recent'   =>  $item->isRecent() ? 1 : 0,
                    'seen'   =>  $item->isSeen() ? 1 : 0,
                    'draft'   =>  $item->isDraft() ? 1 : 0,
                    'flagged'   =>  $item->isFlagged() ? 1 : 0,
                    'answered'   =>  $item->isAnswered() ? 1 : 0,
                    'folder_id'   =>  $folder_id,
                    'email_id'    =>  $this->emailId(),
                    'is_file'  =>  $item->isAttachment() ? 1: 0 //是否附件
                ];
            }catch (\Throwable $e){
                logs([$e->getMessage(),$folder_id,$this->emailId(),$item->uid]);
                continue;
            }

            if(empty($data['from'])&&empty($data['subject'])){
                logs(['邮件没有主题和发件人',[$folder_id,$item->uid],$data??[],$item->header->getRaw()]);
            }


            $data['from'] = mb_substr($data['from'],0,120);

            // 不知道为什么 有些邮件标题有下划线,但是发件那边并没有添加下划线
            $data['subject'] = str_replace('_',' ',$data['subject']);

            // 查询是否存在
            $id = $this->db->value(listsSql::first(dbWhere([
                'email_id'=> $data['email_id'],
                'folder_id' =>  $data['folder_id'],
                'uid'   =>  $data['uid']
            ]),'`id`'));

            if(!$id){
                $this->eLog(
                    sprintf("正在插入数据 eid:%d fid:%d uid:%d subject: %s",
                        $data['email_id'],
                        $data['folder_id'],
                        $data['uid'],
                        $data['subject'],
                    ));

// 收件箱直接 未读 不远程为准
                if(defined('CLI_AI_SYNC_START') && !empty($this->folder_inbox_id) && $this->folder_inbox_id == $data['folder_id']){
                    $data['seen'] = 0;
                }
                $id = $this->insert($data);

                if(!$id){
                    continue;
                }
                // 新邮件标记
                if($item->getFolderName() == 'INBOX')
                    redis()->incr('have_new_mail_'.$this->emailId(),120);
                // 执行事件
                $data['Aicc-Hot-Mail'] = $item->header->get('Aicc-Hot-Mail');

                Event::call('mail_sync_list',$id, $data);

            }else{
                // 非ai邮件才更新
                if(!defined('CLI_AI_SYNC_START')){
                    $this->eLog(
                        sprintf("正在update数据 eid:%d fid:%d uid:%d subject: %s",
                            $data['email_id'],
                            $data['folder_id'],
                            $data['uid'],
                            $data['subject'],
                        ));
                    $this->db->update(listsSql::$table,$data,dbWhere(['id'=> $id]));
                }

                if(php_sapi_name()=='cli'){
                    // 更新的就不需要操作body了
                    continue;
                }

            }

            //TODO 如果header 头信息里面有2段数据 第二段就作为内容解析
            if($item->header->body()){
                $parseBody = $item->header->body();
            }elseif($isBody && $item->body->getRaw()){
                $parseBody = $item->body;
            }else{
                $parseBody = false;
            }

            // 是否同步body内容
            if($parseBody instanceof Body){
                // 记录邮件体 源文件
//                Log::put(
//                    ROOT_PATH.'/eml/'.$this->email['email']."/".$item->getFolderName().'/'.$item->uid.'.eml',
//                    $item->header->getRaw()."\r\n\r\n".$parseBody->getRaw()
//                );

                $body = [
                    'lists_id'  =>  $id,
                    'text_html' =>  []
                ];


                $body['text_html'][] = [
                    'body'  =>  base64_encode($parseBody->getHtml() ? : $parseBody->getText()),
                    'type'  =>  $parseBody->getHtml()?'text/html':'text/plain',
                    'charset'   =>  'utf-8',
                    'encode'   =>  'base64',
                ];

                // 处理附件
                foreach ($parseBody->getAttachment() as $itemBody){
                    $tmp = [
                        'body'  =>  '',
                        'type'  =>  $itemBody->getFileType(),
                        'charset'   =>  'binary',
                        'encode'   =>  $itemBody->data->get('content-transfer-encoding'),
                        'name'   =>  $itemBody->getFilename(),
                        'filename'   =>  $itemBody->getFilename(),
                        'path'  =>  $itemBody->save(MAIL_ATTACHMENT_PATH)
                    ];
                    if(!$tmp){
                        throw  new \Exception('请检查附件是否有写入权限');
                    }
                    if($itemBody->getContentId()){
                        $tmp['content-id'] = $itemBody->getContentId();
                    }
                    if($itemBody->data->get('Content-Disposition')){
                        $tmp['content-disposition'] = $itemBody->data->get('Content-Disposition');
                    }
                    $tmp['signName'] = explode('/',$tmp['path']);
                    $tmp['signName'] = end($tmp['signName']);

                    $body['text_html'][] = $tmp;

                }

                if($this->db->count(bodySql::has($id))){
                    $this->db->update(bodySql::$table,$body,'`lists_id` = '.$id,false);
                }else{
                    $this->db->insert(bodySql::$table,$body,false);
                }

                // 更新描述
                $this->db->update(listsSql::$table,[
                    'description'=>
                        Fun::mb_convert_encoding(mb_substr($parseBody->getText(),0,150),'utf-8')
                ],dbWhere(['id'=> $id]));

            }


        }


    }

    /**
     * 查询数据 并重试
     * @param array $data
     * @param int $num
     * @return int
     * @author:dc
     * @time 2024/10/12 15:32
     */
    protected function insert(array $data, int $num = 0){
        if($num>2){
            return 0;
        }
        try {
            $id = $this->db->throw()->insert(listsSql::$table,$data);
        }catch (\Throwable $e){
            // 字符串编码异常
            if(stripos($e->getMessage(),'Incorrect string value:')!==false){
                // 编码异常的 字段
                preg_match("/for column '([a-z0-9_]{2,})' at/",$e->getMessage(),$filed);
                if(!empty($filed[1]) && isset($data[$filed[1]])){
                    // 进行编码转换 大概率会失败
                    $data[$filed[1]] = Fun::mb_convert_encoding($data[$filed[1]],'UTF-8');
                }

                $id = $this->insert($data,$num+1);

            }
            logs([$data,$e->getMessage()]);
        }

        return $id??0;
    }








    public function __destruct()
    {
        // TODO: Implement __destruct() method.
        ImapPool::release($this->imap);
        unset($this->imap);
    }


}