<?php
// +----------------------------------------------------------------------
// | onezan-admin 快速开发的管理平台
// +----------------------------------------------------------------------
// | 官方网址：https://www.onezan.cn
// +----------------------------------------------------------------------
// | onezan团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: 543885307@qq.com
// +----------------------------------------------------------------------
namespace app\service\domain\channel;

use Alipay\EasySDK\Kernel\Factory as AlipayFactory;
use app\model\User;
use app\model\UserChannelAccount;
use app\repository\channel\ChannelRepository;
use app\repository\channel\UserChannelAccountRepository;
use app\repository\user\UserRepository;
use app\service\IdService;
use Core\service\BaseCoreService;
use app\service\domain\channel\driver\wechat\EasywechatFactory;
use think\facade\Db;
use think\facade\Log;

/**
 * 渠道认证服务
 * Class CoreChannelAuthService
 * @package app\service\domain\channel
 */
class CoreChannelAuthService extends BaseCoreService
{
    /**
     * 微信小程序登录
     * @param string $code
     * @param int $actorUserId
     * @return array
     */
    public function loginWithWeappCode(string $code, int $actorUserId = 0): array
    {
        $code = trim($code);
        if ($code === '') {
            return ['code' => 1, 'msg' => 'Unknown error'];
        }

        $runtime = $this->getChannelRuntime('weapp', ['app_id', 'app_secret']);
        if ((int)($runtime['code'] ?? 0) !== 0) {
            return $runtime;
        }

        try {
            $session = $this->sdkFactory()->miniApp($runtime['config'])->getUtils()->codeToSession($code);
        } catch (\Throwable $e) {
            Log::error('channel weapp login failed: ' . $e->getMessage(), ['channel' => 'weapp']);
            return ['code' => 1, 'msg' => 'Unknown error'];
        }

        return $this->loginOrBind([
            'channel' => 'weapp',
            'provider_uid' => (string)($session['openid'] ?? ''),
            'openid' => (string)($session['openid'] ?? ''),
            'unionid' => (string)($session['unionid'] ?? ''),
            'session_key' => (string)($session['session_key'] ?? ''),
            'extra' => [
                'code' => $code,
                'session' => $session,
            ],
        ], $actorUserId);
    }

    /**
     * 微信小程序手机号登录
     * @param string $code
     * @param string $iv
     * @param string $encryptedData
     * @param int $actorUserId
     * @return array
     */
    public function loginWithWeappPhoneCode(string $code, string $iv, string $encryptedData, int $actorUserId = 0): array
    {
        $code = trim($code);
        if ($code === '') {
            return ['code' => 1, 'msg' => 'Unknown error'];
        }

        $iv = trim($iv);
        $encryptedData = trim($encryptedData);
        if ($iv === '' || $encryptedData === '') {
            return ['code' => 1, 'msg' => ''];
        }

        $runtime = $this->getChannelRuntime('weapp', ['app_id', 'app_secret']);
        if ((int)($runtime['code'] ?? 0) !== 0) {
            return $runtime;
        }

        try {
            $session = $this->sdkFactory()->miniApp($runtime['config'])->getUtils()->codeToSession($code);
        } catch (\Throwable $e) {
            Log::error('channel weapp phone login code2session failed: ' . $e->getMessage(), ['channel' => 'weapp']);
            return ['code' => 1, 'msg' => 'Unknown error'];
        }

        $sessionKey = (string)($session['session_key'] ?? '');
        if ($sessionKey === '') {
            return ['code' => 1, 'msg' => ''];
        }

        $decrypted = $this->decryptWeappData($sessionKey, $iv, $encryptedData);
        if ($decrypted === null) {
            return ['code' => 1, 'msg' => 'Unknown error'];
        }

        $phoneNumber = (string)($decrypted['phoneNumber'] ?? $decrypted['purePhoneNumber'] ?? '');
        if ($phoneNumber === '') {
            return ['code' => 1, 'msg' => 'Unknown error'];
        }

        $providerUid = (string)($session['openid'] ?? '');
        $unionid = (string)($session['unionid'] ?? '');

        if ($actorUserId > 0) {
            $targetUser = $this->activeUser($actorUserId);
            if (!$targetUser) {
                return ['code' => 403, 'msg' => 'Forbidden', '_http_status' => 403];
            }
            $isNewUser = false;
        } else {
            $targetUser = $this->userRepository()->findByPhone($phoneNumber);
            if ($targetUser && (int)$targetUser->status !== 1) {
                return ['code' => 1, 'msg' => ''];
            }
            $isNewUser = !$targetUser;
        }

        Db::startTrans();
        try {
            if (!$targetUser) {
                $targetUser = $this->createUserByPhone($phoneNumber);
            } elseif ((string)$targetUser->phone !== $phoneNumber) {
                $targetUser->phone = $phoneNumber;
                $targetUser->save();
            }

            $existingBinding = $this->resolveBinding('weapp', $providerUid, $providerUid, $unionid);
            if ($existingBinding && (int)$existingBinding->user_id !== (int)$targetUser->id) {
                Db::rollback();
                return ['code' => 1, 'msg' => ''];
            }

            $binding = $this->accountRepository()->saveBinding([
                'user_id' => (int)$targetUser->id,
                'channel' => 'weapp',
                'provider_uid' => $providerUid,
                'openid' => $providerUid,
                'unionid' => $unionid,
                'session_key' => $sessionKey,
                'extra' => json_encode([
                    'code' => $code,
                    'session' => $session,
                    'phone' => $phoneNumber,
                ], JSON_UNESCAPED_UNICODE),
            ]);

            Db::commit();
        } catch (\Throwable $e) {
            Db::rollback();
            Log::error('channel weapp phone bind failed: ' . $e->getMessage(), ['channel' => 'weapp']);
            return ['code' => 1, 'msg' => 'Unknown error'];
        }

        return [
            'code' => 0,
            'msg' => $actorUserId > 0 ? '' : '',
            'data' => [
                'user_id' => (int)$targetUser->id,
                'is_new_user' => $isNewUser ? 1 : 0,
                'channel_auth' => $this->buildChannelAuthData($binding),
            ],
        ];
    }

    /**
     * 构建微信授权URL
     * @param string $scope
     * @param string $state
     * @return array
     */
    public function buildWechatOauthUrl(string $scope = 'snsapi_userinfo', string $state = ''): array
    {
        $scope = trim($scope);
        if (!in_array($scope, ['snsapi_base', 'snsapi_userinfo'], true)) {
            $scope = 'snsapi_userinfo';
        }

        $runtime = $this->getChannelRuntime('wechat', ['app_id', 'app_secret', 'oauth_redirect_url']);
        if ((int)($runtime['code'] ?? 0) !== 0) {
            return $runtime;
        }

        try {
            $app = $this->sdkFactory()->officialAccount([
                'app_id' => (string)$runtime['config']['app_id'],
                'app_secret' => (string)$runtime['config']['app_secret'],
                'token' => (string)($runtime['config']['token'] ?? ''),
                'aes_key' => (string)($runtime['config']['aes_key'] ?? ''),
                'oauth' => [
                    'redirect_url' => (string)$runtime['config']['oauth_redirect_url'],
                    'scopes' => [$scope],
                ],
            ]);
            $oauth = $app->getOAuth()->scopes([$scope]);
            if ($state !== '') {
                $oauth->withState($state);
            }
            $url = $oauth->redirect((string)$runtime['config']['oauth_redirect_url']);
        } catch (\Throwable $e) {
            Log::error('channel wechat authorize url build failed: ' . $e->getMessage(), ['channel' => 'wechat']);
            return ['code' => 1, 'msg' => ''];
        }

        return [
            'code' => 0,
            'data' => [
                'channel' => 'wechat',
                'scope' => $scope,
                'state' => $state,
                'authorize_url' => $url,
                'callback_url' => (string)$runtime['config']['oauth_redirect_url'],
            ],
        ];
    }

    /**
     * 微信登录
     * @param string $code
     * @param string $state
     * @param int $actorUserId
     * @return array
     */
    public function loginWithWechatCode(string $code, string $state = '', int $actorUserId = 0): array
    {
        $code = trim($code);
        if ($code === '') {
            return ['code' => 1, 'msg' => 'Unknown error'];
        }

        $runtime = $this->getChannelRuntime('wechat', ['app_id', 'app_secret', 'oauth_redirect_url']);
        if ((int)($runtime['code'] ?? 0) !== 0) {
            return $runtime;
        }

        try {
            $app = $this->sdkFactory()->officialAccount([
                'app_id' => (string)$runtime['config']['app_id'],
                'app_secret' => (string)$runtime['config']['app_secret'],
                'token' => (string)($runtime['config']['token'] ?? ''),
                'aes_key' => (string)($runtime['config']['aes_key'] ?? ''),
                'oauth' => [
                    'redirect_url' => (string)$runtime['config']['oauth_redirect_url'],
                    'scopes' => ['snsapi_userinfo'],
                ],
            ]);
            $oauthUser = $app->getOAuth()->userFromCode($code);
        } catch (\Throwable $e) {
            Log::error('channel wechat callback failed: ' . $e->getMessage(), ['channel' => 'wechat']);
            return ['code' => 1, 'msg' => 'Unknown error'];
        }

        $raw = $oauthUser->getRaw();
        $unionid = (string)($raw['unionid'] ?? '');

        $result = $this->loginOrBind([
            'channel' => 'wechat',
            'provider_uid' => (string)$oauthUser->getId(),
            'openid' => (string)$oauthUser->getId(),
            'unionid' => $unionid,
            'access_token' => (string)($oauthUser->getAccessToken() ?? ''),
            'refresh_token' => (string)($oauthUser->getRefreshToken() ?? ''),
            'expire_time' => $this->resolveExpireTime((int)($oauthUser->getExpiresIn() ?? 0)),
            'nickname' => (string)($oauthUser->getNickname() ?? ''),
            'avatar' => (string)($oauthUser->getAvatar() ?? ''),
            'extra' => [
                'state' => $state,
                'raw' => $raw,
                'token_response' => $oauthUser->getTokenResponse(),
            ],
        ], $actorUserId);

        if ((int)($result['code'] ?? 0) === 0) {
            $result['data']['channel_auth']['state'] = $state;
        }

        return $result;
    }

    /**
     * 支付宝小程序登录
     * @param string $authCode
     * @param int $actorUserId
     * @return array
     */
    public function loginWithAliappAuthCode(string $authCode, int $actorUserId = 0): array
    {
        $authCode = trim($authCode);
        if ($authCode === '') {
            return ['code' => 1, 'msg' => 'Unknown error'];
        }

        $runtime = $this->getChannelRuntime('aliapp', ['app_id', 'private_key', 'alipay_public_key']);
        if ((int)($runtime['code'] ?? 0) !== 0) {
            return $runtime;
        }

        try {
            $this->sdkFactory()->bootAlipay('aliapp');
            $tokenResponse = AlipayFactory::base()->oauth()->getToken($authCode);
            $tokenData = method_exists($tokenResponse, 'toMap') ? $tokenResponse->toMap() : [];
        } catch (\Throwable $e) {
            Log::error('channel aliapp login failed: ' . $e->getMessage(), ['channel' => 'aliapp']);
            return ['code' => 1, 'msg' => ''];
        }

        return $this->loginOrBind([
            'channel' => 'aliapp',
            'provider_uid' => (string)($tokenData['user_id'] ?? ''),
            'access_token' => (string)($tokenData['access_token'] ?? ''),
            'refresh_token' => (string)($tokenData['refresh_token'] ?? ''),
            'expire_time' => $this->resolveExpireTime((int)($tokenData['expires_in'] ?? 0)),
            'extra' => [
                'auth_code' => $authCode,
                'token_response' => $tokenData,
            ],
        ], $actorUserId);
    }

    /**
     * 登录或绑定渠道账号
     * 根据渠道身份信息查找或创建用户，并保存渠道绑定关系
     * @param array $identity 渠道身份信息（channel/provider_uid/openid/unionid等）
     * @param int $actorUserId 操作者用户ID（>0时为指定用户绑定，0时为自动登录/注册）
     * @return array
     */
    private function loginOrBind(array $identity, int $actorUserId = 0): array
    {
        $channel = trim((string)($identity['channel'] ?? ''));
        $providerUid = trim((string)($identity['provider_uid'] ?? ''));
        if ($channel === '' || $providerUid === '') {
            return ['code' => 1, 'msg' => 'Unknown error'];
        }

        $existingBinding = $this->resolveBinding(
            $channel,
            $providerUid,
            (string)($identity['openid'] ?? ''),
            (string)($identity['unionid'] ?? '')
        );

        if ($actorUserId > 0) {
            $targetUser = $this->activeUser($actorUserId);
            if (!$targetUser) {
                return ['code' => 403, 'msg' => '', '_http_status' => 403];
            }
            if ($existingBinding && (int)$existingBinding->user_id !== (int)$targetUser->id) {
                return ['code' => 1, 'msg' => ''];
            }
            $needCreateUser = false;
            $isNewUser = false;
        } else {
            if ($existingBinding) {
                $targetUser = $this->activeUser((int)$existingBinding->user_id);
                if (!$targetUser) {
                    return ['code' => 1, 'msg' => ''];
                }
                $needCreateUser = false;
                $isNewUser = false;
            } else {
                $targetUser = null;
                $needCreateUser = true;
                $isNewUser = true;
            }
        }

        Db::startTrans();
        try {
            if ($needCreateUser) {
                $targetUser = $this->createUserByIdentity($channel, $identity);
            }

            $binding = $this->accountRepository()->saveBinding([
                'user_id' => (int)$targetUser->id,
                'channel' => $channel,
                'provider_uid' => $providerUid,
                'openid' => (string)($identity['openid'] ?? ''),
                'unionid' => (string)($identity['unionid'] ?? ''),
                'session_key' => (string)($identity['session_key'] ?? ''),
                'access_token' => (string)($identity['access_token'] ?? ''),
                'refresh_token' => (string)($identity['refresh_token'] ?? ''),
                'expire_time' => (int)($identity['expire_time'] ?? 0),
                'extra' => json_encode($identity['extra'] ?? [], JSON_UNESCAPED_UNICODE),
            ]);
            Db::commit();
        } catch (\Throwable $e) {
            Db::rollback();
            Log::error('channel auth bind failed: ' . $e->getMessage(), ['channel' => $channel, 'provider_uid' => $providerUid]);
            return ['code' => 1, 'msg' => ''];
        }

        return [
            'code' => 0,
            'msg' => $actorUserId > 0 ? '' : '',
            'data' => [
                'user_id' => (int)$targetUser->id,
                'is_new_user' => $isNewUser ? 1 : 0,
                'channel_auth' => $this->buildChannelAuthData($binding),
            ],
        ];
    }

    /**
     * 解析渠道绑定
     * @param string $channel
     * @param string $providerUid
     * @param string $openid
     * @param string $unionid
     * @return UserChannelAccount|null
     */
    private function resolveBinding(string $channel, string $providerUid, string $openid, string $unionid): ?UserChannelAccount
    {
        $binding = $this->accountRepository()->findByProviderUid($channel, $providerUid);
        if ($binding) {
            return $binding;
        }

        if ($openid !== '') {
            $binding = $this->accountRepository()->findByOpenid($channel, $openid);
            if ($binding) {
                return $binding;
            }
        }

        if ($unionid !== '') {
            return $this->accountRepository()->findByUnionid($unionid);
        }

        return null;
    }

    /**
     * 创建用户
     * @param string $channel
     * @param array $identity
     * @return User
     */
    private function createUserByIdentity(string $channel, array $identity): User
    {
        $nickname = $this->resolveNickname($channel, (string)($identity['nickname'] ?? ''));
        $username = $this->generateUsername($channel, (string)($identity['provider_uid'] ?? ''));

        return $this->userRepository()->create([
            'username' => $username,
            'password' => password_hash(bin2hex(random_bytes(16)), PASSWORD_DEFAULT),
            'nickname' => $nickname,
            'avatar' => (string)($identity['avatar'] ?? ''),
            'invite_code' => IdService::generateUniqueInviteCode(),
            'status' => 1,
        ]);
    }

    /**
     * 解析昵称
     * @param string $channel
     * @param string $nickname
     * @return string
     */
    private function resolveNickname(string $channel, string $nickname): string
    {
        $nickname = trim($nickname);
        if ($nickname !== '') {
            return mb_substr($nickname, 0, 50);
        }

        $meta = $this->catalog()->channels()[$channel] ?? null;
        $prefix = (string)($meta['name'] ?? '');
        return mb_substr($prefix . '', 0, 50);
    }

    /**
     * 生成用户名
     * @param string $channel
     * @param string $providerUid
     * @return string
     */
    private function generateUsername(string $channel, string $providerUid): string
    {
        $prefix = strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $channel) ?: 'channel');
        $seed = strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $providerUid));
        $seed = $seed !== '' ? substr($seed, -12) : bin2hex(random_bytes(6));

        for ($i = 0; $i < 30; $i++) {
            $suffix = $i === 0 ? $seed : $seed . substr(bin2hex(random_bytes(3)), 0, 6);
            $username = substr($prefix . '_' . $suffix, 0, 50);
            if (!$this->userRepository()->findByUsername($username)) {
                return $username;
            }
        }

        return substr($prefix . '_' . date('YmdHis') . mt_rand(1000, 9999), 0, 50);
    }

    /**
     * 解析过期时间
     * @param int $expiresIn
     * @return int
     */
    private function resolveExpireTime(int $expiresIn): int
    {
        return $expiresIn > 0 ? (time() + $expiresIn) : 0;
    }

    /**
     * 获取渠道运行时配置
     * @param string $channel
     * @param array $requiredFields
     * @return array
     */
    private function getChannelRuntime(string $channel, array $requiredFields): array
    {
        $profile = $this->channelRepository()->getProfile($channel);
        if ($profile && (int)($profile['status'] ?? 0) !== 1) {
            return ['code' => 1, 'msg' => 'Unknown error'];
        }

        $config = $this->channelRepository()->getConfig($channel, 'base');
        foreach ($requiredFields as $field) {
            if (trim((string)($config[$field] ?? '')) === '') {
                return ['code' => 1, 'msg' => ''];
            }
        }

        return ['code' => 0, 'config' => $config];
    }

    /**
     * 构建渠道认证数据
     * @param UserChannelAccount $binding
     * @return array
     */
    private function buildChannelAuthData(UserChannelAccount $binding): array
    {
        return [
            'channel' => (string)($binding->channel ?? ''),
            'provider_uid' => (string)($binding->provider_uid ?? ''),
            'openid' => (string)($binding->openid ?? ''),
            'unionid' => (string)($binding->unionid ?? ''),
            'expire_time' => (int)($binding->expire_time ?? 0),
        ];
    }

    /**
     * 解密微信小程序数据
     * @param string $sessionKey
     * @param string $iv
     * @param string $encryptedData
     * @return array|null
     */
    private function decryptWeappData(string $sessionKey, string $iv, string $encryptedData): ?array
    {
        try {
            $key = base64_decode($sessionKey);
            $decodedIv = base64_decode($iv);
            $ciphertext = base64_decode($encryptedData);

            if ($key === false || $decodedIv === false || $ciphertext === false) {
                return null;
            }

            $decrypted = openssl_decrypt($ciphertext, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $decodedIv);
            if ($decrypted === false) {
                return null;
            }

            $data = json_decode($decrypted, true);
            return is_array($data) ? $data : null;
        } catch (\Throwable $e) {
            Log::error('weapp data decrypt failed: ' . $e->getMessage());
            return null;
        }
    }

    /**
     * 创建用户手机号
     * @param string $phone
     * @return User
     */
    private function createUserByPhone(string $phone): User
    {
        return $this->userRepository()->create([
            'username' => $this->generateUsername('weapp', $phone),
            'password' => password_hash(bin2hex(random_bytes(16)), PASSWORD_DEFAULT),
            'nickname' => '',
            'phone' => $phone,
            'invite_code' => IdService::generateUniqueInviteCode(),
            'status' => 1,
        ]);
    }

    /**
     * 激活用户
     * @param int $userId
     * @return User|null
     */
    private function activeUser(int $userId): ?User
    {
        if ($userId <= 0) {
            return null;
        }

        $user = $this->userRepository()->findById($userId);
        if (!$user || (int)($user->status ?? 0) !== 1) {
            return null;
        }

        return $user;
    }

    /**
     * 获取用户仓储实例
     * @return UserRepository
     */
    private function userRepository(): UserRepository
    {
        return $this->app->make(UserRepository::class);
    }

    /**
     * 获取用户渠道账号仓储实例
     * @return UserChannelAccountRepository
     */
    private function accountRepository(): UserChannelAccountRepository
    {
        return $this->app->make(UserChannelAccountRepository::class);
    }

    /**
     * 获取渠道仓储实例
     * @return ChannelRepository
     */
    private function channelRepository(): ChannelRepository
    {
        return $this->app->make(ChannelRepository::class);
    }

    /**
     * 获取微信SDK工厂实例
     * @return EasywechatFactory
     */
    private function sdkFactory(): EasywechatFactory
    {
        return $this->app->make(EasywechatFactory::class);
    }

    /**
     * 获取渠道目录服务实例
     * @return CoreChannelCatalogService
     */
    private function catalog(): CoreChannelCatalogService
    {
        return $this->app->make(CoreChannelCatalogService::class);
    }
}
