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

final class SecurityBehaviorAnalyticsService
{
    private string $profileFile;

    public function __construct()
    {
        $base = defined('BASE_PATH') ? BASE_PATH : dirname(__DIR__, 2);
        $this->profileFile = $base . '/storage/private/security_ueba_profiles.json';
    }

    /** @return array<string,mixed> */
    public function report(int $hours = 24): array
    {
        $hours = max(1, min(168, $hours));
        $events = $this->events($hours);
        $profiles = $this->readProfiles();
        $groups = [];
        foreach ($events as $row) {
            $context = is_array($row['context'] ?? null) ? $row['context'] : [];
            $actorRaw = (string)($context['user_id'] ?? $context['actor_hash'] ?? $context['ip_hash'] ?? 'anonymous');
            $actor = hash('sha256', 'ueba|' . $actorRaw);
            $event = (string)($context['event'] ?? $row['event'] ?? $row['message'] ?? 'security.event');
            $path = $this->normalizePath((string)($context['path'] ?? '/'));
            $ts = $this->timestamp($row);
            $bucket = date('G', $ts);
            if (!isset($groups[$actor])) $groups[$actor] = ['events'=>0,'types'=>[],'paths'=>[],'hours'=>[],'last_at'=>0];
            $groups[$actor]['events']++;
            $groups[$actor]['types'][$event] = ($groups[$actor]['types'][$event] ?? 0) + 1;
            $groups[$actor]['paths'][$path] = ($groups[$actor]['paths'][$path] ?? 0) + 1;
            $groups[$actor]['hours'][$bucket] = ($groups[$actor]['hours'][$bucket] ?? 0) + 1;
            $groups[$actor]['last_at'] = max((int)$groups[$actor]['last_at'], $ts);
        }

        $findings = [];
        foreach ($groups as $actor => $current) {
            $baseline = is_array($profiles[$actor] ?? null) ? $profiles[$actor] : [];
            $baselineEvents = max(1, (int)($baseline['avg_events'] ?? 1));
            $volumeRatio = $current['events'] / $baselineEvents;
            $newPaths = array_values(array_diff(array_keys($current['paths']), array_keys((array)($baseline['paths'] ?? []))));
            $newTypes = array_values(array_diff(array_keys($current['types']), array_keys((array)($baseline['types'] ?? []))));
            $unusualHours = [];
            $knownHours = array_keys((array)($baseline['hours'] ?? []));
            foreach (array_keys($current['hours']) as $h) if ($knownHours !== [] && !in_array((string)$h, array_map('strval',$knownHours), true)) $unusualHours[]=(int)$h;
            $sensitive = $this->sensitiveSignals((array)$current['types'], (array)$current['paths']);
            $score = 0;
            $reasons = [];
            if ($volumeRatio >= 5) { $score += 35; $reasons[]='volume très supérieur au profil récent'; }
            elseif ($volumeRatio >= 2.5) { $score += 20; $reasons[]='hausse inhabituelle du volume'; }
            if (count($newPaths) >= 5) { $score += 20; $reasons[]='plusieurs chemins jamais observés'; }
            elseif ($newPaths !== []) { $score += min(12, count($newPaths)*3); $reasons[]='chemin inhabituel'; }
            if (count($newTypes) >= 4) { $score += 15; $reasons[]='nouveaux types d’événements'; }
            if ($unusualHours !== []) { $score += 10; $reasons[]='horaire atypique'; }
            if ($sensitive !== []) { $score += min(35, count($sensitive)*12); $reasons[]='séquence sensible détectée'; }
            $score = min(100,$score);
            if ($score < 10) continue;
            $findings[] = [
                'actor'=>substr($actor,0,12).'…', 'score'=>$score, 'level'=>$this->level($score),
                'events'=>(int)$current['events'], 'last_at'=>date('Y-m-d H:i:s',(int)$current['last_at']),
                'new_paths'=>array_slice($newPaths,0,8), 'new_types'=>array_slice($newTypes,0,8),
                'unusual_hours'=>$unusualHours, 'signals'=>$sensitive, 'reasons'=>$reasons,
            ];
        }
        usort($findings, fn(array $a,array $b): int => [$b['score'],$b['last_at']] <=> [$a['score'],$a['last_at']]);
        $this->updateProfiles($profiles,$groups);
        $max = $findings === [] ? 0 : max(array_column($findings,'score'));
        return [
            'hours'=>$hours,'generated_at'=>date('Y-m-d H:i:s'),'events'=>count($events),
            'actors'=>count($groups),'findings'=>array_slice($findings,0,100),'finding_count'=>count($findings),
            'high_count'=>count(array_filter($findings,fn(array $f):bool=>$f['score']>=60)),
            'risk_score'=>$max,'status'=>$max>=80?'Critique':($max>=60?'Élevé':($max>=30?'Sous surveillance':'Stable')),
            'recommendations'=>$this->recommendations($findings),
        ];
    }

    /** @return list<array<string,mixed>> */
    private function events(int $hours): array
    {
        $base = defined('BASE_PATH') ? BASE_PATH : dirname(__DIR__, 2);
        $since=time()-$hours*3600; $rows=[];
        foreach (['security-events.log','security-alerts.log'] as $name) {
            $root=$base.'/storage/logs/security/'.$name;
            foreach ([$root,$root.'.1',$root.'.2',$root.'.3'] as $file) {
                if (!is_file($file)) continue; $h=@fopen($file,'rb'); if ($h===false) continue;
                while (($line=fgets($h))!==false) {
                    $row=json_decode(trim($line),true);
                    if (is_array($row) && $this->timestamp($row)>=$since) $rows[]=$row;
                    if (count($rows)>20000) array_shift($rows);
                }
                fclose($h);
            }
        }
        return $rows;
    }
    /** @param array<string,mixed> $row */
    private function timestamp(array $row): int { $v=(string)($row['timestamp']??$row['context']['timestamp']??''); $t=strtotime($v); return $t===false?time():$t; }
    private function normalizePath(string $path): string
    {
        $path=parse_url($path,PHP_URL_PATH) ?: '/';
        $path=preg_replace('#/[0-9]+(?=/|$)#','/{id}',$path) ?: $path;
        return mb_substr($path,0,180);
    }
    /** @param array<string,int> $types @param array<string,int> $paths @return list<string> */
    private function sensitiveSignals(array $types,array $paths): array
    {
        $hay=strtolower(implode(' ',array_keys($types)).' '.implode(' ',array_keys($paths))); $signals=[];
        $rules=['permission'=>'changement ou refus de permission','role'=>'activité sur les rôles','backup'=>'activité sur les sauvegardes','export'=>'export de données','mfa'=>'événement MFA','session'=>'révocation ou anomalie de session','update'=>'activité sur les mises à jour','sql'=>'signal SQL'];
        foreach($rules as $needle=>$label) if(str_contains($hay,$needle)) $signals[]=$label;
        return $signals;
    }
    private function level(int $score): string { return $score>=80?'critical':($score>=60?'high':($score>=30?'warning':'info')); }
    /** @return array<string,array<string,mixed>> */
    private function readProfiles(): array
    {
        if(!is_file($this->profileFile)) return [];
        $raw=@file_get_contents($this->profileFile); $data=is_string($raw)?json_decode($raw,true):null;
        return is_array($data)?$data:[];
    }
    /** @param array<string,array<string,mixed>> $profiles @param array<string,array<string,mixed>> $groups */
    private function updateProfiles(array $profiles,array $groups): void
    {
        foreach($groups as $actor=>$g){
            $old=is_array($profiles[$actor]??null)?$profiles[$actor]:[];
            $samples=max(0,(int)($old['samples']??0)); $weight=min(20,$samples);
            $profiles[$actor]=[
                'samples'=>min(1000,$samples+1),
                'avg_events'=>(int)round((((int)($old['avg_events']??$g['events']))*$weight+$g['events'])/($weight+1)),
                'paths'=>$this->mergeCounts((array)($old['paths']??[]),(array)$g['paths']),
                'types'=>$this->mergeCounts((array)($old['types']??[]),(array)$g['types']),
                'hours'=>$this->mergeCounts((array)($old['hours']??[]),(array)$g['hours']),
                'updated_at'=>date('c'),
            ];
        }
        if(count($profiles)>2000) $profiles=array_slice($profiles,-2000,null,true);
        $dir=dirname($this->profileFile); if(!is_dir($dir)) @mkdir($dir,0700,true);
        $json=json_encode($profiles,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
        if(!is_string($json)) return; $tmp=$this->profileFile.'.tmp.'.bin2hex(random_bytes(4));
        if(@file_put_contents($tmp,$json,LOCK_EX)!==false){@chmod($tmp,0600);@rename($tmp,$this->profileFile);@chmod($this->profileFile,0600);} else {@unlink($tmp);}
    }
    /** @param array<string,int> $a @param array<string,int> $b @return array<string,int> */
    private function mergeCounts(array $a,array $b): array { foreach($b as $k=>$v)$a[(string)$k]=min(100000,(int)($a[(string)$k]??0)+(int)$v); arsort($a); return array_slice($a,0,200,true); }
    /** @param list<array<string,mixed>> $findings @return list<string> */
    private function recommendations(array $findings): array
    {
        if($findings===[]) return ['Aucune anomalie comportementale significative. Laisser le moteur enrichir progressivement ses profils locaux.'];
        $items=[]; $max=max(array_column($findings,'score'));
        if($max>=80)$items[]='Confinement conseillé : vérifier immédiatement le compte pseudonymisé et ses sessions actives.';
        if(count(array_filter($findings,fn(array $f):bool=>$f['score']>=60))>0)$items[]='Comparer les anomalies avec le SOC et le registre de réponse aux incidents.';
        $items[]='Valider les changements de rôle, exports, sauvegardes et opérations MFA observés pendant la fenêtre.';
        return $items;
    }
}
