<?php
declare(strict_types=1);

namespace app\api\middleware;

use support\Log;
use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;

/**
 * 记录 /api 相关接口的访问参数与响应摘要（敏感头、敏感字段脱敏）
 */
class ApiAccessLogMiddleware implements MiddlewareInterface
{
    private const REQUEST_PARAM_MAX_LEN = 4096;

    private const RESPONSE_BODY_MAX_LEN = 8192;

    /** 请求头名称（小写） */
    private const SENSITIVE_HEADER_NAMES = [
        'api-key',
        'auth-token',
        'token',
        'authorization',
        'cookie',
    ];

    /** 参数键名（小写匹配） */
    private const SENSITIVE_PARAM_KEYS = [
        'password',
        'secret',
        'signature',
        'token',
        'api-key',
        'api_key',
        'auth-token',
        'auth_token',
        'old_token',
    ];

    public function process(Request $request, callable $handler): Response
    {
        $startedAt = microtime(true);
        $response = null;
        $thrown = null;
        try {
            $response = $handler($request);
            return $response;
        } catch (Throwable $e) {
            $thrown = $e;
            throw $e;
        } finally {
            $durationMs = round((microtime(true) - $startedAt) * 1000, 3);
            $requestLog = $this->buildRequestLog($request);
            $responseLog = null;
            if ($response instanceof Response) {
                $responseLog = $this->buildResponseLog($response);
            }

            Log::info('api_access', [
                'request' => $requestLog,
                'response' => $responseLog,
                'duration_ms' => $durationMs,
                'exception' => $thrown ? [
                    'class' => $thrown::class,
                    'message' => $thrown->getMessage(),
                ] : null,
            ]);
        }
    }

    /**
     * @return array<string, mixed>
     */
    private function buildRequestLog(Request $request): array
    {
        $headers = $request->header();
        if (!is_array($headers)) {
            $headers = [];
        }
        $headersOut = [];
        foreach ($headers as $name => $value) {
            $nameStr = strtolower((string) $name);
            if ($this->isSensitiveHeaderName($nameStr)) {
                $headersOut[$nameStr] = $this->maskToken(is_string($value) ? $value : (string) $value);
            } else {
                $headersOut[$nameStr] = $value;
            }
        }

        $get = $request->get();
        $post = $request->post();
        $params = [];
        if (is_array($get)) {
            $params = array_merge($params, $get);
        }
        if (is_array($post)) {
            $params = array_merge($params, $post);
        }
        $params = $this->sanitizeForLog($params);

        $paramsJson = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
        if (!is_string($paramsJson)) {
            $paramsJson = '';
        }
        if (strlen($paramsJson) > self::REQUEST_PARAM_MAX_LEN) {
            $paramsJson = substr($paramsJson, 0, self::REQUEST_PARAM_MAX_LEN) . '...(truncated)';
        }

        return [
            'method' => $request->method(),
            'path' => $request->path(),
            'uri' => $request->uri(),
            'ip' => method_exists($request, 'getRealIp') ? $request->getRealIp() : ($request->getRemoteIp() ?? ''),
            'content_type' => $request->header('content-type', ''),
            'headers' => $headersOut,
            'params' => $paramsJson,
            'agent_id' => $request->agent_id ?? null,
            'player_id' => $request->player_id ?? null,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function buildResponseLog(Response $response): array
    {
        $raw = method_exists($response, 'rawBody') ? $response->rawBody() : '';
        if (!is_string($raw)) {
            $raw = '';
        }
        $bodyForLog = $raw;
        $decoded = json_decode($raw, true);
        if (is_array($decoded)) {
            $sanitized = $this->sanitizeForLog($decoded);
            $encoded = json_encode($sanitized, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
            if (is_string($encoded)) {
                $bodyForLog = $encoded;
            }
        }
        if (strlen($bodyForLog) > self::RESPONSE_BODY_MAX_LEN) {
            $bodyForLog = substr($bodyForLog, 0, self::RESPONSE_BODY_MAX_LEN) . '...(truncated)';
        }

        return [
            'status' => $response->getStatusCode(),
            'body' => $bodyForLog,
        ];
    }

    private function isSensitiveHeaderName(string $name): bool
    {
        return in_array($name, self::SENSITIVE_HEADER_NAMES, true);
    }

    private function isSensitiveParamKey(string $keyLower): bool
    {
        foreach (self::SENSITIVE_PARAM_KEYS as $s) {
            if ($keyLower === $s) {
                return true;
            }
        }
        return false;
    }

    private function maskToken(string $value): string
    {
        $t = trim($value);
        if ($t === '') {
            return '';
        }
        if (strlen($t) <= 12) {
            return '***';
        }
        return substr($t, 0, 8) . '***' . substr($t, -4);
    }

    /**
     * @param mixed $value
     * @return mixed
     */
    private function sanitizeForLog($value)
    {
        if (!is_array($value)) {
            return $value;
        }
        $out = [];
        foreach ($value as $k => $v) {
            $keyLower = is_string($k) ? strtolower($k) : '';
            if ($keyLower !== '' && $this->isSensitiveParamKey($keyLower)) {
                $out[$k] = '***';
                continue;
            }
            if (is_array($v)) {
                $out[$k] = $this->sanitizeForLog($v);
                continue;
            }
            $out[$k] = $v;
        }
        return $out;
    }
}
