<?php
declare(strict_types=1);

namespace App\Services\Pipeline;

use App\Core\Database;
use App\Repositories\UpdatePackageRegistryRepository;
use App\Services\Pipeline\Steps\CompatibilityPipelineStep;
use App\Services\Pipeline\Steps\DependencyPipelineStep;
use App\Services\Pipeline\Steps\NativePreflightStep;
use App\Services\Pipeline\Steps\PolicyPipelineStep;
use App\Services\Pipeline\Steps\TrustPipelineStep;
use PDO;
use RuntimeException;
use Throwable;
use ZipArchive;

final class LivePipelineMonitorService
{
    /** @var array<int,PipelineStep> */
    private array $steps;

    public function __construct()
    {
        $this->steps = [
            new NativePreflightStep(),
            new TrustPipelineStep(),
            new CompatibilityPipelineStep(),
            new PolicyPipelineStep(),
            new DependencyPipelineStep(),
        ];

        usort(
            $this->steps,
            static fn(PipelineStep $left, PipelineStep $right): int =>
                $left->order() <=> $right->order()
        );
    }

    /**
     * @return array<string,mixed>
     */
    public function start(int $packageId, ?int $userId): array
    {
        $package = (new UpdatePackageRegistryRepository())->find($packageId);

        if ($package === null) {
            throw new RuntimeException('Package introuvable.');
        }

        $archive = (string) ($package['fichier_original'] ?? '');
        if ($archive === '') {
            throw new RuntimeException('Archive du package absente.');
        }

        $zipPath = BASE_PATH . '/storage/updates/packages/' . basename($archive);
        if (!is_file($zipPath)) {
            throw new RuntimeException('Fichier du package introuvable.');
        }

        $manifest = $this->manifest($zipPath);

        $statement = Database::connection()->prepare(
            "INSERT INTO system_package_pipeline_runs
             (
                package_id,
                package_name,
                package_version,
                archive_name,
                run_type,
                status,
                decision,
                score,
                executed_by,
                created_at
             )
             VALUES
             (
                :package_id,
                :package_name,
                :package_version,
                :archive_name,
                'live_simulation',
                'RUNNING',
                'PENDING',
                0,
                :executed_by,
                NOW()
             )"
        );

        $statement->execute([
            'package_id' => $packageId,
            'package_name' => (string) ($manifest['package'] ?? $package['nom_package'] ?? ''),
            'package_version' => (string) ($manifest['version'] ?? $package['version_package'] ?? ''),
            'archive_name' => basename($zipPath),
            'executed_by' => $userId,
        ]);

        $runId = (int) Database::connection()->lastInsertId();

        return $this->status($runId);
    }

    /**
     * Exécute au maximum une étape.
     *
     * @return array<string,mixed>
     */
    public function advance(int $runId, ?int $userId): array
    {
        if ($runId <= 0) {
            throw new RuntimeException('Exécution invalide.');
        }

        $lockDirectory = BASE_PATH . '/storage/updates/live-pipeline';
        if (!is_dir($lockDirectory) && !@mkdir($lockDirectory, 0770, true) && !is_dir($lockDirectory)) {
            throw new RuntimeException('Dossier de verrouillage indisponible.');
        }

        $lockPath = $lockDirectory . '/run-' . $runId . '.lock';
        $handle = @fopen($lockPath, 'c+');

        if (!is_resource($handle)) {
            throw new RuntimeException('Verrou du pipeline indisponible.');
        }

        try {
            if (!flock($handle, LOCK_EX | LOCK_NB)) {
                return $this->status($runId);
            }

            $run = $this->run($runId);

            if (!in_array((string) $run['status'], ['RUNNING'], true)) {
                return $this->status($runId);
            }

            if ($userId !== null && !empty($run['executed_by'])
                && (int) $run['executed_by'] !== $userId) {
                throw new RuntimeException('Cette exécution appartient à une autre session administrateur.');
            }

            $completedCodes = $this->completedCodes($runId);
            $nextStep = null;

            foreach ($this->steps as $step) {
                if (!in_array($step->code(), $completedCodes, true)) {
                    $nextStep = $step;
                    break;
                }
            }

            if ($nextStep === null) {
                $this->finish($runId);
                return $this->status($runId);
            }

            $zipPath = BASE_PATH . '/storage/updates/packages/' . basename((string) $run['archive_name']);
            if (!is_file($zipPath)) {
                throw new RuntimeException('Archive du package introuvable.');
            }

            $context = [
                'zip_path' => $zipPath,
                'package_id' => isset($run['package_id']) ? (int) $run['package_id'] : null,
                'user_id' => $userId,
                'run_type' => 'live_simulation',
                'manifest' => $this->manifest($zipPath),
                'installed_version' => $this->installedVersion(),
            ];

            $started = microtime(true);

            try {
                $result = $nextStep->execute($context);
            } catch (Throwable $error) {
                $result = [
                    'code' => $nextStep->code(),
                    'label' => $nextStep->label(),
                    'order' => $nextStep->order(),
                    'status' => 'ERROR',
                    'decision' => 'ERROR',
                    'score' => 0,
                    'critical' => true,
                    'message' => 'Étape interrompue.',
                    'issues' => [$error->getMessage()],
                    'warnings' => [],
                    'details' => [],
                ];
            }

            $result['duration_ms'] = (int) round((microtime(true) - $started) * 1000);
            $this->persistStep($runId, $result);

            if (($result['critical'] ?? false)
                && in_array(
                    (string) ($result['decision'] ?? ''),
                    ['BLOCKED', 'DENIED', 'REJECTED', 'INCOMPATIBLE', 'ERROR'],
                    true
                )) {
                $this->finish($runId);
                return $this->status($runId);
            }

            if (count($completedCodes) + 1 >= count($this->steps)) {
                $this->finish($runId);
            }

            return $this->status($runId);
        } finally {
            @flock($handle, LOCK_UN);
            @fclose($handle);
        }
    }

    /**
     * @return array<string,mixed>
     */
    public function status(int $runId): array
    {
        $run = $this->run($runId);

        $statement = Database::connection()->prepare(
            "SELECT
                id,
                run_id,
                step_code,
                step_label,
                step_order,
                status,
                decision,
                score,
                critical,
                message,
                issues_json,
                warnings_json,
                duration_ms,
                created_at
             FROM system_package_pipeline_steps
             WHERE run_id = :run_id
             ORDER BY step_order ASC, id ASC"
        );
        $statement->execute(['run_id' => $runId]);

        $steps = [];
        foreach ($statement->fetchAll(PDO::FETCH_ASSOC) ?: [] as $step) {
            $issues = json_decode((string) ($step['issues_json'] ?? ''), true);
            $warnings = json_decode((string) ($step['warnings_json'] ?? ''), true);

            $step['issues'] = is_array($issues) ? array_values($issues) : [];
            $step['warnings'] = is_array($warnings) ? array_values($warnings) : [];

            unset($step['issues_json'], $step['warnings_json']);
            $steps[] = $step;
        }
        $statement->closeCursor();

        $completed = count($steps);
        $total = count($this->steps);
        $progress = $total > 0
            ? (int) round(($completed / $total) * 100)
            : 0;

        if ((string) $run['status'] !== 'RUNNING') {
            $progress = 100;
        }

        $next = null;
        $completedCodes = array_map(
            static fn(array $step): string => (string) $step['step_code'],
            $steps
        );

        foreach ($this->steps as $step) {
            if (!in_array($step->code(), $completedCodes, true)) {
                $next = [
                    'code' => $step->code(),
                    'label' => $step->label(),
                    'order' => $step->order(),
                ];
                break;
            }
        }

        return [
            'ok' => true,
            'run' => [
                'id' => (int) $run['id'],
                'package_id' => isset($run['package_id']) ? (int) $run['package_id'] : null,
                'package_name' => (string) $run['package_name'],
                'package_version' => (string) $run['package_version'],
                'status' => (string) $run['status'],
                'decision' => (string) $run['decision'],
                'score' => (int) $run['score'],
                'blocking_step' => $run['blocking_step'],
                'duration_ms' => (int) $run['duration_ms'],
                'created_at' => (string) $run['created_at'],
                'finished_at' => $run['finished_at'],
            ],
            'steps' => $steps,
            'completed_steps' => $completed,
            'total_steps' => $total,
            'progress' => $progress,
            'next_step' => $next,
            'finished' => (string) $run['status'] !== 'RUNNING',
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function run(int $runId): array
    {
        $statement = Database::connection()->prepare(
            "SELECT
                id,
                package_id,
                package_name,
                package_version,
                archive_name,
                run_type,
                status,
                decision,
                score,
                blocking_step,
                duration_ms,
                executed_by,
                created_at,
                finished_at
             FROM system_package_pipeline_runs
             WHERE id = :id
             LIMIT 1"
        );
        $statement->execute(['id' => $runId]);
        $run = $statement->fetch(PDO::FETCH_ASSOC);
        $statement->closeCursor();

        if (!is_array($run)) {
            throw new RuntimeException('Exécution du pipeline introuvable.');
        }

        return $run;
    }

    /**
     * @return array<int,string>
     */
    private function completedCodes(int $runId): array
    {
        $statement = Database::connection()->prepare(
            "SELECT step_code
             FROM system_package_pipeline_steps
             WHERE run_id = :run_id
             ORDER BY step_order ASC, id ASC"
        );
        $statement->execute(['run_id' => $runId]);

        $codes = array_map(
            'strval',
            $statement->fetchAll(PDO::FETCH_COLUMN) ?: []
        );
        $statement->closeCursor();

        return $codes;
    }

    /**
     * @param array<string,mixed> $result
     */
    private function persistStep(int $runId, array $result): void
    {
        $statement = Database::connection()->prepare(
            "INSERT INTO system_package_pipeline_steps
             (
                run_id,
                step_code,
                step_label,
                step_order,
                status,
                decision,
                score,
                critical,
                message,
                issues_json,
                warnings_json,
                details_json,
                duration_ms,
                created_at
             )
             VALUES
             (
                :run_id,
                :step_code,
                :step_label,
                :step_order,
                :status,
                :decision,
                :score,
                :critical,
                :message,
                :issues_json,
                :warnings_json,
                :details_json,
                :duration_ms,
                NOW()
             )"
        );

        $statement->execute([
            'run_id' => $runId,
            'step_code' => (string) $result['code'],
            'step_label' => (string) $result['label'],
            'step_order' => (int) $result['order'],
            'status' => (string) $result['status'],
            'decision' => (string) $result['decision'],
            'score' => (int) $result['score'],
            'critical' => !empty($result['critical']) ? 1 : 0,
            'message' => (string) ($result['message'] ?? ''),
            'issues_json' => json_encode(
                $result['issues'] ?? [],
                JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
            ),
            'warnings_json' => json_encode(
                $result['warnings'] ?? [],
                JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
            ),
            'details_json' => json_encode(
                $result['details'] ?? [],
                JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
            ),
            'duration_ms' => (int) $result['duration_ms'],
        ]);
    }

    private function finish(int $runId): void
    {
        $statement = Database::connection()->prepare(
            "SELECT
                step_code,
                decision,
                score,
                critical,
                issues_json,
                warnings_json,
                duration_ms
             FROM system_package_pipeline_steps
             WHERE run_id = :run_id
             ORDER BY step_order ASC, id ASC"
        );
        $statement->execute(['run_id' => $runId]);
        $steps = $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
        $statement->closeCursor();

        $blockingStep = null;
        $issues = [];
        $warnings = [];
        $scores = [];
        $duration = 0;

        foreach ($steps as $step) {
            $scores[] = (int) $step['score'];
            $duration += (int) $step['duration_ms'];

            $stepIssues = json_decode((string) ($step['issues_json'] ?? ''), true);
            $stepWarnings = json_decode((string) ($step['warnings_json'] ?? ''), true);

            if (is_array($stepIssues)) {
                $issues = array_merge($issues, $stepIssues);
            }

            if (is_array($stepWarnings)) {
                $warnings = array_merge($warnings, $stepWarnings);
            }

            if ($blockingStep === null
                && (int) $step['critical'] === 1
                && in_array(
                    (string) $step['decision'],
                    ['BLOCKED', 'DENIED', 'REJECTED', 'INCOMPATIBLE', 'ERROR'],
                    true
                )) {
                $blockingStep = (string) $step['step_code'];
            }
        }

        $score = $scores !== []
            ? (int) round(array_sum($scores) / count($scores))
            : 0;

        $decision = $blockingStep !== null
            ? 'DENIED'
            : ($warnings !== [] ? 'WARNING' : 'ALLOWED');

        $summary = [
            'ok' => $blockingStep === null,
            'decision' => $decision,
            'score' => $score,
            'blocking_step' => $blockingStep,
            'issues' => array_values(array_unique(array_map('strval', $issues))),
            'warnings' => array_values(array_unique(array_map('strval', $warnings))),
            'duration_ms' => $duration,
            'run_type' => 'live_simulation',
            'writes_performed' => false,
            'executed_at' => gmdate('c'),
        ];

        $update = Database::connection()->prepare(
            "UPDATE system_package_pipeline_runs
             SET
                status = :status,
                decision = :decision,
                score = :score,
                blocking_step = :blocking_step,
                summary_json = :summary_json,
                duration_ms = :duration_ms,
                finished_at = NOW()
             WHERE id = :id
               AND status = 'RUNNING'"
        );

        $update->execute([
            'status' => $blockingStep === null ? 'COMPLETED' : 'BLOCKED',
            'decision' => $decision,
            'score' => $score,
            'blocking_step' => $blockingStep,
            'summary_json' => json_encode(
                $summary,
                JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
            ),
            'duration_ms' => $duration,
            'id' => $runId,
        ]);
    }

    /**
     * @return array<string,mixed>
     */
    private function manifest(string $zipPath): array
    {
        $zip = new ZipArchive();

        if ($zip->open($zipPath) !== true) {
            throw new RuntimeException('Archive ZIP illisible.');
        }

        try {
            $raw = $zip->getFromName('manifest.json');
            $manifest = is_string($raw) ? json_decode($raw, true) : null;

            if (!is_array($manifest)) {
                throw new RuntimeException('manifest.json absent ou invalide.');
            }

            return $manifest;
        } finally {
            $zip->close();
        }
    }

    private function installedVersion(): string
    {
        return is_file(BASE_PATH . '/VERSION')
            ? trim((string) file_get_contents(BASE_PATH . '/VERSION'))
            : '0.0.0';
    }
}
