<?php
declare(strict_types=1);

namespace App\Services\Deployment;

use App\Core\Database;
use App\Models\BackupJob;
use App\Services\BackupService;
use App\Services\PackageManager\DependencyResolverService;
use App\Services\UpdatePackageService;
use PDO;
use RuntimeException;
use Throwable;

final class EnterpriseDeploymentOrchestratorService
{
    /**
     * @return array<string,mixed>
     */
    public function dashboard(?int $selectedId = null): array
    {
        $orchestrations = $this->orchestrations();

        if (($selectedId === null || $selectedId <= 0)
            && isset($_SESSION['deployment_orchestration_focus'])) {
            $selectedId = (int) $_SESSION['deployment_orchestration_focus'];
            unset($_SESSION['deployment_orchestration_focus']);
        }

        if (($selectedId === null || $selectedId <= 0)
            && isset($orchestrations[0]['id'])) {
            $selectedId = (int) $orchestrations[0]['id'];
        }

        return [
            'registry' => $this->registry(),
            'orchestrations' => $orchestrations,
            'selected' => $selectedId !== null && $selectedId > 0
                ? $this->status($selectedId)
                : null,
            'generated_at' => date('Y-m-d H:i:s'),
            'current_version' => $this->installedVersion(),
        ];
    }

    /**
     * @return array<string,mixed>
     */
    public function create(
        int $rootRegistryId,
        bool $automaticRollback,
        bool $stopOnWarning,
        ?int $userId
    ): array {
        if ($rootRegistryId <= 0) {
            throw new RuntimeException('Package racine invalide.');
        }

        $dependencyService = new DependencyResolverService();
        $dependencyService->analyze($rootRegistryId, $userId);
        $dependencyDashboard = $dependencyService->dashboard($rootRegistryId);
        $selected = $dependencyDashboard['selected'] ?? null;

        if (!is_array($selected)) {
            throw new RuntimeException('Package racine introuvable.');
        }

        $analysis = $selected['latest_analysis'] ?? null;
        if (!is_array($analysis)) {
            throw new RuntimeException('Analyse des dépendances indisponible.');
        }

        if ((string) $analysis['decision'] === 'INCOMPATIBLE') {
            throw new RuntimeException(
                'Orchestration refusée : dépendances incompatibles.'
            );
        }

        if ($stopOnWarning && (string) $analysis['decision'] === 'WARNING') {
            throw new RuntimeException(
                'Orchestration arrêtée : des avertissements sont présents.'
            );
        }

        $plan = (array) ($selected['plan']['steps'] ?? []);
        $planIssues = (array) ($selected['plan']['issues'] ?? []);

        if ($planIssues !== []) {
            throw new RuntimeException(
                'Plan de déploiement invalide : '
                . implode(' | ', array_map('strval', $planIssues))
            );
        }

        if ($plan === []) {
            throw new RuntimeException('Aucun package à orchestrer.');
        }

        $resolvedPlan = [];
        foreach ($plan as $step) {
            $registry = $this->registryByKey((string) $step['package_key']);

            if ($registry === null) {
                throw new RuntimeException(
                    'Package absent du registre : '
                    . (string) $step['package_key']
                );
            }

            $updatePackageId = isset($registry['update_package_id'])
                ? (int) $registry['update_package_id']
                : 0;

            if ($updatePackageId <= 0) {
                throw new RuntimeException(
                    'Aucun package importé n’est associé à '
                    . (string) $registry['package_name']
                    . '.'
                );
            }

            $updatePackage = $this->updatePackage($updatePackageId);
            if ($updatePackage === null) {
                throw new RuntimeException(
                    'Archive de mise à jour introuvable pour '
                    . (string) $registry['package_name']
                    . '.'
                );
            }

            $status = (string) ($updatePackage['statut'] ?? '');
            if (!in_array($status, ['pret', 'echec', 'installe'], true)) {
                throw new RuntimeException(
                    'État non orchestrable pour '
                    . (string) $registry['package_name']
                    . ' : '
                    . $status
                    . '.'
                );
            }

            $resolvedPlan[] = [
                'registry_id' => (int) $registry['id'],
                'update_package_id' => $updatePackageId,
                'package_key' => (string) $registry['package_key'],
                'package_name' => (string) $registry['package_name'],
                'package_version' => (string) $registry['version'],
                'current_status' => $status,
            ];
        }

        $pdo = Database::connection();
        $pdo->beginTransaction();

        try {
            $insert = $pdo->prepare(
                "INSERT INTO system_deployment_orchestrations
                 (
                    root_registry_id,
                    status,
                    decision,
                    current_step,
                    total_steps,
                    score,
                    automatic_rollback,
                    stop_on_warning,
                    plan_json,
                    message,
                    requested_by,
                    created_at
                 )
                 VALUES
                 (
                    :root_registry_id,
                    'PLANNED',
                    :decision,
                    0,
                    :total_steps,
                    :score,
                    :automatic_rollback,
                    :stop_on_warning,
                    :plan_json,
                    'Plan calculé et prêt à démarrer.',
                    :requested_by,
                    NOW()
                 )"
            );

            $insert->execute([
                'root_registry_id' => $rootRegistryId,
                'decision' => (string) $analysis['decision'],
                'total_steps' => count($resolvedPlan),
                'score' => (int) $analysis['score'],
                'automatic_rollback' => $automaticRollback ? 1 : 0,
                'stop_on_warning' => $stopOnWarning ? 1 : 0,
                'plan_json' => json_encode(
                    $resolvedPlan,
                    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
                ),
                'requested_by' => $userId,
            ]);

            $orchestrationId = (int) $pdo->lastInsertId();

            $stepInsert = $pdo->prepare(
                "INSERT INTO system_deployment_orchestration_steps
                 (
                    orchestration_id,
                    step_order,
                    registry_id,
                    update_package_id,
                    package_key,
                    package_name,
                    package_version,
                    status,
                    decision,
                    message
                 )
                 VALUES
                 (
                    :orchestration_id,
                    :step_order,
                    :registry_id,
                    :update_package_id,
                    :package_key,
                    :package_name,
                    :package_version,
                    :status,
                    :decision,
                    :message
                 )"
            );

            foreach ($resolvedPlan as $index => $step) {
                $alreadyInstalled = (string) $step['current_status'] === 'installe';

                $stepInsert->execute([
                    'orchestration_id' => $orchestrationId,
                    'step_order' => $index + 1,
                    'registry_id' => (int) $step['registry_id'],
                    'update_package_id' => (int) $step['update_package_id'],
                    'package_key' => (string) $step['package_key'],
                    'package_name' => (string) $step['package_name'],
                    'package_version' => (string) $step['package_version'],
                    'status' => $alreadyInstalled ? 'SKIPPED' : 'PENDING',
                    'decision' => $alreadyInstalled ? 'ALREADY_INSTALLED' : 'PENDING',
                    'message' => $alreadyInstalled
                        ? 'Package déjà installé : étape ignorée.'
                        : 'En attente de déploiement.',
                ]);
            }

            $pdo->commit();

            return $this->status($orchestrationId);
        } catch (Throwable $error) {
            $pdo->rollBack();
            throw $error;
        }
    }

    /**
     * Exécute une seule étape pour garantir la reprise après interruption.
     *
     * @return array<string,mixed>
     */
    public function advance(int $orchestrationId, ?int $userId): array
    {
        if ($orchestrationId <= 0) {
            throw new RuntimeException('Orchestration invalide.');
        }

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

        $lockPath = $lockDirectory
            . '/orchestration-'
            . $orchestrationId
            . '.lock';

        $handle = @fopen($lockPath, 'c+');

        if (!is_resource($handle)) {
            throw new RuntimeException(
                'Verrou de l’orchestration indisponible.'
            );
        }

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

            $orchestration = $this->orchestration($orchestrationId);

            if (in_array(
                (string) $orchestration['status'],
                ['COMPLETED', 'FAILED', 'ROLLED_BACK', 'CANCELLED'],
                true
            )) {
                return $this->status($orchestrationId);
            }

            if ((string) $orchestration['status'] === 'PLANNED') {
                $this->markStarted($orchestrationId);
            }

            $step = $this->nextPendingStep($orchestrationId);

            if ($step === null) {
                $this->finishSuccess($orchestrationId);
                return $this->status($orchestrationId);
            }

            $this->markStepRunning((int) $step['id']);
            $started = microtime(true);

            try {
                $package = $this->updatePackage(
                    (int) $step['update_package_id']
                );

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

                $manifest = (new UpdatePackageService())->install(
                    $package,
                    (int) ($userId ?? 0)
                );

                $durationMs = (int) round(
                    (microtime(true) - $started) * 1000
                );

                $installedPackage = $this->updatePackage(
                    (int) $step['update_package_id']
                );

                $this->markStepCompleted(
                    (int) $step['id'],
                    $durationMs,
                    isset($installedPackage['backup_job_id'])
                        ? (int) $installedPackage['backup_job_id']
                        : null,
                    [
                        'manifest' => $manifest,
                        'installed_version' => (string) (
                            $manifest['version'] ?? ''
                        ),
                    ]
                );

                $this->refreshProgress($orchestrationId);

                if ($this->nextPendingStep($orchestrationId) === null) {
                    $this->finishSuccess($orchestrationId);
                }
            } catch (Throwable $error) {
                $durationMs = (int) round(
                    (microtime(true) - $started) * 1000
                );

                $failedPackage = $this->updatePackage(
                    (int) $step['update_package_id']
                );

                $backupJobId = isset($failedPackage['backup_job_id'])
                    ? (int) $failedPackage['backup_job_id']
                    : null;

                $this->markStepFailed(
                    (int) $step['id'],
                    $durationMs,
                    $backupJobId,
                    $error
                );

                $orchestration = $this->orchestration($orchestrationId);

                if ((int) $orchestration['automatic_rollback'] === 1) {
                    $this->rollbackCompletedSteps(
                        $orchestrationId,
                        (int) ($userId ?? 0)
                    );
                } else {
                    $this->finishFailure(
                        $orchestrationId,
                        'Déploiement interrompu : ' . $error->getMessage(),
                        'FAILED'
                    );
                }
            }

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

    public function cancel(int $orchestrationId): void
    {
        $statement = Database::connection()->prepare(
            "UPDATE system_deployment_orchestrations
             SET
                status = 'CANCELLED',
                decision = 'CANCELLED',
                message = 'Orchestration annulée avant la prochaine étape.',
                finished_at = NOW()
             WHERE id = :id
               AND status IN ('PLANNED', 'RUNNING')"
        );
        $statement->execute(['id' => $orchestrationId]);
    }

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

        $statement = Database::connection()->prepare(
            "SELECT
                id,
                orchestration_id,
                step_order,
                registry_id,
                update_package_id,
                package_key,
                package_name,
                package_version,
                status,
                decision,
                backup_job_id,
                duration_ms,
                message,
                details_json,
                started_at,
                finished_at
             FROM system_deployment_orchestration_steps
             WHERE orchestration_id = :orchestration_id
             ORDER BY step_order ASC"
        );
        $statement->execute([
            'orchestration_id' => $orchestrationId,
        ]);

        $steps = [];
        foreach ($statement->fetchAll(PDO::FETCH_ASSOC) ?: [] as $step) {
            $details = json_decode(
                (string) ($step['details_json'] ?? ''),
                true
            );
            $step['details'] = is_array($details) ? $details : [];
            unset($step['details_json']);
            $steps[] = $step;
        }
        $statement->closeCursor();

        $completed = count(array_filter(
            $steps,
            static fn(array $step): bool =>
                in_array(
                    (string) $step['status'],
                    ['COMPLETED', 'SKIPPED', 'ROLLED_BACK'],
                    true
                )
        ));

        $total = (int) $orchestration['total_steps'];
        $progress = $total > 0
            ? (int) round(($completed / $total) * 100)
            : 0;

        if (in_array(
            (string) $orchestration['status'],
            ['COMPLETED', 'FAILED', 'ROLLED_BACK', 'CANCELLED'],
            true
        )) {
            $progress = 100;
        }

        return [
            'ok' => true,
            'orchestration' => $orchestration,
            'steps' => $steps,
            'progress' => $progress,
            'completed_steps' => $completed,
            'total_steps' => $total,
            'finished' => in_array(
                (string) $orchestration['status'],
                ['COMPLETED', 'FAILED', 'ROLLED_BACK', 'CANCELLED'],
                true
            ),
        ];
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function registry(): array
    {
        $statement = Database::connection()->query(
            "SELECT
                id,
                update_package_id,
                package_key,
                package_name,
                version,
                package_status,
                category,
                installed_at,
                updated_at
             FROM system_package_registry
             WHERE update_package_id IS NOT NULL
             ORDER BY COALESCE(installed_at, updated_at) DESC, id DESC
             LIMIT 100"
        );

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

        return $rows;
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function orchestrations(): array
    {
        try {
            $statement = Database::connection()->query(
                "SELECT
                    o.id,
                    o.root_registry_id,
                    o.status,
                    o.decision,
                    o.current_step,
                    o.total_steps,
                    o.score,
                    o.automatic_rollback,
                    o.stop_on_warning,
                    o.message,
                    o.requested_by,
                    o.created_at,
                    o.started_at,
                    o.finished_at,
                    r.package_name AS root_package_name,
                    r.version AS root_version
                 FROM system_deployment_orchestrations AS o
                 LEFT JOIN system_package_registry AS r
                    ON r.id = o.root_registry_id
                 ORDER BY o.id DESC
                 LIMIT 40"
            );

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

            return $rows;
        } catch (Throwable) {
            return [];
        }
    }

    /**
     * @return array<string,mixed>
     */
    private function orchestration(int $id): array
    {
        $statement = Database::connection()->prepare(
            "SELECT
                id,
                root_registry_id,
                status,
                decision,
                current_step,
                total_steps,
                score,
                automatic_rollback,
                stop_on_warning,
                plan_json,
                summary_json,
                message,
                requested_by,
                created_at,
                started_at,
                finished_at
             FROM system_deployment_orchestrations
             WHERE id = :id
             LIMIT 1"
        );
        $statement->execute(['id' => $id]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        $statement->closeCursor();

        if (!is_array($row)) {
            throw new RuntimeException('Orchestration introuvable.');
        }

        return $row;
    }

    /**
     * @return array<string,mixed>|null
     */
    private function nextPendingStep(int $orchestrationId): ?array
    {
        $statement = Database::connection()->prepare(
            "SELECT
                id,
                orchestration_id,
                step_order,
                registry_id,
                update_package_id,
                package_key,
                package_name,
                package_version,
                status
             FROM system_deployment_orchestration_steps
             WHERE orchestration_id = :orchestration_id
               AND status = 'PENDING'
             ORDER BY step_order ASC
             LIMIT 1"
        );
        $statement->execute([
            'orchestration_id' => $orchestrationId,
        ]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        $statement->closeCursor();

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

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

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

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

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

    private function markStarted(int $id): void
    {
        $statement = Database::connection()->prepare(
            "UPDATE system_deployment_orchestrations
             SET
                status = 'RUNNING',
                decision = 'RUNNING',
                message = 'Déploiement en cours.',
                started_at = COALESCE(started_at, NOW())
             WHERE id = :id
               AND status = 'PLANNED'"
        );
        $statement->execute(['id' => $id]);
    }

    private function markStepRunning(int $stepId): void
    {
        $statement = Database::connection()->prepare(
            "UPDATE system_deployment_orchestration_steps
             SET
                status = 'RUNNING',
                decision = 'RUNNING',
                message = 'Installation en cours.',
                started_at = NOW()
             WHERE id = :id
               AND status = 'PENDING'"
        );
        $statement->execute(['id' => $stepId]);
    }

    /**
     * @param array<string,mixed> $details
     */
    private function markStepCompleted(
        int $stepId,
        int $durationMs,
        ?int $backupJobId,
        array $details
    ): void {
        $statement = Database::connection()->prepare(
            "UPDATE system_deployment_orchestration_steps
             SET
                status = 'COMPLETED',
                decision = 'INSTALLED',
                backup_job_id = :backup_job_id,
                duration_ms = :duration_ms,
                message = 'Package installé avec succès.',
                details_json = :details_json,
                finished_at = NOW()
             WHERE id = :id"
        );

        $statement->execute([
            'backup_job_id' => $backupJobId,
            'duration_ms' => $durationMs,
            'details_json' => json_encode(
                $details,
                JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
            ),
            'id' => $stepId,
        ]);
    }

    private function markStepFailed(
        int $stepId,
        int $durationMs,
        ?int $backupJobId,
        Throwable $error
    ): void {
        $statement = Database::connection()->prepare(
            "UPDATE system_deployment_orchestration_steps
             SET
                status = 'FAILED',
                decision = 'FAILED',
                backup_job_id = :backup_job_id,
                duration_ms = :duration_ms,
                message = :message,
                details_json = :details_json,
                finished_at = NOW()
             WHERE id = :id"
        );

        $statement->execute([
            'backup_job_id' => $backupJobId,
            'duration_ms' => $durationMs,
            'message' => $error->getMessage(),
            'details_json' => json_encode([
                'type' => get_class($error),
                'message' => $error->getMessage(),
            ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            'id' => $stepId,
        ]);
    }

    private function refreshProgress(int $orchestrationId): void
    {
        $statement = Database::connection()->prepare(
            "SELECT COUNT(*)
             FROM system_deployment_orchestration_steps
             WHERE orchestration_id = :orchestration_id
               AND status IN ('COMPLETED', 'SKIPPED', 'ROLLED_BACK')"
        );
        $statement->execute([
            'orchestration_id' => $orchestrationId,
        ]);

        $completed = (int) $statement->fetchColumn();
        $statement->closeCursor();

        $update = Database::connection()->prepare(
            "UPDATE system_deployment_orchestrations
             SET current_step = :current_step
             WHERE id = :id"
        );
        $update->execute([
            'current_step' => $completed,
            'id' => $orchestrationId,
        ]);
    }

    private function finishSuccess(int $orchestrationId): void
    {
        $statement = Database::connection()->prepare(
            "UPDATE system_deployment_orchestrations
             SET
                status = 'COMPLETED',
                decision = 'DEPLOYED',
                current_step = total_steps,
                message = 'Tous les packages ont été déployés.',
                summary_json = :summary_json,
                finished_at = NOW()
             WHERE id = :id
               AND status IN ('PLANNED', 'RUNNING')"
        );

        $statement->execute([
            'summary_json' => json_encode([
                'result' => 'DEPLOYED',
                'completed_at' => gmdate('c'),
            ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            'id' => $orchestrationId,
        ]);
    }

    private function finishFailure(
        int $orchestrationId,
        string $message,
        string $status
    ): void {
        $statement = Database::connection()->prepare(
            "UPDATE system_deployment_orchestrations
             SET
                status = :status,
                decision = :decision,
                message = :message,
                summary_json = :summary_json,
                finished_at = NOW()
             WHERE id = :id"
        );

        $statement->execute([
            'status' => $status,
            'decision' => $status,
            'message' => $message,
            'summary_json' => json_encode([
                'result' => $status,
                'message' => $message,
                'finished_at' => gmdate('c'),
            ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            'id' => $orchestrationId,
        ]);
    }

    private function rollbackCompletedSteps(
        int $orchestrationId,
        int $userId
    ): void {
        $statement = Database::connection()->prepare(
            "SELECT
                id,
                step_order,
                package_name,
                backup_job_id
             FROM system_deployment_orchestration_steps
             WHERE orchestration_id = :orchestration_id
               AND status = 'COMPLETED'
               AND backup_job_id IS NOT NULL
             ORDER BY step_order DESC"
        );
        $statement->execute([
            'orchestration_id' => $orchestrationId,
        ]);

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

        try {
            /*
             * Un seul point de retour suffit : le backup du premier package
             * installé correspond à l’état antérieur à toute la vague.
             */
            $firstInstalled = end($steps);

            if (!is_array($firstInstalled)) {
                $this->finishFailure(
                    $orchestrationId,
                    'Échec sans package précédemment installé à restaurer.',
                    'FAILED'
                );
                return;
            }

            $backupId = (int) $firstInstalled['backup_job_id'];
            $backup = BackupJob::find($backupId);

            if ($backup === null
                || (string) ($backup['statut'] ?? '') !== 'succes') {
                throw new RuntimeException(
                    'Point de retour automatique indisponible.'
                );
            }

            /*
             * Sauvegarde de secours de l’état dégradé avant restauration.
             */
            (new BackupService())->create(
                $userId,
                'manuel'
            );

            (new BackupService())->restore($backupId);

            /*
             * Les tables d’orchestration peuvent revenir à leur état antérieur.
             * Une trace externe persistante est donc écrite hors restauration.
             */
            $directory = BASE_PATH
                . '/storage/updates/orchestrator';

            if (!is_dir($directory)) {
                @mkdir($directory, 0770, true);
            }

            $entry = json_encode([
                'at' => gmdate('c'),
                'event' => 'deployment.auto_rollback_completed',
                'orchestration_id' => $orchestrationId,
                'backup_id' => $backupId,
            ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

            if (is_string($entry)) {
                @file_put_contents(
                    $directory . '/orchestrator_rollbacks.jsonl',
                    $entry . PHP_EOL,
                    FILE_APPEND | LOCK_EX
                );
            }
        } catch (Throwable $rollbackError) {
            $this->finishFailure(
                $orchestrationId,
                'Échec du rollback automatique : '
                . $rollbackError->getMessage(),
                'FAILED'
            );
        }
    }

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