<?php
declare(strict_types=1);

namespace App\Services\PackageManager;

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

final class DependencyResolverService
{
    /**
     * @return array<string,mixed>
     */
    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 $package) {
                if ((int) $package['id'] === $selectedId) {
                    $selected = $package;
                    break;
                }
            }
        }

        if ($selected !== null) {
            $registryId = (int) $selected['id'];
            $packageKey = (string) $selected['package_key'];

            $selected['dependencies'] = $this->dependencies($pdo, $registryId);
            $selected['dependents'] = $this->dependents($pdo, $packageKey);
            $selected['latest_analysis'] = $this->latestAnalysis($pdo, $registryId);
            $selected['plan'] = $this->buildPlan($pdo, $registryId);
            $selected['impact_chain'] = $this->impactChain($pdo, $packageKey);
            $selected['cycles'] = array_values(array_filter(
                $this->cycles($pdo),
                static fn(array $cycle): bool =>
                    in_array($packageKey, $cycle, true)
            ));
        }

        $graph = $this->graphModel($pdo);

        return [
            'packages' => $packages,
            'selected' => $selected,
            'metrics' => $this->metrics($pdo, $packages, $graph),
            'graph' => $graph,
            'cycles' => $this->cycles($pdo),
            'generated_at' => date('Y-m-d H:i:s'),
        ];
    }

    /**
     * @return array<string,mixed>
     */
    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']);
            $key = (string) $dependency['dependency_key'];
            $target = $this->packageByKey($pdo, $key);

            if ($type === 'CONFLICT') {
                if ($target !== null
                    && in_array(
                        (string) $target['package_status'],
                        ['INSTALLED', 'SYSTEM', 'UPDATE_AVAILABLE'],
                        true
                    )) {
                    $issues[] = 'Conflit actif avec ' . $key . '.';
                }
                continue;
            }

            if ($target === null
                || !in_array(
                    (string) $target['package_status'],
                    ['INSTALLED', 'SYSTEM', 'UPDATE_AVAILABLE'],
                    true
                )) {
                $message = 'Dépendance absente : ' . $key . '.';

                if ($type === 'REQUIRED') {
                    $issues[] = $message;
                } else {
                    $warnings[] = $message;
                }

                continue;
            }

            $version = (string) $target['version'];
            $minimum = trim((string) ($dependency['min_version'] ?? ''));
            $maximum = trim((string) ($dependency['max_version'] ?? ''));

            if ($minimum !== '' && version_compare($version, $minimum, '<')) {
                $issues[] = $key
                    . ' requiert au minimum '
                    . $minimum
                    . ' (installé : '
                    . $version
                    . ').';
                continue;
            }

            if ($maximum !== '' && version_compare($version, $maximum, '>')) {
                $issues[] = $key
                    . ' dépasse la version maximale '
                    . $maximum
                    . ' (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->impactChain(
                $pdo,
                (string) $package['package_key']
            )
        );

        if ($impact > 0) {
            $warnings[] = $impact
                . ' package(s) seraient potentiellement impactés.';
        }

        $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,
            'impact_chain' => $this->impactChain(
                $pdo,
                (string) $package['package_key']
            ),
            'plan' => $this->buildPlan($pdo, $registryId),
        ];

        $statement = $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()
             )"
        );

        $statement->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->query(
            "SELECT
                r.id,
                r.package_key,
                p.result_json
             FROM system_package_registry AS r
             LEFT JOIN cda_update_packages AS p
                ON p.id = r.update_package_id
             ORDER BY r.id"
        );

        $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',
                ]);
            }
        }

        $rows->closeCursor();
    }

    /**
     * @return array<string,mixed>
     */
    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
            : [];
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function packages(PDO $pdo): array
    {
        $statement = $pdo->query(
            "SELECT
                r.*,
                (
                    SELECT COUNT(*)
                    FROM system_package_dependencies AS d
                    WHERE d.registry_id = r.id
                      AND d.dependency_type = 'REQUIRED'
                ) AS required_dependencies,
                (
                    SELECT COUNT(*)
                    FROM system_package_dependencies AS d
                    WHERE d.registry_id = r.id
                      AND d.dependency_type = 'OPTIONAL'
                ) AS optional_dependencies,
                (
                    SELECT COUNT(*)
                    FROM system_package_dependencies AS d
                    WHERE d.registry_id = r.id
                      AND d.dependency_type = 'CONFLICT'
                ) AS conflicts,
                (
                    SELECT a.score
                    FROM system_package_dependency_analyses AS a
                    WHERE a.registry_id = r.id
                    ORDER BY a.id DESC
                    LIMIT 1
                ) AS dependency_score,
                (
                    SELECT a.decision
                    FROM system_package_dependency_analyses AS a
                    WHERE a.registry_id = r.id
                    ORDER BY a.id DESC
                    LIMIT 1
                ) AS dependency_decision
             FROM system_package_registry AS r
             ORDER BY COALESCE(r.installed_at, r.updated_at) DESC, r.id DESC"
        );

        $rows = $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
        $statement->closeCursor();

        return $rows;
    }

    /**
     * @return array<string,mixed>|null
     */
    private function package(PDO $pdo, int $id): ?array
    {
        $statement = $pdo->prepare(
            "SELECT *
             FROM system_package_registry
             WHERE id = :id
             LIMIT 1"
        );
        $statement->execute(['id' => $id]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        $statement->closeCursor();

        return is_array($row) ? $row : null;
    }

    /**
     * @return array<string,mixed>|null
     */
    private function packageByKey(PDO $pdo, string $key): ?array
    {
        $statement = $pdo->prepare(
            "SELECT *
             FROM system_package_registry
             WHERE package_key = :package_key
                OR package_name = :package_name
             ORDER BY id DESC
             LIMIT 1"
        );
        $statement->execute([
            'package_key' => $key,
            'package_name' => $key,
        ]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        $statement->closeCursor();

        return is_array($row) ? $row : null;
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function dependencies(PDO $pdo, int $id): array
    {
        $statement = $pdo->prepare(
            "SELECT *
             FROM system_package_dependencies
             WHERE registry_id = :id
             ORDER BY
                CASE dependency_type
                    WHEN 'REQUIRED' THEN 1
                    WHEN 'OPTIONAL' THEN 2
                    WHEN 'CONFLICT' THEN 3
                    ELSE 4
                END,
                dependency_key"
        );
        $statement->execute(['id' => $id]);
        $rows = $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
        $statement->closeCursor();

        return $rows;
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function dependents(PDO $pdo, string $key): array
    {
        $statement = $pdo->prepare(
            "SELECT
                r.id,
                r.package_key,
                r.package_name,
                r.version,
                r.package_status,
                d.dependency_type
             FROM system_package_dependencies AS d
             INNER JOIN system_package_registry AS r
                ON r.id = d.registry_id
             WHERE d.dependency_key = :dependency_key
             ORDER BY r.package_name"
        );
        $statement->execute(['dependency_key' => $key]);
        $rows = $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
        $statement->closeCursor();

        return $rows;
    }

    /**
     * @return array<string,mixed>|null
     */
    private function latestAnalysis(PDO $pdo, int $id): ?array
    {
        $statement = $pdo->prepare(
            "SELECT *
             FROM system_package_dependency_analyses
             WHERE registry_id = :id
             ORDER BY id DESC
             LIMIT 1"
        );
        $statement->execute(['id' => $id]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        $statement->closeCursor();

        if (!is_array($row)) {
            return null;
        }

        $details = json_decode((string) $row['details_json'], true);
        $row['details'] = is_array($details) ? $details : [];

        return $row;
    }

    /**
     * @return array<string,mixed>
     */
    private function graphModel(PDO $pdo): array
    {
        $nodeStatement = $pdo->query(
            "SELECT
                id,
                package_key,
                package_name,
                version,
                package_status,
                category
             FROM system_package_registry
             ORDER BY package_name, id"
        );

        $nodes = [];
        foreach ($nodeStatement->fetchAll(PDO::FETCH_ASSOC) ?: [] as $node) {
            $nodes[] = [
                'id' => (int) $node['id'],
                'key' => (string) $node['package_key'],
                'label' => (string) $node['package_name'],
                'version' => (string) $node['version'],
                'status' => (string) $node['package_status'],
                'category' => (string) ($node['category'] ?? ''),
            ];
        }
        $nodeStatement->closeCursor();

        $edgeStatement = $pdo->query(
            "SELECT
                r.id AS source_id,
                r.package_key AS source_key,
                r.package_name AS source_label,
                d.dependency_key AS target_key,
                d.dependency_type AS relation_type,
                d.min_version,
                d.max_version
             FROM system_package_dependencies AS d
             INNER JOIN system_package_registry AS r
                ON r.id = d.registry_id
             ORDER BY r.package_name, d.dependency_key"
        );

        $edges = [];
        foreach ($edgeStatement->fetchAll(PDO::FETCH_ASSOC) ?: [] as $edge) {
            $edges[] = [
                'source_id' => (int) $edge['source_id'],
                'source_key' => (string) $edge['source_key'],
                'source_label' => (string) $edge['source_label'],
                'target_key' => (string) $edge['target_key'],
                'type' => (string) $edge['relation_type'],
                'min_version' => $edge['min_version'],
                'max_version' => $edge['max_version'],
            ];
        }
        $edgeStatement->closeCursor();

        return [
            'nodes' => $nodes,
            'edges' => $edges,
        ];
    }

    /**
     * @param array<int,array<string,mixed>> $packages
     * @param array<string,mixed> $graph
     * @return array<string,int>
     */
    private function metrics(
        PDO $pdo,
        array $packages,
        array $graph
    ): array {
        $statement = $pdo->query(
            "SELECT
                COUNT(*) AS total,
                SUM(decision = 'INCOMPATIBLE') AS incompatible,
                SUM(decision = 'WARNING') AS warnings,
                SUM(decision = 'RESOLVED') AS resolved
             FROM (
                SELECT a.*
                FROM system_package_dependency_analyses AS a
                INNER JOIN (
                    SELECT registry_id, MAX(id) AS id
                    FROM system_package_dependency_analyses
                    GROUP BY registry_id
                ) AS latest
                    ON latest.id = a.id
             ) AS current_analyses"
        );

        $metrics = $statement->fetch(PDO::FETCH_ASSOC) ?: [];
        $statement->closeCursor();

        return [
            'packages' => count($packages),
            'relations' => count((array) ($graph['edges'] ?? [])),
            'resolved' => (int) ($metrics['resolved'] ?? 0),
            'warnings' => (int) ($metrics['warnings'] ?? 0),
            'incompatible' => (int) ($metrics['incompatible'] ?? 0),
            'cycles' => count($this->cycles($pdo)),
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function buildPlan(PDO $pdo, int $id): array
    {
        $root = $this->package($pdo, $id);

        if ($root === null) {
            return [
                'steps' => [],
                'issues' => ['Package racine introuvable.'],
            ];
        }

        $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;
            $package = $this->packageByKey($pdo, $key);

            if ($package === null) {
                $issues[] = 'Package manquant : ' . $key . '.';
                unset($visiting[$key]);
                $visited[$key] = true;
                return;
            }

            foreach ($this->dependencies($pdo, (int) $package['id']) as $dependency) {
                if ((string) $dependency['dependency_type'] === 'REQUIRED') {
                    $walk((string) $dependency['dependency_key']);
                }
            }

            $plan[] = [
                'package_key' => (string) $package['package_key'],
                'package_name' => (string) $package['package_name'],
                'version' => (string) $package['version'],
                'status' => (string) $package['package_status'],
            ];

            unset($visiting[$key]);
            $visited[$key] = true;
        };

        $walk((string) $root['package_key']);

        return [
            'steps' => $plan,
            'issues' => array_values(array_unique($issues)),
        ];
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function impactChain(PDO $pdo, string $rootKey): array
    {
        $queue = [[$rootKey, 0]];
        $visited = [$rootKey => true];
        $impact = [];

        while ($queue !== []) {
            [$key, $depth] = array_shift($queue);

            foreach ($this->dependents($pdo, $key) as $dependent) {
                $dependentKey = (string) $dependent['package_key'];

                if (isset($visited[$dependentKey])) {
                    continue;
                }

                $visited[$dependentKey] = true;
                $impact[] = [
                    'package_key' => $dependentKey,
                    'package_name' => (string) $dependent['package_name'],
                    'version' => (string) $dependent['version'],
                    'status' => (string) $dependent['package_status'],
                    'dependency_type' => (string) $dependent['dependency_type'],
                    'depth' => $depth + 1,
                ];

                $queue[] = [$dependentKey, $depth + 1];
            }
        }

        return $impact;
    }

    /**
     * @return array<int,array<int,string>>
     */
    private function cycles(PDO $pdo): array
    {
        $graph = $this->graphModel($pdo);
        $edges = [];

        foreach ((array) ($graph['edges'] ?? []) as $edge) {
            if ((string) $edge['type'] === 'REQUIRED') {
                $edges[(string) $edge['source_key']][] =
                    (string) $edge['target_key'];
            }
        }

        $cycles = [];
        $visited = [];
        $stack = [];
        $path = [];

        $visit = function (string $node) use (
            &$visit,
            &$cycles,
            &$visited,
            &$stack,
            &$path,
            $edges
        ): void {
            if (isset($stack[$node])) {
                $position = array_search($node, $path, true);
                $cycle = $position === false
                    ? [$node]
                    : array_slice($path, $position);
                $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;
    }
}
