<?php
namespace app\mobile\controller;

use app\BaseController;
use think\facade\Cache;
use think\facade\Db;
use think\facade\Log;
use think\exception\HttpResponseException;

/** 新小程序共用鉴权；数据库使用 TP6 默认连接。 */
abstract class OmsBase extends BaseController
{
    protected const CLOTHES_SIZES = ['S','M','L','XL','XXL','XXXL','XXXXL','XXXXXL'];
    protected function templateService() { return app()->make(\app\mobile\service\WechatTemplateService::class); }
    protected function cache() { return Cache::store('redis'); }
    protected function cacheKey($key) {
        // 服务号切换后不能复用上一服务号的 access_token。
        if (substr($key,-10)===':gzh-token') $key.=':'.env('wxoms.gzh_appid','');
        return 'wxoms-v3:' . $this->app->config->get('database.connections.' . $this->app->config->get('database.default') . '.database') . ':' . $key;
    }
    protected function fail($message, $status = 400, $code = 1)
    {
        throw new HttpResponseException(json(['code' => $code, 'message' => $message, 'data' => null], $status));
    }
    protected function requireMethod($method)
    {
        if ($this->request->method() !== $method) $this->fail('请求方式不正确', 405);
    }
    protected function requiredText($key, $max = 128, $trim = true)
    {
        $value = $this->request->post($key);
        if (!is_string($value) || strlen($value) > $max) $this->fail('请求参数不正确');
        if ($trim) $value = trim($value);
        if ($value === '') $this->fail('请填写完整信息');
        return $value;
    }
    protected function verifyPassword($password, $hash)
    {
        if (!$hash) return false;
        if (preg_match('/^[a-f0-9]{32}$/i', $hash)) return hash_equals(strtolower($hash), md5(trim($password)));
        return password_verify($password, $hash);
    }

    /** 机动工及后台停用人员按在职员工认证；后台停用不授予管理权限。 */
    protected function normalizeAccount($account)
    {
        $admin = Db::name('admin_user')->where('username',$account['username'])->find();
        if ($admin && ((int)$admin['job'] === 79 || (int)$admin['status'] !== 1)) {
            $member = Db::name('member_list')->where(['eid'=>$account['username'],'status'=>1])->find();
            $account['user_type'] = 1;
            $account['user_id'] = (int)($member['id'] ?? 0);
            $account['admin_id'] = 0;
        }
        return $account;
    }
    protected function account($id)
    {
        $row = Db::name('auth')->find($id);
        if (!$row) $this->fail('登录已失效，请重新认证', 401);
        $row = $this->normalizeAccount($row);
        $this->assertActive($row, true);
        return $row;
    }
    protected function assertActive($account, $normalized = false)
    {
        if (!$normalized) $account = $this->normalizeAccount($account);
        if ((int)$account['status'] !== 1) {
            $this->fail('账号已停用，请联系管理员', 401);
        }
        $table = (int)$account['user_type'] === 1 ? 'member_list' : 'admin_user';
        $user = Db::name($table)->find((int)$account['user_type'] === 1 ? $account['user_id'] : $account['admin_id']);
        $employeeNo = $user ? ((int)$account['user_type'] === 1 ? $user['eid'] : $user['username']) : '';
        if (!$user || (int)$user['status'] !== 1 || $employeeNo !== $account['username']) $this->fail('账号已离职或停用，无法使用', 401);
    }
    protected function bearer()
    {
        $header = $this->request->header('authorization', '');
        // Apache FastCGI / 内部重写可能把凭证保存在服务器变量中。
        if ($header === '') $header = $this->request->server('HTTP_AUTHORIZATION', '');
        if ($header === '') $header = $this->request->server('REDIRECT_HTTP_AUTHORIZATION', '');
        if (!preg_match('/^Bearer ([a-f0-9]{64})$/', $header, $match)) $this->fail('请先登录', 401);
        return $match[1];
    }
    protected function current($allowPending = false)
    {
        $token = $this->bearer();
        $session = $this->cache()->get($this->cacheKey('session:' . hash('sha256', $token)));
        if (!$session || $session['expires_at'] <= time()) $this->fail('登录已过期，请重新登录', 401);
        // 已下线的模拟会话必须明确拒绝，不能退化成目标账号的普通登录。
        if (array_key_exists('test_actor', $session)) {
            $this->cache()->delete($this->cacheKey('session:' . hash('sha256', $token)));
            $this->fail('测试身份功能已停用，请重新登录本人账号', 401);
        }
        $account = $this->account($session['id']);
        if ((int)$account['version'] !== (int)$session['version']) $this->fail('登录已失效，请重新登录', 401);
        return $account;
    }
    protected function issue($account)
    {
        $account = $this->normalizeAccount($account);
        $token = bin2hex(random_bytes(32));
        $expires = time() + 7 * 86400;
        $this->cache()->set($this->cacheKey('session:' . hash('sha256', $token)), ['id' => (int)$account['id'], 'version' => (int)$account['version'], 'expires_at' => $expires], 7 * 86400);
        return ['token' => $token, 'expiresAt' => $expires, 'profile' => $this->profileData($account)];
    }
    protected function profileData($account)
    {
        $account = $this->normalizeAccount($account);
        $member = Db::name('member_list')->where('id', $account['user_id'])->where('status', 1)->find();
        $team = $member ? Db::name('team_list')->find($member['tid']) : null;
        $type = (int)$account['user_type'];
        $clothesSize = Db::name('member_base')->where(['mid'=>20,'title'=>$account['username']])->order('id','desc')->value('content');
        $result = [
            'id' => (int)$account['id'], 'userId' => (int)$account['user_id'], 'adminId' => (int)$account['admin_id'], 'employeeNo' => $account['username'],
            'userType' => $type, 'roleName' => $type === 1 ? '现场员工' : '管理人员',
            'name' => '', 'department' => '', 'team' => $team['team'] ?? '', 'position' => '',
            'leader' => '', 'phone' => '', 'email' => '', 'extension' => '',
            'clothesSize' => in_array($clothesSize,self::CLOTHES_SIZES,true) ? $clothesSize : (self::CLOTHES_SIZES[(int)($account['size'] ?? 0)-1] ?? ''),
            'gzhBound' => trim((string)($account['gzhOpenid'] ?? '')) !== '',
            'mustChangePassword' => false,
        ];
        if ($type === 1) {
            $detail = Db::name('member_interview')->where('m_id', $account['user_id'])->field('phone,size')->find();
            $position = $member ? Db::name('team_plan_position')->where('id', $member['poid'])->value('position') : '';
            $result['name'] = $member['ename'] ?? '';
            $result['department'] = $team['team'] ?? '';
            $result['position'] = $position ?: '';
            $result['phone'] = $detail['phone'] ?? '';
            $result['leader'] = $team ? (Db::name('admin_user')->where('id', $team['aid'])->value('truename') ?: '') : '';
        } else {
            $admin = Db::name('admin_user')->find($account['admin_id']);
            $result['name'] = $admin['truename'];
            $result['department'] = Db::name('admin_org')->where('id', $admin['dpt'])->value('title') ?: '';
            $result['position'] = Db::name('admin_org')->where('id', $admin['job'])->value('title') ?: '';
            $result['leader'] = Db::name('admin_user')->where('id', $admin['leader'])->value('truename') ?: '';
            $result['phone'] = $admin['phone'] ?: '';
            $result['email'] = $admin['e_mail'] && $admin['e_mail'] !== '0' ? $admin['e_mail'] : '';
            $result['extension'] = $admin['ext'] ?: '';
        }
        if (($account['name'] ?? '') !== $result['name']) Db::name('auth')->where('id', $account['id'])->update(['name'=>$result['name']]);
        return $result;
    }
    protected function wechatRequest($url, $params)
    {
        $ch = curl_init($url . '?' . http_build_query($params));
        curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_CONNECTTIMEOUT=>5, CURLOPT_TIMEOUT=>12, CURLOPT_SSL_VERIFYPEER=>true, CURLOPT_SSL_VERIFYHOST=>2]);
        $caFile = $this->app->getRootPath() . 'config/cacert.pem';
        if (is_file($caFile)) curl_setopt($ch, CURLOPT_CAINFO, $caFile);
        $body = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_errno($ch);
        curl_close($ch);
        if ($curlError) {
            Log::error('wxoms wechat transport: curl_errno=' . $curlError . ', http_status=' . $status);
            if ($curlError === 60 || $curlError === 77) $this->fail('服务器 HTTPS 证书校验失败，请配置 config/cacert.pem（cURL ' . $curlError . '）', 502);
            $this->fail('服务器连接微信失败，请检查服务器网络（cURL ' . $curlError . '）', 502);
        }
        $data = $body ? json_decode($body, true) : null;
        if ($status !== 200 || !is_array($data)) $this->fail('微信接口响应异常（HTTP ' . $status . '），请稍后重试', 502);
        if (!empty($data['errcode'])) {
            $errorCode = (int)$data['errcode'];
            Log::error('wxoms wechat API: errcode=' . $errorCode);
            $messages = [
                40013=>'后端小程序 AppID 不正确，请核对配置',
                40125=>'后端小程序 AppSecret 不正确，请核对配置',
                40029=>'微信登录 code 无效，请重新点击认证；同时确认前后端小程序 AppID 一致',
                40163=>'微信登录 code 已使用，请重新点击认证',
                45011=>'微信登录请求过于频繁，请稍后再试',
                -1=>'微信服务暂时繁忙，请稍后再试',
            ];
            $this->fail(($messages[$errorCode] ?? '微信身份验证失败，请核对后端微信配置') . '（微信错误码 ' . $errorCode . '）', 502);
        }
        if (empty($data['openid'])) $this->fail('微信接口未返回用户身份，请重新认证', 502);
        return $data;
    }
    protected function wechatOpenid($code)
    {
        $appid = env('wxoms.appid', ''); $secret = env('wxoms.secret', '');
        if (!$appid || !$secret) $this->fail('微信登录尚未配置，请联系管理员', 503);
        $data = $this->wechatRequest('https://api.weixin.qq.com/sns/jscode2session', ['appid'=>$appid, 'secret'=>$secret, 'js_code'=>$code, 'grant_type'=>'authorization_code']);
        return $data['openid'];
    }
    protected function lockRegistration($action)
    {
        // ponytail: 注册和绑定共用数据库锁；单机低频认证足够，多实例高吞吐时拆分为账户锁。
        $lock = 'wxoms-v2-auth:' . $this->app->config->get('database.connections.' . $this->app->config->get('database.default') . '.database');
        $result = Db::query('SELECT GET_LOCK(?, 5) AS acquired', [$lock]);
        if ((int)$result[0]['acquired'] !== 1) $this->fail('系统繁忙，请稍后重试', 503);
        try { return $action(); }
        finally { Db::query('SELECT RELEASE_LOCK(?)', [$lock]); }
    }
    protected function guarded($action)
    {
        try {
            $mutation=\app\mobile\service\MessageInbox::mutation(get_class($this),strtolower($this->request->action()),$this->request);
            $inbox=null;$pending=[];$account=null;
            if($mutation)try {
                $account=$this->current();$inbox=new \app\mobile\service\MessageInbox($this->app);$inbox->sync($account,[$mutation[0]]);
                $pending=$inbox->pendingFor($account['id'],$mutation[0],$mutation[1]);
            } catch(\Throwable $error) { Log::warning('oms inbox preflight unavailable'); }
            $response=$action();
            if($inbox && $account)try {
                $inbox->sync($account,[$mutation[0]]);$inbox->finish($account['id'],$pending,strtolower($this->request->action()));
            } catch(\Throwable $error) { Log::warning('oms inbox completion unavailable'); }
            $business=\app\mobile\service\MessageInbox::business(get_class($this));
            if($business && $this->request->method()==='POST' && (in_array(strtolower($this->request->action()),['approve','withdraw','confirm','cancel','feedback','publish'],true) || ($business==='check-issue' && in_array(strtolower($this->request->action()),['assign','reject'],true))))try {
                (new \app\mobile\service\MessageInbox($this->app))->captureResult($business,(int)$this->request->post('id'));
            } catch(\Throwable $error) { Log::warning('oms inbox result unavailable'); }
            if($this->request->method()==='POST')try { \app\mobile\service\MessageInbox::invalidate(); } catch(\Throwable $error) { Log::warning('oms inbox cache invalidation unavailable'); }
            return $response;
        }
        catch (HttpResponseException $exception) { throw $exception; }
        catch (\Throwable $exception) {
            $reference = uniqid();
            $driverCode = 0;
            $missingField = '';
            $missingTable = '';
            $message = '服务暂不可用，请稍后重试';
            if ($exception instanceof \think\db\exception\DbException && preg_match('/^fields not exists:\[([a-zA-Z_][a-zA-Z0-9_]{0,63})\]$/', $exception->getMessage(), $fieldMatch)) $missingField = $fieldMatch[1];
            if ($missingField !== '') {
                // 从实际失败的查询对象取表名，不输出查询数据、绑定参数或SQL。
                foreach ($exception->getTrace() as $frame) {
                    foreach ($frame['args'] ?? [] as $argument) {
                        if (!$argument instanceof \think\db\BaseQuery) continue;
                        $table = $argument->getTable();
                        if (is_array($table)) $table = is_string(key($table)) ? key($table) : current($table);
                        if (!is_string($table)) continue;
                        $table = str_replace('`', '', $table);
                        if (preg_match('/^[a-zA-Z_][a-zA-Z0-9_]{0,63}(?:\.[a-zA-Z_][a-zA-Z0-9_]{0,63})?$/', $table)) { $missingTable = $table; break 2; }
                    }
                }
            }
            if ($exception instanceof \think\db\exception\PDOException) {
                $details = $exception->getData();
                $driverCode = (int)($details['PDO Error Info']['Driver Error Code'] ?? 0);
            } elseif ($exception instanceof \PDOException) {
                $driverCode = (int)($exception->errorInfo[1] ?? 0);
            }
            if ($exception instanceof \RedisException || ($exception instanceof \BadFunctionCallException && $exception->getMessage() === 'not support: redis')) {
                $message = '服务器 Redis 不可用，请检查 Redis 服务、连接配置和 PHP redis 扩展';
            } elseif ($missingField !== '') {
                $message = '数据库字段结构不一致，' . ($missingTable !== '' ? '表 ' . $missingTable . ' ' : '') . '无法写入字段 ' . $missingField . '；请核对表结构及字段缓存';
            } elseif ($driverCode) {
                $messages = [
                    1045=>'数据库账号或密码不正确，请检查后端数据库配置',
                    1049=>'配置的数据库不存在，请检查数据库名称',
                    1146=>'数据库缺少所需数据表，请检查 oms_auth 和业务表是否已迁移',
                    1054=>'数据库缺少所需字段，请检查 oms_auth 表结构是否完整',
                    2002=>'服务器无法连接数据库，请检查 MySQL 服务及连接配置',
                    2003=>'服务器无法连接数据库，请检查 MySQL 服务及连接配置',
                ];
                $message = ($messages[$driverCode] ?? '数据库操作失败，请查看服务器日志') . '（MySQL ' . $driverCode . '）';
            }
            // 只记录分类与代码位置，不记录密码、微信 code、token、SQL 或异常原文。
            Log::error('wxoms failure: ref=' . $reference . ', type=' . get_class($exception) . ', driver_code=' . $driverCode . ($missingTable !== '' ? ', table=' . $missingTable : '') . ($missingField !== '' ? ', missing_field=' . $missingField : '') . ', origin=' . basename($exception->getFile()) . ':' . $exception->getLine());
            return json(['code'=>500, 'message'=>$message . '（故障编号 ' . $reference . '）', 'data'=>null], 503);
        }
    }
}
