<?php
declare(strict_types=1);

namespace App\Security;

use App\Core\Database;
use App\Core\TenantContext;
use PDO;
use RuntimeException;

/** Contrôles anti-IDOR et anti-accès inter-organisation. */
final class OrganizationGuard
{
    /**
     * Vérifie qu'un enregistrement appartient à l'organisation active.
     * Le nom de table/colonne doit provenir du code, jamais de l'utilisateur.
     */
    public static function requireOwnedRecord(
        string $table,
        int $id,
        string $idColumn = 'id',
        string $organisationColumn = 'organisation_id',
        bool $allowDeleted = false
    ): array {
        self::assertIdentifier($table);
        self::assertIdentifier($idColumn);
        self::assertIdentifier($organisationColumn);
        if ($id <= 0) {
            self::notFound();
        }

        $deletedFilter = $allowDeleted ? '' : self::deletedFilter($table);
        $sql = sprintf(
            'SELECT * FROM `%s` WHERE `%s`=:id AND `%s`=:organisation_id%s LIMIT 1',
            $table,
            $idColumn,
            $organisationColumn,
            $deletedFilter
        );
        $stmt = Database::connection()->prepare($sql);
        $stmt->execute(['id' => $id, 'organisation_id' => TenantContext::id()]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            self::notFound();
        }
        return $row;
    }

    public static function requireRelation(
        string $childTable,
        int $childId,
        string $parentTable,
        string $childForeignKey,
        int $parentId
    ): void {
        $child = self::requireOwnedRecord($childTable, $childId);
        $parent = self::requireOwnedRecord($parentTable, $parentId);
        if ((int)($child[$childForeignKey] ?? 0) !== (int)($parent['id'] ?? 0)) {
            self::notFound();
        }
    }

    /** Ajoute systématiquement organisation_id à un jeu de paramètres d'écriture. */
    public static function scopedValues(array $values): array
    {
        if (isset($values['organisation_id']) && (int)$values['organisation_id'] !== TenantContext::id()) {
            self::notFound();
        }
        $values['organisation_id'] = TenantContext::id();
        return $values;
    }

    private static function deletedFilter(string $table): string
    {
        static $cache = [];
        if (array_key_exists($table, $cache)) {
            return $cache[$table] ? ' AND `deleted_at` IS NULL' : '';
        }
        $stmt = Database::connection()->prepare(
            'SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=:table AND COLUMN_NAME=\'deleted_at\''
        );
        $stmt->execute(['table' => $table]);
        $cache[$table] = (bool)$stmt->fetchColumn();
        return $cache[$table] ? ' AND `deleted_at` IS NULL' : '';
    }

    private static function assertIdentifier(string $value): void
    {
        if (!preg_match('/^[A-Za-z0-9_]+$/', $value)) {
            throw new RuntimeException('Identifiant SQL de garde invalide.');
        }
    }

    private static function notFound(): never
    {
        // 404 volontaire pour ne pas confirmer l'existence d'une ressource externe.
        http_response_code(404);
        exit('Ressource introuvable.');
    }
}
