<?php

namespace app\service\wx;

use app\exception\RepositoryException;
use app\model\SubscribeMessageLog;
use EasyWeChat\Factory;
use EasyWeChat\Kernel\Exceptions\HttpException;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
use EasyWeChat\Kernel\Exceptions\RuntimeException;
use EasyWeChat\MiniProgram\Application;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use think\facade\Config;
use think\facade\Log;
use think\Model;

/**
 * 微信小程序
 * Class WechatApplets
 * @package app\service\wx
 */
class WechatApplets
{
    private static $app = null;

    /**
     * TODO 正式上线时需要替换模板配置信息和相关小程序配置信息
     */
    // 订阅消息模板:预约通知
    public const SUBSCRIBE_TPL_APPOINTMENT = 'uvGd7RqaegheGU-uVxR-uM3y2MadZeMOHdQaNiiWm8U';
    // 订阅消息模版:新活动通知
    public const SUBSCRIBE_TPL_ACTIVITY = 'd0efR-Ga27c6eIvx9mAwJcnAqzhM_Sq68XiFvjvlBJM';
    // 未读消息通知:自然流量待分配通知管理员、新顾客通知员工、新客户通知客服 后台分配客服、邀请的新客户绑定手机号通知相应客服
    public const SUBSCRIBE_TPL_NEW_INFO_NOTICE = 'eyxvInLLF3L_wmcSQc_O7XLKF7RoGK1dM3OwKj5fHio';

    private function __construct()
    {
    }

    private function __clone()
    {
    }

    //微信小程序实例  单例模式
    public static function getInstance(): ?Application
    {
        if (self::$app == null) {
            Config::load('extra/wechat', 'wechat');
            $conf      = config('wechat');
            $config    = [
                // 必要配置
                'app_id'        => $conf['applets_appId'],
                'secret'        => $conf['applets_appSecret'],
                // 返回数据类型 array | xml
                'response_type' => 'array',
            ];
            self::$app = Factory::miniProgram($config);
        }
        return self::$app;
    }

    /**
     * 生成微信小程序链接
     *
     * @param  string  $path
     * @param  string  $sourceCode
     * @param  bool  $tokenRefresh  是否强制刷新access_token 默认false  非特殊条件请勿设为true
     * @return string
     * @throws GuzzleException
     * @throws HttpException
     * @throws InvalidArgumentException
     * @throws InvalidConfigException
     * @throws RuntimeException
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public static function generateActivityUrl(string $path, string $sourceCode, bool $tokenRefresh = false): string
    {
        $accessToken = self::getInstance()->access_token;

        $client = new Client();

        $url = 'https://api.weixin.qq.com/wxa/generate_urllink?access_token=';

        $params         = [];
        $query          = '';
        $params['path'] = 'pages/tabbar/pagehome/pagehome';//首页路径
        if (!empty($path)) {
            $pathArr        = explode('?', $path);
            $query          = isset($pathArr[1]) ? $pathArr[1].'&' : '';
            $params['path'] = $pathArr[0];
        }

        $params['query'] = $query.'channel=activity&source_code='.$sourceCode;
        $jsonStr         = !empty($params) ? json_encode($params, JSON_UNESCAPED_UNICODE) : '';

        Log::info('【小程序链接生成请求参数】'.$jsonStr);

        $response = $client->request('POST', $url.($accessToken->getToken($tokenRefresh)['access_token'] ?? ''), [
            'body' => $jsonStr
        ]);

        $res = json_decode($response->getBody(), true);
        Log::info('【小程序链接生成响应】'.json_encode($res, JSON_UNESCAPED_UNICODE));
        if ($res['errcode'] == 0) {
            return $res['url_link'] ?? '';
        }

        if ($res['errcode'] == 40001 && $tokenRefresh == false) {
            // 可能是token过期 刷新一次
            // tokenRefresh 防止无限循环
            return self::generateActivityUrl($path, $sourceCode, true);
        }

        throw new Exception('链接生成失败 '.$res['errmsg'] ?? '');
    }

    /**
     * 发送订阅消息
     *
     * 注1:订阅消息需要在用户预约后订阅成功后才能发送,每次订阅只能推送一条订阅消息;
     * 注2:用户取消订阅后无法下发订阅消息
     *
     * @param  string  $openid  接收者openid
     * @param  array  $data  ['template_id' => '模版ID', 'msg' => msgBody, 'page' => '小程序跳转页面', 'state' => '小程序类型']
     *  消息体 样例:msgBody=['date1'=>date('Y年m月d日'), 'thing2'=>'植发', 'character_string12'=>'8:30 ~ 9:30']
     *  state  跳转小程序类型。 developer为开发版;trial=体验版;formal为正式版;默认为正式版
     * @return boolean
     * @throws RepositoryException|GuzzleException
     */
    public static function sendSubscribeMessage(string $openid, array $data): bool
    {
        try {
            $wxApp      = self::getInstance();
            $msgBody    = $data['msg'] ?? [];
            $templateId = $data['template_id'] ?? '';
            $page       = $data['page'] ?? '';
            $state      = $data['state'] ?? 'formal';
            $state      = in_array($state, ['developer', 'trial', 'formal']) ? $state : '';
            if (empty($msgBody)) {
                throw new RepositoryException('订阅消息不能为空');
            }

            if (empty($templateId)) {
                throw new RepositoryException('消息模版不能为空');
            }

            $data = [
                // 所需下发的订阅模板id
                'template_id'       => $templateId,
                // 接收者(用户)的 openid
                'touser'            => $openid,
                // 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。
                'page'              => $page,
                //跳转小程序类型:developer=开发版;trial=体验版;formal=正式版;默认为正式版
                'miniprogram_state' => $state,
                // 模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } }
                'data'              => $msgBody
            ];

            Log::info('[发送订阅消息]:'.json_encode($data, JSON_UNESCAPED_UNICODE));
            $res = $wxApp->subscribe_message->send($data);
            Log::info('[发送回执]'.json_encode($res, JSON_UNESCAPED_UNICODE));
            if (isset($res['errcode']) && $res['errcode'] == 0) {
                return true;
            }

            //40003	touser字段openid为空或者不正确
            //40037	订阅模板id为空不正确
            //43101	用户拒绝接受消息,如果用户之前曾经订阅过,则表示用户取消了订阅关系
            //47003	模板参数不准确,可能为空或者不满足规则,errmsg会提示具体是哪个字段出错
            //41030	page路径不正确,需要保证在现网版本小程序中存在,与app.json保持一致
            if ($res['errcode'] == '43101') {
                throw new RepositoryException('用户未订阅或订阅次数已用完');
            }
            throw new RepositoryException('【订阅消息发送失败】code:'.$res['errcode'].' msg:'.$res['errmsg']);
        } catch (RepositoryException $e) {
            throw $e;
        } catch (Exception $e) {
            Log::error('【订阅消息发送失败】:'.$e->getMessage());
            return false;
        }
    }

    /**
     * 批量发送订阅消息
     *
     * 注1:订阅消息需要在用户预约后订阅成功后才能发送,每次订阅只能推送一条订阅消息;
     * 注2:用户取消订阅后无法下发订阅消息
     *
     * @param  array  $openidList  接收者openid列表
     * @param  array  $data  ['template_id' => '模版ID', 'msg' => msgBody, 'page' => '小程序跳转页面', 'state' => '小程序类型', 'message_id' => '消息ID']
     *  消息体 样例:msgBody=['date1'=>date('Y年m月d日'), 'thing2'=>'植发', 'character_string12'=>'8:30 ~ 9:30']
     *  state  跳转小程序类型。 developer为开发版;trial=体验版;formal为正式版;默认为正式版
     * @throws GuzzleException
     * @throws InvalidArgumentException
     * @throws InvalidConfigException
     * @throws RepositoryException
     */
    public static function sendBatchSubscribeMessage(int $messageId, array $openidList, array $data)
    {
        try {
            $wxApp      = self::getInstance();
            $msgBody    = $data['msg'] ?? [];
            $templateId = $data['template_id'] ?? '';
            $page       = $data['page'] ?? '';
            $state      = $data['state'] ?? 'formal';
            $state      = in_array($state, ['developer', 'trial', 'formal']) ? $state : '';
            if (empty($msgBody)) {
                throw new RepositoryException('订阅消息不能为空');
            }

            if (empty($templateId)) {
                throw new RepositoryException('消息模版不能为空');
            }

            if (empty($openidList)) {
                throw new RepositoryException('消息模版接受人列表不能为空');
            }

            $resData = [];
            $now     = date('Y-m-d H:i:s');
            $resInfo = [
                'openid'      => '',
                'status'      => SubscribeMessageLog::STATUS_SUCCESS,
                'message_id'  => $messageId,
                'template_id' => $templateId,
                'created_at'  => $now,
                'data'        => json_encode($msgBody, JSON_UNESCAPED_UNICODE),
            ];
            foreach ($openidList as $openid) {
                $resInfo['openid'] = $openid;
                $sendData          = [
                    // 所需下发的订阅模板id
                    'template_id'       => $templateId,
                    // 接收者(用户)的 openid
                    'touser'            => $openid,
                    // 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。
                    'page'              => $page,
                    //跳转小程序类型:developer=开发版;trial=体验版;formal=正式版;默认为正式版
                    'miniprogram_state' => $state,
                    // 模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } }
                    'data'              => $msgBody
                ];

                Log::info('[发送订阅消息]:'.json_encode($data, JSON_UNESCAPED_UNICODE));
                $res = $wxApp->subscribe_message->send($sendData);
                Log::info('[发送回执]'.json_encode($res, JSON_UNESCAPED_UNICODE));
                if (isset($res['errcode']) && $res['errcode'] == 0) {
                    $resData[] = $resInfo;
                    continue;
                }

                $resInfo['status'] = SubscribeMessageLog::STATUS_FAIL;

                //40003	touser字段openid为空或者不正确
                //40037	订阅模板id为空不正确
                //43101	用户拒绝接受消息,如果用户之前曾经订阅过,则表示用户取消了订阅关系
                //47003	模板参数不准确,可能为空或者不满足规则,errmsg会提示具体是哪个字段出错
                //41030	page路径不正确,需要保证在现网版本小程序中存在,与app.json保持一致
                if ($res['errcode'] == '43101') {
                    $resInfo['remarks'] = '用户未订阅或订阅次数已用完';
                    $resData[]          = $resInfo;
                    continue;
                }

                $resInfo['remarks'] = $res['errcode'].' msg:'.$res['errmsg'];
                $resData[]          = $resInfo;
                (new SubscribeMessageLog())->saveAll($resData);
                throw new RepositoryException('【订阅消息发送失败】code:'.$res['errcode'].' msg:'.$res['errmsg']);
            }
            (new SubscribeMessageLog())->saveAll($resData);
        } catch (RepositoryException $e) {
            throw $e;
        } catch (Exception $e) {
            Log::error('【订阅消息发送失败】:'.$e->getMessage());
            throw $e;
        }
    }

    // 订阅消息模版
    public static function msgTemplateList(): array
    {
        return [
            ['name' => '新活动通知', 'value' => self::SUBSCRIBE_TPL_ACTIVITY],
            ['name' => '未读消息通知', 'value' => self::SUBSCRIBE_TPL_NEW_INFO_NOTICE],
        ];
    }

    // 订阅消息参数列表
    public static function msgTemplateParams(): array
    {
        return [
            self::SUBSCRIBE_TPL_ACTIVITY => [
                [
                    'name'  => '活动名称',
                    'type'  => 'string',
                    'value' => 'thing1',
                ],
                [
                    'name'  => '活动时间',
                    'type'  => 'datetime',
                    'value' => 'time4',
                ],
                [
                    'name'  => '温馨提示',
                    'type'  => 'string',
                    'value' => 'thing5',
                ],
            ],
            self::SUBSCRIBE_TPL_NEW_INFO_NOTICE => [
                [
                    'name'  => '消息内容',
                    'type'  => 'string',
                    'value' => 'thing2',
                ],
                [
                    'name'  => '时间',
                    'type'  => 'datetime',
                    'value' => 'time3',
                ],
                [
                    'name'  => '备注',
                    'type'  => 'string',
                    'value' => 'thing5',
                ],
            ],
        ];
    }
}