<?php
declare(strict_types=1);
namespace App\Platform\Health;

use App\Core\Database;
use App\Services\DatabaseHealthService;
use App\Services\SecurityEventMonitor;
use Throwable;
use ZipArchive;

final class HealthMonitorService
{
    public function report(bool $persist = true): array
    {
        $results = [];
        foreach ($this->registry()->all() as $check) {
            try { $results[] = $check->run(); }
            catch (Throwable) { $results[] = new HealthResult($check->key(), $check->key(), HealthStatus::CRITICAL, 0, 0, 'Contrôle indisponible.'); }
        }
        if ($persist) { $this->persist($results); $this->persistMetrics($results); }
        $score = (new HealthScoreCalculator())->calculate($results);
        $counts = ['ok'=>0,'warning'=>0,'critical'=>0];
        foreach ($results as $result) { $counts[$result->status->value]++; }
        return [
            'score'=>$score,
            'status'=>$score >= 85 ? 'ok' : ($score >= 60 ? 'warning' : 'critical'),
            'checked_at'=>date('Y-m-d H:i:s'),
            'counts'=>$counts,
            'components'=>array_map(static fn(HealthResult $r): array=>$r->toArray(), $results),
            'history'=>$this->history(),
            'trends'=>$this->trends(),
            'alerts'=>$this->alerts(),
            'metrics'=>$this->metrics(),
            'integrity'=>$this->integritySummary(),
        ];
    }

    public function acknowledgeAlerts(): void
    {
        try { Database::connection()->exec("UPDATE system_health_alerts SET acknowledged_at=NOW() WHERE acknowledged_at IS NULL"); } catch (Throwable) {}
    }

    private function registry(): HealthRegistry
    {
        $registry = new HealthRegistry();
        $registry->register($this->callback('database','Base SQL',20,function(): array {
            $start=microtime(true); $r=(new DatabaseHealthService())->report(); $ms=(int)round((microtime(true)-$start)*1000);
            $status=($r['status']??'warning')==='ok'?HealthStatus::OK:HealthStatus::WARNING;
            return [$status,$status===HealthStatus::OK?100:70,$ms,(string)($r['message']??'État SQL inconnu'),['version'=>$r['server_version']??'','tables'=>$r['tables']??0]];
        }));
        $registry->register($this->callback('runtime_integrity','Intégrité Runtime',18,function(): array {
            $file=BASE_PATH.'/storage/private/runtime-integrity/state.json';
            if(!is_file($file)) return [HealthStatus::WARNING,65,0,'Aucun contrôle Runtime enregistré.',[]];
            $state=json_decode((string)file_get_contents($file),true)?:[]; $bad=(int)($state['anomalies']??$state['failed']??0);
            return [$bad===0?HealthStatus::OK:HealthStatus::CRITICAL,$bad===0?100:15,0,$bad===0?'Runtime conforme.':'Anomalies Runtime détectées.',['anomalies'=>$bad,'last_scan'=>$state['checked_at']??$state['created_at']??'']];
        }));
        $registry->register($this->callback('security_stack','Pile de sécurité native',14,function(): array {
            $classes=['App\\Security\\ZeroTrustEngine','App\\Security\\MfaService','App\\Services\\SecurityEventMonitor']; $missing=[];
            foreach($classes as $class) if(!class_exists($class)) $missing[]=$class;
            return [empty($missing)?HealthStatus::OK:HealthStatus::CRITICAL,empty($missing)?100:20,0,empty($missing)?'Services critiques disponibles.':'Services critiques manquants.',['missing'=>implode(', ',$missing)]];
        }));
        $registry->register($this->callback('mfa','Couverture MFA',10,function(): array {
            $pdo=Database::connection();
            $exists=(bool)$pdo->query("SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='cda_user_mfa'")->fetchColumn();
            if(!$exists) return [HealthStatus::CRITICAL,0,0,'Table MFA absente.',[]];
            $enabled=(int)$pdo->query('SELECT COUNT(*) FROM cda_user_mfa WHERE enabled_at IS NOT NULL')->fetchColumn();
            return [$enabled>0?HealthStatus::OK:HealthStatus::WARNING,$enabled>0?100:65,0,$enabled.' compte(s) protégé(s).',['enabled_accounts'=>$enabled]];
        }));
        $registry->register($this->callback('sensitive_files','Fichiers sensibles',10,function(): array {
            $issues=[];
            $required=[BASE_PATH.'/.env',BASE_PATH.'/config/update_signing.php'];
            foreach($required as $file) if(!is_file($file)) $issues[]=basename($file).' absent';
            $protectedStorage=BASE_PATH.'/storage/private';
            if(!is_dir($protectedStorage)) $issues[]='stockage protégé absent';
            if(is_dir($protectedStorage) && DIRECTORY_SEPARATOR==='/' && ((fileperms($protectedStorage)&0007)!==0)) $issues[]='stockage protégé accessible aux autres utilisateurs';
            return [empty($issues)?HealthStatus::OK:HealthStatus::WARNING,empty($issues)?100:60,0,empty($issues)?'Configuration sensible présente et stockage protégé isolé.':'Points de vigilance détectés.',['issues'=>implode(' · ',$issues)]];
        }));
        $registry->register($this->callback('backups','Sauvegardes',10,function(): array {
            $files=[]; foreach([BASE_PATH.'/storage/backups',BASE_PATH.'/backups'] as $dir) if(is_dir($dir)) foreach(glob($dir.'/*')?:[] as $f) if(is_file($f)) $files[]=$f;
            usort($files,static fn($a,$b)=>filemtime($b)<=>filemtime($a));
            if(!$files) return [HealthStatus::WARNING,50,0,'Aucune sauvegarde détectée.',[]];
            $latest=$files[0]; $age=time()-(int)filemtime($latest); $coherent=true;
            if(strtolower(pathinfo($latest,PATHINFO_EXTENSION))==='zip' && class_exists(ZipArchive::class)) { $z=new ZipArchive(); $coherent=$z->open($latest)===true; if($coherent)$z->close(); }
            $status=!$coherent?HealthStatus::CRITICAL:($age<=172800?HealthStatus::OK:HealthStatus::WARNING);
            return [$status,!$coherent?20:($age<=172800?100:65),0,(!$coherent?'Archive illisible.':'Dernière sauvegarde : '.date('d/m/Y H:i',(int)filemtime($latest))),['file'=>basename($latest),'size_bytes'=>filesize($latest)?:0,'age_hours'=>(int)round($age/3600)]];
        }));
        $registry->register($this->callback('permissions','Permissions critiques',7,function(): array {
            $pdo=Database::connection(); $count=(int)$pdo->query("SELECT COUNT(*) FROM permissions WHERE code IN ('system.health.view','system.health.manage')")->fetchColumn();
            return [$count===2?HealthStatus::OK:HealthStatus::CRITICAL,$count===2?100:0,0,$count===2?'Permissions Health Monitor complètes.':'Permissions Health Monitor incomplètes.',['found'=>$count,'expected'=>2]];
        }));
        $registry->register($this->callback('scheduled_tasks','Tâches planifiées',4,function(): array {
            $candidates=[BASE_PATH.'/config/tasks.php',BASE_PATH.'/config/scheduler.php',BASE_PATH.'/storage/private/cron.last']; $found=[];
            foreach($candidates as $f) if(is_file($f))$found[]=basename($f);
            return [empty($found)?HealthStatus::WARNING:HealthStatus::OK,empty($found)?65:100,0,empty($found)?'Aucun marqueur de planification détecté.':'Planification détectée.',['markers'=>implode(', ',$found)]];
        }));
        $registry->register($this->callback('storage','Stockages AXIOM',4,function(): array {
            $bad=[]; foreach(['private','logs','updates','sessions'] as $name){$dir=BASE_PATH.'/storage/'.$name;if(!is_dir($dir)||!is_writable($dir))$bad[]=$name;}
            return [empty($bad)?HealthStatus::OK:HealthStatus::CRITICAL,empty($bad)?100:20,0,empty($bad)?'Stockages opérationnels.':'Stockages indisponibles.',['unavailable'=>implode(', ',$bad)]];
        }));
        $registry->register($this->callback('file_integrity','Intégrité des fichiers critiques',12,function(): array {
            $summary=$this->integritySummary();
            if(($summary['baseline_count']??0)===0) return [HealthStatus::WARNING,60,0,'Aucune baseline d’intégrité enregistrée.',['baseline_count'=>0]];
            $changed=(int)($summary['changed']??0); $missing=(int)($summary['missing']??0); $added=(int)($summary['added']??0);
            $issues=$changed+$missing+$added;
            $status=$issues===0?HealthStatus::OK:($missing>0?HealthStatus::CRITICAL:HealthStatus::WARNING);
            $score=$issues===0?100:max(10,100-($issues*12));
            return [$status,$score,0,$issues===0?'Baseline conforme.':'Écarts d’intégrité détectés.',['baseline_count'=>$summary['baseline_count']??0,'changed'=>$changed,'missing'=>$missing,'added'=>$added]];
        }));
        $registry->register($this->callback('platform_runtime','Runtime PHP et performances',8,function(): array {
            $memory=memory_get_usage(true); $peak=memory_get_peak_usage(true); $limit=$this->parseBytes((string)ini_get('memory_limit'));
            $ratio=$limit>0?($peak/$limit*100):0; $status=$ratio>=90?HealthStatus::CRITICAL:($ratio>=75?HealthStatus::WARNING:HealthStatus::OK);
            return [$status,$status===HealthStatus::OK?100:($status===HealthStatus::WARNING?70:20),0,'PHP '.PHP_VERSION.' · pic mémoire '.number_format($peak/1048576,1,',',' ').' Mo.',['php_version'=>PHP_VERSION,'memory_bytes'=>$memory,'peak_bytes'=>$peak,'memory_limit_bytes'=>$limit,'usage_percent'=>round($ratio,1)]];
        }));
        $registry->register($this->callback('disk','Espace disque',3,function(): array {
            $free=@disk_free_space(BASE_PATH); $total=@disk_total_space(BASE_PATH); $pct=($free!==false&&$total>0)?($free/$total*100):0;
            $status=$pct<5?HealthStatus::CRITICAL:($pct<15?HealthStatus::WARNING:HealthStatus::OK);
            return [$status,(int)min(100,max(0,$pct*4)),0,number_format($pct,1,',',' ').' % libres.',['free_bytes'=>$free?:0,'total_bytes'=>$total?:0]];
        }));
        return $registry;
    }

    private function callback(string $key,string $label,int $weight,callable $fn): HealthCheckInterface
    {
        return new class($key,$label,$weight,$fn) implements HealthCheckInterface {
            public function __construct(private string $keyValue,private string $label,private int $weight,private $fn){}
            public function key(): string{return $this->keyValue;}
            public function run(): HealthResult{$start=microtime(true);[$status,$score,$ms,$message,$details]=($this->fn)();$duration=$ms?:((int)round((microtime(true)-$start)*1000));return new HealthResult($this->keyValue,$this->label,$status,$score,$duration,$message,$details,$this->weight);}
        };
    }

    private function persist(array $results): void
    {
        try {
            $pdo=Database::connection(); $pdo->beginTransaction();
            $history=$pdo->prepare('INSERT INTO system_health_history(component,status,score,duration_ms,message,created_at) VALUES(:component,:status,:score,:duration_ms,:message,NOW())');
            $alert=$pdo->prepare("INSERT INTO system_health_alerts(component,severity,message,first_seen_at,last_seen_at,occurrences) VALUES(:component,:severity,:message,NOW(),NOW(),1) ON DUPLICATE KEY UPDATE severity=VALUES(severity),message=VALUES(message),last_seen_at=NOW(),occurrences=occurrences+1,acknowledged_at=NULL");
            foreach($results as $r){
                $history->execute(['component'=>$r->component,'status'=>$r->status->value,'score'=>$r->score,'duration_ms'=>$r->durationMs,'message'=>mb_substr($r->message,0,500)]);
                if($r->status!==HealthStatus::OK){$alert->execute(['component'=>$r->component,'severity'=>$r->status->value,'message'=>mb_substr($r->message,0,500)]);}
                if($r->status===HealthStatus::CRITICAL) SecurityEventMonitor::record('platform.health.critical','critical',['component'=>$r->component]);
            }
            $pdo->commit();
        } catch(Throwable) { try { if(isset($pdo)&&$pdo->inTransaction())$pdo->rollBack(); } catch(Throwable) {} }
    }

    private function history(): array { try{$stmt=Database::connection()->query("SELECT component,status,score,duration_ms,message,created_at FROM system_health_history ORDER BY id DESC LIMIT 150");return $stmt->fetchAll()?:[];}catch(Throwable){return [];} }
    private function alerts(): array { try{$stmt=Database::connection()->query("SELECT component,severity,message,occurrences,first_seen_at,last_seen_at FROM system_health_alerts WHERE acknowledged_at IS NULL ORDER BY FIELD(severity,'critical','warning'),last_seen_at DESC LIMIT 50");return $stmt->fetchAll()?:[];}catch(Throwable){return [];} }
    private function trends(): array { try{$stmt=Database::connection()->query("SELECT DATE(created_at) day,ROUND(AVG(score),1) average_score,SUM(status='critical') critical_count,SUM(status='warning') warning_count FROM system_health_history WHERE created_at>=DATE_SUB(NOW(),INTERVAL 7 DAY) GROUP BY DATE(created_at) ORDER BY day");return $stmt->fetchAll()?:[];}catch(Throwable){return [];} }


    public function createIntegrityBaseline(): int
    {
        $files=$this->criticalFiles();
        $pdo=Database::connection();
        $stmt=$pdo->prepare("INSERT INTO system_health_integrity_baseline(relative_path,sha256,size_bytes,baseline_at,last_verified_at) VALUES(:path,:sha,:size,NOW(),NOW()) ON DUPLICATE KEY UPDATE sha256=VALUES(sha256),size_bytes=VALUES(size_bytes),baseline_at=NOW(),last_verified_at=NOW()");
        $count=0;
        foreach($files as $relative=>$absolute){
            $stmt->execute(['path'=>$relative,'sha'=>hash_file('sha256',$absolute),'size'=>(int)(filesize($absolute)?:0)]);
            $count++;
        }
        return $count;
    }

    private function integritySummary(): array
    {
        try {
            $pdo=Database::connection();
            $rows=$pdo->query("SELECT relative_path,sha256,size_bytes FROM system_health_integrity_baseline ORDER BY relative_path")->fetchAll()?:[];
            $current=$this->criticalFiles();
            $baseline=[]; foreach($rows as $row)$baseline[(string)$row['relative_path']]=$row;
            $changed=[]; $missing=[]; $added=[];
            foreach($baseline as $relative=>$row){
                if(!isset($current[$relative])){$missing[]=$relative;continue;}
                if(!hash_equals((string)$row['sha256'],(string)hash_file('sha256',$current[$relative])))$changed[]=$relative;
            }
            foreach($current as $relative=>$absolute)if(!isset($baseline[$relative]))$added[]=$relative;
            if($baseline){$pdo->exec("UPDATE system_health_integrity_baseline SET last_verified_at=NOW()");}
            return ['baseline_count'=>count($baseline),'current_count'=>count($current),'changed'=>count($changed),'missing'=>count($missing),'added'=>count($added),'changed_files'=>array_slice($changed,0,15),'missing_files'=>array_slice($missing,0,15),'added_files'=>array_slice($added,0,15)];
        } catch(Throwable) { return ['baseline_count'=>0,'current_count'=>0,'changed'=>0,'missing'=>0,'added'=>0]; }
    }

    private function criticalFiles(): array
    {
        $config=is_file(BASE_PATH.'/config/health_monitor.php')?(require BASE_PATH.'/config/health_monitor.php'):[];
        $paths=$config['integrity_paths']??['app','config','routes','public/index.php'];
        $excluded=$config['integrity_exclusions']??['storage','vendor','.git'];
        $files=[];
        foreach($paths as $relative){
            $absolute=BASE_PATH.'/'.ltrim((string)$relative,'/\\');
            if(is_file($absolute)){$files[str_replace('\\','/',substr($absolute,strlen(BASE_PATH)+1))]=$absolute;continue;}
            if(!is_dir($absolute))continue;
            $it=new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($absolute,\FilesystemIterator::SKIP_DOTS));
            foreach($it as $file){
                if(!$file->isFile())continue;
                $rel=str_replace('\\','/',substr($file->getPathname(),strlen(BASE_PATH)+1));
                $skip=false; foreach($excluded as $part)if(str_contains('/'.$rel.'/','/'.trim((string)$part,'/').'/')){$skip=true;break;}
                if(!$skip && preg_match('/\.(php|json|js|css)$/i',$rel))$files[$rel]=$file->getPathname();
            }
        }
        ksort($files,SORT_STRING); return $files;
    }

    private function persistMetrics(array $results): void
    {
        try {
            $pdo=Database::connection();
            $stmt=$pdo->prepare('INSERT INTO system_health_metrics(metric_key,metric_value,metric_unit,context_json,recorded_at) VALUES(:key,:value,:unit,:context,NOW())');
            $stmt->execute(['key'=>'php.memory.usage','value'=>memory_get_usage(true),'unit'=>'bytes','context'=>null]);
            $stmt->execute(['key'=>'php.memory.peak','value'=>memory_get_peak_usage(true),'unit'=>'bytes','context'=>null]);
            foreach($results as $r)$stmt->execute(['key'=>'component.'.$r->component.'.duration','value'=>$r->durationMs,'unit'=>'ms','context'=>json_encode(['status'=>$r->status->value,'score'=>$r->score],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)]);
            $pdo->exec("DELETE FROM system_health_metrics WHERE recorded_at<DATE_SUB(NOW(),INTERVAL 30 DAY)");
        } catch(Throwable) {}
    }

    private function metrics(): array
    {
        try {
            $stmt = Database::connection()->prepare(
                'SELECT metric_key, ROUND(AVG(metric_value), 2) AS average_value, '
                . 'MAX(metric_value) AS maximum_value, metric_unit, COUNT(*) AS samples '
                . 'FROM system_health_metrics '
                . 'WHERE recorded_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) '
                . 'GROUP BY metric_key, metric_unit '
                . 'ORDER BY metric_key LIMIT 80'
            );
            $stmt->execute();
            return $stmt->fetchAll() ?: [];
        } catch(Throwable) { return []; }
    }

    private function parseBytes(string $value): int
    {
        $value=trim($value); if($value===''||$value==='-1')return 0;
        $unit=strtolower(substr($value,-1)); $number=(float)$value;
        return (int)match($unit){'g'=>$number*1073741824,'m'=>$number*1048576,'k'=>$number*1024,default=>$number};
    }
}
