<?php

declare(strict_types=1);

namespace App\Infrastructure\Logging;

use Throwable;

/**
 * Produces a stringified backtrace that, unlike Throwable::getTraceAsString(),
 * never inlines call-arguments into frames (SEC_REVIEW F21).
 *
 * PHP's built-in stringifier renders scalar args directly, so an exception
 * thrown from `password_verify($plaintext, $hash)` writes the plaintext
 * password into the trace; SecretScrubbingProcessor only matches known token
 * shapes (irdb_*, $argon2$, bcrypt) and won't catch arbitrary plaintext
 * passwords or generic OIDC client_secret values. This formatter keeps
 * file/line/class/function so the trace stays useful for debugging, but
 * drops args entirely.
 *
 * Walks `$previous` chains so wrapped exceptions don't smuggle args back in
 * via "Caused by" sections.
 */
final class SafeTrace
{
    public static function format(Throwable $e): string
    {
        $out = self::formatOne($e);
        $prev = $e->getPrevious();
        while ($prev !== null) {
            $out .= "\nCaused by: " . $prev::class . ': ' . $prev->getMessage()
                . "\n" . self::formatOne($prev);
            $prev = $prev->getPrevious();
        }

        return $out;
    }

    private static function formatOne(Throwable $e): string
    {
        $lines = [];
        $i = 0;
        foreach ($e->getTrace() as $frame) {
            $location = isset($frame['file'])
                ? $frame['file'] . '(' . ($frame['line'] ?? '?') . ')'
                : '[internal function]';
            $call = ($frame['class'] ?? '') . ($frame['type'] ?? '') . $frame['function'];
            $lines[] = '#' . $i . ' ' . $location . ': ' . $call . '()';
            $i++;
        }
        $lines[] = '#' . $i . ' {main}';

        return implode("\n", $lines);
    }
}
