<?php

declare(strict_types=1);

namespace App\Middleware;

use App\Model\ApiRequestLog;
use App\Utils\Log;
use App\Utils\RequestHelper;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Qbhy\HyperfAuth\AuthManager;
use Swoole\Coroutine;

class RequestLogMiddleware implements MiddlewareInterface
{
    public function __construct(
        protected AuthManager $auth
    ) {
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $start = microtime(true);

        $response = $handler->handle($request);

        $duration_ms = (int) ((microtime(true) - $start) * 1000);

        // 在父协程中提取 user_id（子协程不继承 Swoole Context）
        $user_id = null;
        try {
            $ctx_request = \Hyperf\Context\Context::get(\Psr\Http\Message\ServerRequestInterface::class);
            $auth_user = $ctx_request?->getAttribute('auth_user');
            $user_id = $auth_user?->getId() ?? $this->auth->guard('jwt')->user()?->getId();
        } catch (\Throwable) {
            // 未认证请求，user_id 保持 null
        }

        $method = $request->getMethod();
        $path = $request->getUri()->getPath();
        $status_code = $response->getStatusCode();
        $ip = RequestHelper::getClientIp($request);
        $user_agent = $request->getHeaderLine('User-Agent') ?: null;

        // GET/DELETE 请求不记录请求体
        $request_body = null;
        if (!in_array($method, ['GET', 'DELETE', 'HEAD', 'OPTIONS'], true)) {
            $parsed_body = $request->getParsedBody();
            if (is_array($parsed_body) && !empty($parsed_body)) {
                $request_body = self::sanitizeBody($parsed_body);
            }
        }

        $response_code = self::extractResponseCode($response);

        // defer 写库：在当前协程退出时执行，响应已返回客户端，同时保证协程内 DB 连接可用
        Coroutine::defer(static function () use (
            $user_id, $method, $path, $status_code, $ip,
            $user_agent, $request_body, $response_code, $duration_ms
        ): void {
            try {
                ApiRequestLog::query()->create([
                    'user_id' => $user_id,
                    'method' => $method,
                    'path' => $path,
                    'status_code' => $status_code,
                    'ip' => $ip,
                    'user_agent' => $user_agent,
                    'request_body' => $request_body,
                    'response_code' => $response_code,
                    'duration_ms' => $duration_ms,
                ]);
            } catch (\Throwable $e) {
                Log::get()->error('RequestLogMiddleware: 写入请求日志失败', [
                    'error' => $e->getMessage(),
                    'path' => $path,
                ]);
            }
        });

        return $response;
    }

    /**
     * 递归替换敏感字段为 ***
     */
    public static function sanitizeBody(array $body): array
    {
        $sensitive_keys = [
            'password', 'old_password', 'new_password', 'password_confirmation',
            'token', 'secret', 'api_key', 'access_token', 'refresh_token',
        ];

        foreach ($body as $key => $value) {
            if (in_array($key, $sensitive_keys, true)) {
                $body[$key] = '***';
            } elseif (is_array($value)) {
                $body[$key] = self::sanitizeBody($value);
            }
        }

        return $body;
    }

    /**
     * 从 JSON 响应体中提取 code 字段
     */
    public static function extractResponseCode(ResponseInterface $response): ?int
    {
        $content_type = $response->getHeaderLine('Content-Type');
        if (stripos($content_type, 'application/json') === false) {
            return null;
        }

        try {
            $stream = $response->getBody();
            $body = (string) $stream;
            // 回退流指针，避免后续读取得到空内容
            if ($stream->isSeekable()) {
                $stream->rewind();
            }
            $data = json_decode($body, true);
            if (is_array($data) && isset($data['code'])) {
                return (int) $data['code'];
            }
        } catch (\Throwable) {
            // 解析失败返回 null
        }

        return null;
    }

}
