<?php
declare(strict_types=1);
namespace App\Services\Policy;

use App\Core\Database;
use App\Services\Compatibility\PackageCompatibilityService;
use App\Services\Package\PackageDescriptor;
use App\Services\Trust\TrustEngineService;
use PDO;
use RuntimeException;

final class PackagePolicyService
{
    public function evaluatePackageId(int $packageId, ?int $userId = null, bool $persist = true): array
    {
        if ($packageId <= 0) {
            throw new RuntimeException('Identifiant de package invalide.');
        }
        $stmt = Database::connection()->prepare('SELECT fichier_original FROM cda_update_packages WHERE id=:id LIMIT 1');
        $stmt->execute(['id' => $packageId]);
        $name = $stmt->fetchColumn();
        if (!is_string($name) || $name === '') {
            throw new RuntimeException('Package introuvable.');
        }
        return $this->evaluatePackageFile(
            BASE_PATH.'/storage/updates/packages/'.basename($name),
            $packageId,
            $userId,
            $persist
        );
    }

    public function evaluatePackageFile(string $zipPath, ?int $packageId = null, ?int $userId = null, bool $persist = true): array
    {
        $descriptor = PackageDescriptor::fromZip($zipPath, $packageId);
        $environment = $this->environment();
        $policy = $this->activePolicy($environment);
        $trust = (new TrustEngineService())->evaluatePackageFile($zipPath, $packageId, $userId, false);
        $compatibility = (new PackageCompatibilityService())->evaluatePackageFile($zipPath, $packageId, $userId, false);
        $certificate = $packageId !== null ? $this->activeCertificate($packageId) : null;

        $issues = [];
        $warnings = [];
        $trustScore = (int)($trust['score'] ?? 0);
        $compatibilityScore = (int)($compatibility['score'] ?? 0);
        $trustDecision = (string)($trust['decision'] ?? 'REJECTED');
        $compatibilityDecision = (string)($compatibility['decision'] ?? 'INCOMPATIBLE');

        $minimumTrust = (int)$policy['min_trust_score'];
        $minimumCompatibility = (int)$policy['min_compatibility_score'];
        $warningsAllowed = (bool)$policy['allow_warnings'];
        $isLocal = $environment === 'local';

        // Une décision technique explicite reste toujours bloquante.
        if ($trustDecision === 'REJECTED') {
            $issues[] = 'Le Trust Engine rejette ce package.';
        }
        if ($compatibilityDecision === 'INCOMPATIBLE') {
            $issues[] = 'Le Compatibility Engine déclare ce package incompatible.';
        }

        // En local, une note sous le seuil devient un avertissement lorsque la
        // politique autorise explicitement les avertissements. En recette et
        // production, les seuils restent strictement bloquants.
        if ($trustScore < $minimumTrust) {
            $message = 'Score de confiance '.$trustScore.'/100 inférieur au seuil '.$minimumTrust.'/100.';
            if ($isLocal && $warningsAllowed && $trustDecision !== 'REJECTED') {
                $warnings[] = $message;
            } else {
                $issues[] = $message;
            }
        }
        if ($compatibilityScore < $minimumCompatibility) {
            $message = 'Score de compatibilité '.$compatibilityScore.'/100 inférieur au seuil '.$minimumCompatibility.'/100.';
            if ($isLocal && $warningsAllowed && $compatibilityDecision !== 'INCOMPATIBLE') {
                $warnings[] = $message;
            } else {
                $issues[] = $message;
            }
        }
        if (!(bool)$policy['allow_warnings'] && ($trustDecision === 'WARNING' || $compatibilityDecision === 'WARNING')) {
            $issues[] = 'Les packages avec avertissements sont interdits par la politique.';
        } elseif ($trustDecision === 'WARNING' || $compatibilityDecision === 'WARNING') {
            $warnings[] = 'Le package comporte des avertissements acceptés par la politique.';
        }

        if ((bool)$policy['require_certificate']) {
            if ($packageId === null) {
                $issues[] = 'La certification obligatoire sera contrôlée après l’enregistrement interne du package.';
            } elseif (!$certificate) {
                $issues[] = 'Un certificat actif est obligatoire dans cet environnement.';
            } elseif (($certificate['status'] ?? '') !== 'ACTIVE') {
                $issues[] = 'Le certificat du package n’est pas actif.';
            } elseif (($certificate['decision'] ?? '') === 'REJECTED') {
                $issues[] = 'Le certificat porte une décision de rejet.';
            }
        }

        $publisher = $descriptor->publisher();
        $trustLevel = $descriptor->trustLevel();
        $allowedPublishers = $this->decodeList((string)$policy['allowed_publishers_json']);
        $allowedAuthorities = $this->decodeList((string)$policy['allowed_authorities_json']);
        if ($allowedPublishers && ($publisher === '' || !in_array($publisher, $allowedPublishers, true))) $issues[] = 'Éditeur non autorisé par la politique.';
        if ((bool)$policy['block_development_packages'] && in_array($trustLevel, ['development', 'dev', 'local'], true)) $issues[] = 'Les packages de développement sont interdits dans cet environnement.';
        if ($certificate && $allowedAuthorities && !in_array((string)($certificate['authority_code'] ?? ''), $allowedAuthorities, true)) $issues[] = 'Autorité de certification non autorisée.';

        $decision = $issues ? 'DENIED' : ($warnings ? 'WARNING' : 'ALLOWED');
        $score = max(0, min(100, (int)round(($trustScore * 0.5) + ($compatibilityScore * 0.4) + ($certificate ? 10 : 0))));
        $result = [
            'decision' => $decision,
            'score' => $score,
            'environment' => $environment,
            'policy' => $policy,
            'policy_runtime' => [
                'policy_id' => (int)$policy['id'],
                'policy_name' => (string)$policy['name'],
                'minimum_trust' => $minimumTrust,
                'minimum_compatibility' => $minimumCompatibility,
                'allow_warnings' => $warningsAllowed,
                'source' => 'system_package_policies',
            ],
            'package' => $descriptor->packageRecord(),
            'descriptor' => ['package' => $descriptor->name(), 'version' => $descriptor->version(), 'publisher' => $publisher, 'checksum' => $descriptor->checksum()],
            'trust' => $trust,
            'compatibility' => $compatibility,
            'certificate' => $certificate,
            'publisher' => $publisher,
            'issues' => array_values(array_unique($issues)),
            'warnings' => array_values(array_unique($warnings)),
            'evaluated_at' => gmdate('c'),
        ];
        if ($persist && $packageId !== null) $this->persistDecision($packageId, $userId, $result);
        return $result;
    }

    public function dashboard(): array
    {
        $pdo = Database::connection();
        $policies = $pdo->prepare('SELECT * FROM system_package_policies ORDER BY environment_code, priority DESC, id');
        $policies->execute();
        $decisions = $pdo->prepare('SELECT d.*, p.nom_package, p.version_cible FROM system_package_policy_decisions d LEFT JOIN cda_update_packages p ON p.id=d.package_id ORDER BY d.id DESC LIMIT 100');
        $decisions->execute();
        return ['environment'=>$this->environment(),'policies'=>$policies->fetchAll(PDO::FETCH_ASSOC)?:[],'decisions'=>$decisions->fetchAll(PDO::FETCH_ASSOC)?:[],'generated_at'=>date('Y-m-d H:i:s')];
    }

    public function savePolicy(array $input, ?int $userId): void
    {
        $environment = strtolower(trim((string)($input['environment_code'] ?? 'local')));
        if (!in_array($environment, ['local','staging','production'], true)) throw new RuntimeException('Environnement non autorisé.');
        $name = trim((string)($input['name'] ?? ''));
        if ($name === '') throw new RuntimeException('Le nom de la politique est obligatoire.');
        $id = (int)($input['id'] ?? 0);
        $params=['name'=>$name,'environment'=>$environment,'active'=>isset($input['active'])?1:0,'priority'=>max(0,min(999,(int)($input['priority']??100))),'min_trust'=>max(0,min(100,(int)($input['min_trust_score']??80))),'min_compatibility'=>max(0,min(100,(int)($input['min_compatibility_score']??80))),'require_certificate'=>isset($input['require_certificate'])?1:0,'allow_warnings'=>isset($input['allow_warnings'])?1:0,'block_development'=>isset($input['block_development_packages'])?1:0,'publishers'=>$this->encodeCsv((string)($input['allowed_publishers']??'')),'authorities'=>$this->encodeCsv((string)($input['allowed_authorities']??'')),'user_id'=>$userId];
        if($id>0){$params['id']=$id;$stmt=Database::connection()->prepare('UPDATE system_package_policies SET name=:name,environment_code=:environment,active=:active,priority=:priority,min_trust_score=:min_trust,min_compatibility_score=:min_compatibility,require_certificate=:require_certificate,allow_warnings=:allow_warnings,block_development_packages=:block_development,allowed_publishers_json=:publishers,allowed_authorities_json=:authorities,updated_by=:user_id,updated_at=NOW() WHERE id=:id');}
        else{$stmt=Database::connection()->prepare('INSERT INTO system_package_policies(name,environment_code,active,priority,min_trust_score,min_compatibility_score,require_certificate,allow_warnings,block_development_packages,allowed_publishers_json,allowed_authorities_json,created_by,updated_by,created_at,updated_at) VALUES(:name,:environment,:active,:priority,:min_trust,:min_compatibility,:require_certificate,:allow_warnings,:block_development,:publishers,:authorities,:user_id,:user_id,NOW(),NOW())');}
        $stmt->execute($params);
    }

    public function togglePolicy(int $id, bool $active, ?int $userId): void
    {
        $stmt=Database::connection()->prepare('UPDATE system_package_policies SET active=:active,updated_by=:user_id,updated_at=NOW() WHERE id=:id');$stmt->execute(['active'=>$active?1:0,'user_id'=>$userId,'id'=>$id]);if($stmt->rowCount()!==1)throw new RuntimeException('Politique introuvable.');
    }

    private function activePolicy(string $environment): array
    {
        $stmt=Database::connection()->prepare('SELECT * FROM system_package_policies WHERE environment_code=:environment AND active=1 ORDER BY priority DESC,id DESC LIMIT 1');$stmt->execute(['environment'=>$environment]);$policy=$stmt->fetch(PDO::FETCH_ASSOC);if(!$policy)throw new RuntimeException('Aucune politique active pour l’environnement '.$environment.'.');return $policy;
    }
    private function activeCertificate(int $packageId): ?array{$stmt=Database::connection()->prepare("SELECT * FROM system_package_certificates WHERE package_id=:package_id AND status='ACTIVE' ORDER BY id DESC LIMIT 1");$stmt->execute(['package_id'=>$packageId]);$row=$stmt->fetch(PDO::FETCH_ASSOC);return $row?:null;}
    private function persistDecision(int $packageId, ?int $userId, array $result): void
    {
        $json=json_encode($result,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);if(!is_string($json))throw new RuntimeException('Rapport de politique invalide.');
        $stmt=Database::connection()->prepare('INSERT INTO system_package_policy_decisions(package_id,policy_id,environment_code,decision,policy_score,issues_json,warnings_json,report_json,evaluated_by,created_at) VALUES(:package_id,:policy_id,:environment,:decision,:score,:issues,:warnings,:report,:user_id,NOW())');
        $stmt->execute(['package_id'=>$packageId,'policy_id'=>(int)$result['policy']['id'],'environment'=>(string)$result['environment'],'decision'=>(string)$result['decision'],'score'=>(int)$result['score'],'issues'=>json_encode($result['issues'],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'warnings'=>json_encode($result['warnings'],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'report'=>$json,'user_id'=>$userId]);
    }
    private function environment(): string{$value=strtolower(trim((string)($_ENV['APP_ENV']??getenv('APP_ENV')?:'local')));return in_array($value,['production','prod'],true)?'production':(in_array($value,['staging','stage','recette'],true)?'staging':'local');}
    private function decodeList(string $json): array{$value=json_decode($json,true);return is_array($value)?array_values(array_filter(array_map('strval',$value),static fn(string $v):bool=>$v!=='')):[];}
    private function encodeCsv(string $csv): string{$items=array_values(array_unique(array_filter(array_map('trim',preg_split('/[,;\r\n]+/',$csv)?:[]),static fn(string $v):bool=>$v!=='')));return (string)json_encode($items,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);}
}
