<?php
declare(strict_types=1);

namespace App\Support;

use Throwable;

final class FileLogger
{
    public function __construct(private readonly string $directory = '') {}

    public function info(string $message, array $context = []): void
    {
        $this->write('info', $message, null, $context);
    }

    public function warning(string $message, array $context = []): void
    {
        $this->write('warning', $message, null, $context);
    }

    public function error(string $message, ?Throwable $throwable = null, array $context = []): void
    {
        $this->write('error', $message, $throwable, $context);
    }

    /** @return array{path:string,directory_exists:bool,directory_writable:bool,file_writable:bool} */
    public function diagnostics(): array
    {
        $directory = $this->directory();
        $file = $directory . DIRECTORY_SEPARATOR . 'app.log';

        return [
            'path' => $file,
            'directory_exists' => is_dir($directory),
            'directory_writable' => is_dir($directory) && is_writable($directory),
            'file_writable' => !is_file($file) || is_writable($file),
        ];
    }

    private function write(string $level, string $message, ?Throwable $throwable, array $context): void
    {
        $directory = $this->directory();
        $entry = [
            'time' => gmdate('c'),
            'level' => $level,
            'message' => trim($message),
            'context' => $this->redact($context),
        ];
        if ($throwable !== null) {
            $chain = [];
            $current = $throwable;
            // Connection::connect wraps the real PDOException; without the walk the
            // log records only "Database connection failed."
            for ($depth = 0; $current !== null && $depth < 5; $depth++) {
                $chain[] = get_class($current) . ': ' . $current->getMessage();
                $current = $current->getPrevious();
            }
            $entry['exception'] = implode(' <- ', $chain);
        }

        $line = json_encode(
            $entry,
            JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR
        );
        if (!is_string($line)) {
            $line = '{"level":"error","message":"unencodable log entry"}';
        }

        if (!is_dir($directory) && !@mkdir($directory, 0750, true) && !is_dir($directory)) {
            error_log('[Quaeriva] ' . $line);
            return;
        }

        $file = $directory . DIRECTORY_SEPARATOR . 'app.log';
        // Rotate once at 2 MB; one previous file is enough to diagnose and it
        // cannot exhaust a shared-hosting quota.
        if (is_file($file) && filesize($file) > 2097152) {
            @rename($file, $file . '.1');
        }

        if (@file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX) === false) {
            error_log('[Quaeriva] ' . $line);
        }
    }

    private function directory(): string
    {
        return $this->directory !== '' ? $this->directory : app_root_path('storage', 'logs');
    }

    private function redact(mixed $value, string $key = ''): mixed
    {
        if ($key !== '' && preg_match('/(?:password|passwd|secret|token|cookie|authorization|session|csrf)/i', $key) === 1) {
            return '[redacted]';
        }
        if (!is_array($value)) {
            return $value;
        }

        $safe = [];
        foreach ($value as $itemKey => $itemValue) {
            $safe[$itemKey] = $this->redact($itemValue, (string) $itemKey);
        }

        return $safe;
    }
}
