<?php
declare(strict_types=1);

namespace App\Audit;

use PDO;
use Throwable;

final class AuditLogger
{
    public function __construct(private readonly PDO $pdo) {}

    /** @param array<string,mixed>|null $before @param array<string,mixed>|null $after */
    public function log(?int $actorUserId, string $action, ?string $resourceType = null, ?int $resourceId = null, ?array $before = null, ?array $after = null, ?string $ip = null): void
    {
        $action = substr(trim($action), 0, 120);
        if ($action === '') return;
        $resourceType = $resourceType === null ? null : substr(trim($resourceType), 0, 64);
        $statement = $this->pdo->prepare(
            'INSERT INTO audit_log (actor_user_id,action,resource_type,resource_id,before_json,after_json,ip_hash,created_at) '
            . 'VALUES (:actor,:action,:type,:resource_id,:before_json,:after_json,:ip_hash,NOW())'
        );
        $statement->execute([
            'actor' => $actorUserId !== null && $actorUserId > 0 ? $actorUserId : null,
            'action' => $action,
            'type' => $resourceType === '' ? null : $resourceType,
            'resource_id' => $resourceId !== null && $resourceId > 0 ? $resourceId : null,
            'before_json' => $this->encode($before),
            'after_json' => $this->encode($after),
            'ip_hash' => $this->hashIp($ip),
        ]);
    }

    /** @param array<string,mixed>|null $metadata */
    public function moderation(?int $actorUserId, string $action, string $resourceType, int $resourceId, ?string $reason = null, ?array $metadata = null): void
    {
        if ($resourceId <= 0) return;
        $statement = $this->pdo->prepare(
            'INSERT INTO moderation_actions (actor_user_id,action,resource_type,resource_id,reason,metadata_json,created_at) '
            . 'VALUES (:actor,:action,:type,:resource_id,:reason,:metadata,NOW())'
        );
        $statement->execute([
            'actor' => $actorUserId !== null && $actorUserId > 0 ? $actorUserId : null,
            'action' => substr(trim($action), 0, 64),
            'type' => substr(trim($resourceType), 0, 64),
            'resource_id' => $resourceId,
            'reason' => $reason === null || trim($reason) === '' ? null : $this->unicodeSubstr(trim($reason), 0, 500),
            'metadata' => $this->encode($metadata),
        ]);
    }

    /** @param array<string,mixed>|null $value */
    private function encode(?array $value): ?string
    {
        if ($value === null || $value === []) return null;
        try {
            $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
            return strlen($json) > 30000
                ? json_encode(['truncated' => true], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
                : $json;
        } catch (Throwable) {
            return null;
        }
    }

    private function hashIp(?string $ip): ?string
    {
        $ip = trim((string) $ip);
        if ($ip === '') return null;
        $salt = (string) (getenv('APP_KEY') ?: getenv('APP_URL') ?: 'quaeriva-audit');
        return hash('sha256', $salt . '|' . $ip);
    }

    private function unicodeSubstr(string $value, int $start, int $length): string
    {
        return function_exists('mb_substr') ? mb_substr($value, $start, $length, 'UTF-8') : substr($value, $start, $length);
    }
}
