<?php
declare(strict_types=1);

namespace App\Services\PackageManager;

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

final class DependencyResolverService
{
    public function dashboard(?int $selectedId = null): array
    {
        $pdo = Database::connection();
        $this->synchronizeDeclarations($pdo);
        $packages = $this->packages($pdo);
        $selected = null;
        if ($selectedId !== null && $selectedId > 0) {
            foreach ($packages as $row) {
                if ((int)$row['id'] === $selectedId) { $selected = $row; break; }
            }
        }
        if ($selected !== null) {
            $selected['dependencies'] = $this->dependencies($pdo, (int)$selected['id']);
            $selected['dependents'] = $this->dependents($pdo, (string)$selected['package_key']);
            $selected['latest_analysis'] = $this->latestAnalysis($pdo, (int)$selected['id']);
            $selected['plan'] = $this->buildPlan($pdo, (int)$selected['id']);
        }
        return [
            'packages' => $packages,
            'selected' => $selected,
            'metrics' => $this->metrics($pdo, $packages),
            'graph' => $this->graph($pdo),
            'generated_at' => date('Y-m-d H:i:s'),
        ];
    }

    public function analyze(int $registryId, ?int $userId = null): array
    {
        if ($registryId <= 0) throw new RuntimeException('Package invalide.');
        $pdo = Database::connection();
        $package = $this->package($pdo, $registryId);
        if ($package === null) throw new RuntimeException('Package introuvable dans le registre.');
        $this->synchronizeDeclarations($pdo);
        $dependencies = $this->dependencies($pdo, $registryId);
        $issues = [];
        $warnings = [];
        $resolved = 0;
        foreach ($dependencies as $dependency) {
            $type = strtoupper((string)$dependency['dependency_type']);
            $target = $this->packageByKey($pdo, (string)$dependency['dependency_key']);
            if ($type === 'CONFLICT') {
                if ($target !== null && in_array((string)$target['package_status'], ['INSTALLED','SYSTEM','UPDATE_AVAILABLE'], true)) {
                    $issues[] = 'Conflit actif avec '.$dependency['dependency_key'].'.';
                }
                continue;
            }
            if ($target === null || !in_array((string)$target['package_status'], ['INSTALLED','SYSTEM','UPDATE_AVAILABLE'], true)) {
                $message = 'Dépendance absente : '.$dependency['dependency_key'].'.';
                if ($type === 'REQUIRED') $issues[] = $message; else $warnings[] = $message;
                continue;
            }
            $version = (string)$target['version'];
            $min = (string)($dependency['min_version'] ?? '');
            $max = (string)($dependency['max_version'] ?? '');
            if ($min !== '' && version_compare($version, $min, '<')) {
                $issues[] = $dependency['dependency_key'].' requiert au minimum '.$min.' (installé : '.$version.').';
                continue;
            }
            if ($max !== '' && version_compare($version, $max, '>')) {
                $issues[] = $dependency['dependency_key'].' dépasse la version maximale '.$max.' (installé : '.$version.').';
                continue;
            }
            $resolved++;
        }
        $cycles = $this->cycles($pdo);
        foreach ($cycles as $cycle) {
            if (in_array((string)$package['package_key'], $cycle, true)) {
                $issues[] = 'Dépendance circulaire : '.implode(' → ', $cycle).'.';
            }
        }
        $impact = count($this->dependents($pdo, (string)$package['package_key']));
        if ($impact > 0) $warnings[] = $impact.' package(s) dépend(ent) de ce composant.';
        $penalty = count($issues) * 30 + count($warnings) * 5;
        $score = max(0, 100 - min(100, $penalty));
        $decision = $issues ? 'INCOMPATIBLE' : ($warnings ? 'WARNING' : 'RESOLVED');
        $details = ['issues'=>$issues,'warnings'=>$warnings,'resolved'=>$resolved,'total'=>count($dependencies),'cycles'=>$cycles,'impact'=>$impact,'plan'=>$this->buildPlan($pdo,$registryId)];
        $stmt = $pdo->prepare(
            'INSERT INTO system_package_dependency_analyses(registry_id,score,decision,required_count,resolved_count,issue_count,warning_count,details_json,analyzed_by,created_at)
             VALUES(:registry_id,:score,:decision,:required_count,:resolved_count,:issue_count,:warning_count,:details_json,:analyzed_by,NOW())'
        );
        $stmt->execute([
            'registry_id'=>$registryId,'score'=>$score,'decision'=>$decision,'required_count'=>count($dependencies),
            'resolved_count'=>$resolved,'issue_count'=>count($issues),'warning_count'=>count($warnings),
            'details_json'=>json_encode($details, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), 'analyzed_by'=>$userId,
        ]);
        return ['score'=>$score,'decision'=>$decision,'details'=>$details];
    }

    private function synchronizeDeclarations(PDO $pdo): void
    {
        $rows = $pdo->prepare('SELECT r.id,r.package_key,p.result_json FROM system_package_registry r LEFT JOIN cda_update_packages p ON p.id=r.update_package_id ORDER BY r.id');
        $rows->execute();
        $insert = $pdo->prepare(
            'INSERT INTO system_package_dependencies(registry_id,dependency_key,dependency_type,min_version,max_version,source,created_at,updated_at)
             VALUES(:registry_id,:dependency_key,:dependency_type,:min_version,:max_version,:source,NOW(),NOW())
             ON DUPLICATE KEY UPDATE dependency_type=VALUES(dependency_type),min_version=VALUES(min_version),max_version=VALUES(max_version),source=VALUES(source),updated_at=NOW()'
        );
        foreach ($rows->fetchAll(PDO::FETCH_ASSOC) ?: [] as $row) {
            $manifest = $this->manifestFromResult((string)($row['result_json'] ?? ''));
            foreach ((array)($manifest['dependencies'] ?? []) as $dependency) {
                if (!is_array($dependency)) continue;
                $key = trim((string)($dependency['package_id'] ?? $dependency['package'] ?? $dependency['id'] ?? ''));
                if ($key === '') continue;
                $insert->execute(['registry_id'=>(int)$row['id'],'dependency_key'=>$key,'dependency_type'=>'REQUIRED','min_version'=>$this->nullable((string)($dependency['min_version'] ?? '')),'max_version'=>$this->nullable((string)($dependency['max_version'] ?? '')),'source'=>'MANIFEST']);
            }
            foreach ((array)($manifest['optional_dependencies'] ?? []) as $dependency) {
                if (!is_array($dependency)) continue;
                $key = trim((string)($dependency['package_id'] ?? $dependency['package'] ?? $dependency['id'] ?? ''));
                if ($key === '') continue;
                $insert->execute(['registry_id'=>(int)$row['id'],'dependency_key'=>$key,'dependency_type'=>'OPTIONAL','min_version'=>$this->nullable((string)($dependency['min_version'] ?? '')),'max_version'=>$this->nullable((string)($dependency['max_version'] ?? '')),'source'=>'MANIFEST']);
            }
            foreach ((array)($manifest['conflicts'] ?? []) as $conflict) {
                $key = is_array($conflict) ? trim((string)($conflict['package_id'] ?? $conflict['package'] ?? '')) : trim((string)$conflict);
                if ($key === '') continue;
                $insert->execute(['registry_id'=>(int)$row['id'],'dependency_key'=>$key,'dependency_type'=>'CONFLICT','min_version'=>null,'max_version'=>null,'source'=>'MANIFEST']);
            }
        }
    }

    private function manifestFromResult(string $json): array
    {
        if ($json === '') return [];
        $data = json_decode($json, true);
        if (!is_array($data)) return [];
        foreach (['manifest','package_manifest','descriptor'] as $key) if (isset($data[$key]) && is_array($data[$key])) return $data[$key];
        return isset($data['dependencies']) || isset($data['package_id']) ? $data : [];
    }

    private function packages(PDO $pdo): array
    {
        $stmt = $pdo->prepare(
            'SELECT r.*,
                (SELECT COUNT(*) FROM system_package_dependencies d WHERE d.registry_id=r.id AND d.dependency_type="REQUIRED") required_dependencies,
                (SELECT COUNT(*) FROM system_package_dependencies d WHERE d.registry_id=r.id AND d.dependency_type="CONFLICT") conflicts,
                (SELECT a.score FROM system_package_dependency_analyses a WHERE a.registry_id=r.id ORDER BY a.id DESC LIMIT 1) dependency_score,
                (SELECT a.decision FROM system_package_dependency_analyses a WHERE a.registry_id=r.id ORDER BY a.id DESC LIMIT 1) dependency_decision
             FROM system_package_registry r ORDER BY COALESCE(r.installed_at,r.updated_at) DESC,r.id DESC'
        );
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    private function package(PDO $pdo, int $id): ?array
    {
        $stmt=$pdo->prepare('SELECT * FROM system_package_registry WHERE id=:id LIMIT 1');$stmt->execute(['id'=>$id]);$row=$stmt->fetch(PDO::FETCH_ASSOC);return is_array($row)?$row:null;
    }
    private function packageByKey(PDO $pdo, string $key): ?array
    {
        $stmt=$pdo->prepare('SELECT * FROM system_package_registry WHERE package_key=:key OR package_name=:key ORDER BY id DESC LIMIT 1');$stmt->execute(['key'=>$key]);$row=$stmt->fetch(PDO::FETCH_ASSOC);return is_array($row)?$row:null;
    }
    private function dependencies(PDO $pdo, int $id): array
    {
        $stmt=$pdo->prepare('SELECT * FROM system_package_dependencies WHERE registry_id=:id ORDER BY FIELD(dependency_type,"REQUIRED","OPTIONAL","CONFLICT"),dependency_key');$stmt->execute(['id'=>$id]);return $stmt->fetchAll(PDO::FETCH_ASSOC)?:[];
    }
    private function dependents(PDO $pdo, string $key): array
    {
        $stmt=$pdo->prepare('SELECT r.id,r.package_key,r.package_name,r.version,r.package_status,d.dependency_type FROM system_package_dependencies d JOIN system_package_registry r ON r.id=d.registry_id WHERE d.dependency_key=:key ORDER BY r.package_name');$stmt->execute(['key'=>$key]);return $stmt->fetchAll(PDO::FETCH_ASSOC)?:[];
    }
    private function latestAnalysis(PDO $pdo, int $id): ?array
    {
        $stmt=$pdo->prepare('SELECT * FROM system_package_dependency_analyses WHERE registry_id=:id ORDER BY id DESC LIMIT 1');$stmt->execute(['id'=>$id]);$row=$stmt->fetch(PDO::FETCH_ASSOC);if(!is_array($row))return null;$row['details']=json_decode((string)$row['details_json'],true)?:[];return $row;
    }
    private function graph(PDO $pdo): array
    {
        $stmt=$pdo->prepare('SELECT r.package_key,r.package_name,d.dependency_key,d.dependency_type FROM system_package_dependencies d JOIN system_package_registry r ON r.id=d.registry_id ORDER BY r.package_name,d.dependency_key');$stmt->execute();return $stmt->fetchAll(PDO::FETCH_ASSOC)?:[];
    }
    private function metrics(PDO $pdo, array $packages): array
    {
        $stmt=$pdo->prepare('SELECT COUNT(*) total,SUM(decision="INCOMPATIBLE") incompatible,SUM(decision="WARNING") warnings,SUM(decision="RESOLVED") resolved FROM (SELECT a.* FROM system_package_dependency_analyses a JOIN (SELECT registry_id,MAX(id) id FROM system_package_dependency_analyses GROUP BY registry_id) x ON x.id=a.id) q');$stmt->execute();$m=$stmt->fetch(PDO::FETCH_ASSOC)?:[];
        $cycles=count($this->cycles($pdo));
        return ['packages'=>count($packages),'relations'=>count($this->graph($pdo)),'resolved'=>(int)($m['resolved']??0),'warnings'=>(int)($m['warnings']??0),'incompatible'=>(int)($m['incompatible']??0),'cycles'=>$cycles];
    }
    private function buildPlan(PDO $pdo, int $id): array
    {
        $root=$this->package($pdo,$id);if($root===null)return [];$visited=[];$visiting=[];$plan=[];$issues=[];
        $walk=function(string $key) use (&$walk,&$visited,&$visiting,&$plan,&$issues,$pdo):void{
            if(isset($visited[$key]))return;if(isset($visiting[$key])){$issues[]='Cycle détecté autour de '.$key;return;}$visiting[$key]=true;$pkg=$this->packageByKey($pdo,$key);if($pkg!==null){foreach($this->dependencies($pdo,(int)$pkg['id']) as $d){if($d['dependency_type']==='REQUIRED')$walk((string)$d['dependency_key']);}$plan[]=['package_key'=>$pkg['package_key'],'package_name'=>$pkg['package_name'],'version'=>$pkg['version'],'status'=>$pkg['package_status']];}else{$issues[]='Package manquant : '.$key;}unset($visiting[$key]);$visited[$key]=true;};
        $walk((string)$root['package_key']);return ['steps'=>$plan,'issues'=>$issues];
    }
    private function cycles(PDO $pdo): array
    {
        $edges=[];foreach($this->graph($pdo) as $row){if($row['dependency_type']==='REQUIRED')$edges[(string)$row['package_key']][]=(string)$row['dependency_key'];}
        $cycles=[];$visited=[];$stack=[];$path=[];
        $visit=function(string $node) use (&$visit,&$cycles,&$visited,&$stack,&$path,$edges):void{if(isset($stack[$node])){$pos=array_search($node,$path,true);$cycle=$pos===false?[$node]:array_slice($path,$pos);$cycle[]=$node;$cycles[implode('|',$cycle)]=$cycle;return;}if(isset($visited[$node]))return;$visited[$node]=true;$stack[$node]=true;$path[]=$node;foreach($edges[$node]??[] as $next)$visit($next);array_pop($path);unset($stack[$node]);};
        foreach(array_keys($edges) as $node)$visit($node);return array_values($cycles);
    }
    private function nullable(string $value): ?string { $value=trim($value);return $value===''?null:$value; }
}
