<?php

declare(strict_types=1);

namespace app\common\middleware;

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

/**
 * 接口调用日志中间件
 *
 * 目标：
 * - 统一记录接口调用（请求 + 响应）
 * - 成功记录返回参数
 * - 失败记录失败原因
 */
class InterfaceRequestLog implements MiddlewareInterface
{
    /**
     * 仅记录接口路由（避免静态资源噪音）
     */
    private const API_PATH_PREFIXES = ['api/', 'admin/'];

    /**
     * 需要脱敏的字段名（不区分大小写）
     */
    private const SENSITIVE_KEYS = [
        'password',
        'oldpassword',
        'newpassword',
        'token',
        'user-token',
        'user_token',
        'auth-token',
        'auth_token',
        'refresh_token',
        'secret',
        'signature',
        'sign',
    ];

    public function process(Request $request, callable $handler): Response
    {
        $path = trim($request->path(), '/');
        if (!$this->shouldLogPath($path)) {
            return $handler($request);
        }

        $start = microtime(true);
        $requestPayload = $this->buildRequestPayload($request);

        try {
            $response = $handler($request);
        } catch (Throwable $e) {
            $costMs = $this->costMs($start);
            Log::error('[InterfaceRequestLog] ' . $this->encodeJson([
                'path' => '/' . $path,
                'method' => strtoupper($request->method()),
                'ip' => $request->getRealIp(),
                'cost_ms' => $costMs,
                'request' => $requestPayload,
                'success' => false,
                'fail_reason' => $e->getMessage(),
                'exception_class' => get_class($e),
                'exception_trace' => $this->truncateText($e->getTraceAsString(), 3000),
            ]));
            throw $e;
        }

        $costMs = $this->costMs($start);
        $statusCode = method_exists($response, 'getStatusCode') ? intval($response->getStatusCode()) : 200;
        $responseBodyRaw = $this->extractResponseBody($response);
        $responseDecoded = $this->decodeJsonArray($responseBodyRaw);

        $success = false;
        $failReason = '';
        $responseData = null;

        if (is_array($responseDecoded)) {
            if (array_key_exists('code', $responseDecoded)) {
                $success = intval($responseDecoded['code']) === 1;
            } else {
                $success = $statusCode >= 200 && $statusCode < 400;
            }
            $responseData = $responseDecoded['data'] ?? $responseDecoded;
            if (!$success) {
                $failReasonRaw = $responseDecoded['message'] ?? ($responseDecoded['msg'] ?? '');
                $failReason = is_string($failReasonRaw) ? $failReasonRaw : strval($failReasonRaw);
            }
        } else {
            $success = $statusCode >= 200 && $statusCode < 400;
            if ($success) {
                $responseData = $this->truncateText($responseBodyRaw, 2000);
            } else {
                $failReason = $this->truncateText($responseBodyRaw, 2000);
            }
        }

        $logPayload = [
            'path' => '/' . $path,
            'method' => strtoupper($request->method()),
            'ip' => $request->getRealIp(),
            'status_code' => $statusCode,
            'cost_ms' => $costMs,
            'request' => $requestPayload,
            'success' => $success,
        ];

        if ($success) {
            $logPayload['response_data'] = $this->maskMixed($responseData);
            Log::info('[InterfaceRequestLog] ' . $this->encodeJson($logPayload));
        } else {
            $logPayload['fail_reason'] = $failReason !== '' ? $failReason : 'Unknown error';
            if (is_array($responseDecoded)) {
                $logPayload['api_response'] = $this->maskMixed($responseDecoded);
            }
            Log::error('[InterfaceRequestLog] ' . $this->encodeJson($logPayload));
        }

        return $response;
    }

    private function shouldLogPath(string $path): bool
    {
        foreach (self::API_PATH_PREFIXES as $prefix) {
            if (str_starts_with($path, $prefix)) {
                return true;
            }
        }
        return false;
    }

    private function buildRequestPayload(Request $request): array
    {
        $query = $request->get();
        $post = $request->post();
        if (!is_array($query)) {
            $query = [];
        }
        if (!is_array($post)) {
            $post = [];
        }

        $rawBody = method_exists($request, 'rawBody') ? strval($request->rawBody()) : '';
        $jsonBody = [];
        if ($rawBody !== '') {
            $jsonDecoded = json_decode($rawBody, true);
            if (is_array($jsonDecoded)) {
                $jsonBody = $jsonDecoded;
            }
        }

        return [
            'query' => $this->maskMixed($query),
            'post' => $this->maskMixed($post),
            'json' => $this->maskMixed($jsonBody),
        ];
    }

    private function extractResponseBody(Response $response): string
    {
        if (method_exists($response, 'rawBody')) {
            return strval($response->rawBody());
        }
        return '';
    }

    private function decodeJsonArray(string $content): ?array
    {
        if ($content === '') {
            return null;
        }
        $decoded = json_decode($content, true);
        return is_array($decoded) ? $decoded : null;
    }

    private function maskMixed($value, ?string $parentKey = null)
    {
        if (is_array($value)) {
            $result = [];
            foreach ($value as $k => $v) {
                $keyName = is_string($k) ? $k : $parentKey;
                $result[$k] = $this->maskMixed($v, $keyName);
            }
            return $result;
        }

        if ($parentKey !== null && $this->isSensitiveKey($parentKey)) {
            return '***';
        }
        return $value;
    }

    private function isSensitiveKey(string $key): bool
    {
        $normalized = strtolower(trim($key));
        foreach (self::SENSITIVE_KEYS as $sensitive) {
            if ($normalized === $sensitive) {
                return true;
            }
            if (str_contains($normalized, $sensitive)) {
                return true;
            }
        }
        return false;
    }

    private function truncateText(string $text, int $maxLen): string
    {
        if ($maxLen <= 0) {
            return '';
        }
        if (mb_strlen($text) <= $maxLen) {
            return $text;
        }
        return mb_substr($text, 0, $maxLen) . '...';
    }

    private function encodeJson(array $payload): string
    {
        return json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    private function costMs(float $start): int
    {
        return intval((microtime(true) - $start) * 1000);
    }
}
