<?php

declare(strict_types=1);

namespace App\Framework\Deployment\Pipeline\Services;

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Deployment\Pipeline\Configuration\PipelineConfigurationException;
use App\Framework\Deployment\Pipeline\Configuration\YamlPipelineConfigLoader;
use App\Framework\Deployment\Pipeline\Events\PipelineCompleted;
use App\Framework\Deployment\Pipeline\Events\PipelineRolledBack;
use App\Framework\Deployment\Pipeline\Events\PipelineStarted;
use App\Framework\Deployment\Pipeline\Events\StageCompleted;
use App\Framework\Deployment\Pipeline\Events\StageStarted;
use App\Framework\Deployment\Pipeline\Stages\PipelineStageInterface;
use App\Framework\Deployment\Pipeline\ValueObjects\DeploymentEnvironment;
use App\Framework\Deployment\Pipeline\ValueObjects\PipelineConfig;
use App\Framework\Deployment\Pipeline\ValueObjects\PipelineId;
use App\Framework\Deployment\Pipeline\ValueObjects\PipelineResult;
use App\Framework\Deployment\Pipeline\ValueObjects\PipelineStage;
use App\Framework\Deployment\Pipeline\ValueObjects\PipelineStatus;
use App\Framework\Deployment\Pipeline\ValueObjects\StageConfig;
use App\Framework\Deployment\Pipeline\ValueObjects\StageResult;
use App\Framework\DI\Container;
use App\Framework\EventBus\EventBus;
use App\Framework\Logging\Logger;

/**
 * Configurable Pipeline Service
 *
 * Executes deployment pipelines based on YAML configuration files.
 * Supports stage ordering, parameters, timeouts, retries, and conditional execution.
 */
final readonly class ConfigurablePipelineService
{
    public function __construct(
        private Container $container,
        private YamlPipelineConfigLoader $configLoader,
        private EventBus $eventBus,
        private Logger $logger,
        private PipelineStatusStore $statusStore,
        private PipelineHistoryService $historyService
    ) {}

    /**
     * Execute pipeline from YAML configuration file
     */
    public function executeFromConfig(string $configFile): PipelineResult
    {
        $config = $this->configLoader->load($configFile);

        return $this->execute($config);
    }

    /**
     * Execute pipeline from configuration object
     */
    public function execute(PipelineConfig $config): PipelineResult
    {
        $pipelineId = PipelineId::generate();
        $startTime = microtime(true);
        $startedAt = new \DateTimeImmutable();

        $this->logger->info('Starting configurable deployment pipeline', [
            'pipeline_id' => $pipelineId->value,
            'pipeline_name' => $config->name,
            'environment' => $config->environment->value,
            'enabled_stages' => count($config->getEnabledStages())
        ]);

        // Initialize pipeline state
        $stageEnums = array_map(fn(StageConfig $sc) => $sc->stage, $config->getEnabledStages());
        $this->statusStore->initializePipeline($pipelineId, $config->environment, $stageEnums);
        $this->statusStore->updatePipelineStatus($pipelineId, PipelineStatus::RUNNING);

        $this->eventBus->dispatch(new PipelineStarted(
            pipelineId: $pipelineId,
            environment: $config->environment,
            timestamp: new \DateTimeImmutable()
        ));

        $stageResults = [];
        $failedStage = null;

        // Execute enabled stages in order
        foreach ($config->getEnabledStages() as $stageConfig) {
            // Skip stage if environment matches skip list
            if ($stageConfig->shouldSkipForEnvironment($config->environment)) {
                $this->logger->info('Skipping stage for environment', [
                    'pipeline_id' => $pipelineId->value,
                    'stage' => $stageConfig->stage->value,
                    'environment' => $config->environment->value
                ]);
                continue;
            }

            $stageResult = $this->executeStage(
                pipelineId: $pipelineId,
                stageConfig: $stageConfig,
                environment: $config->environment
            );

            $stageResults[] = $stageResult;

            // Handle stage failure
            if ($stageResult->isFailed()) {
                if ($stageConfig->continueOnFailure) {
                    $this->logger->warning('Stage failed but continuing pipeline', [
                        'pipeline_id' => $pipelineId->value,
                        'stage' => $stageConfig->stage->value
                    ]);
                    continue; // Continue to next stage
                }

                if ($config->stopOnFailure) {
                    $failedStage = $stageResult;
                    break; // Stop pipeline
                }
            }
        }

        $totalDuration = Duration::fromSeconds(microtime(true) - $startTime);

        // Handle failure with optional rollback
        if ($failedStage !== null) {
            return $this->handleFailure(
                pipelineId: $pipelineId,
                config: $config,
                failedStage: $failedStage,
                completedStages: $stageResults,
                totalDuration: $totalDuration,
                startedAt: $startedAt
            );
        }

        // Success
        return $this->handleSuccess(
            pipelineId: $pipelineId,
            config: $config,
            stageResults: $stageResults,
            totalDuration: $totalDuration,
            startedAt: $startedAt
        );
    }

    /**
     * Execute single stage with configuration support
     */
    private function executeStage(
        PipelineId $pipelineId,
        StageConfig $stageConfig,
        DeploymentEnvironment $environment
    ): StageResult {
        // Resolve stage implementation from container
        $stage = $this->resolveStage($stageConfig->stage);

        if ($stage === null) {
            return StageResult::failure(
                stage: $stageConfig->stage,
                duration: Duration::fromMilliseconds(0),
                error: "Stage implementation not found: {$stageConfig->stage->value}"
            );
        }

        // Update status store
        $this->statusStore->updateStageStatus($pipelineId, $stageConfig->stage, 'running');

        $this->eventBus->dispatch(new StageStarted(
            pipelineId: $pipelineId,
            stage: $stageConfig->stage,
            timestamp: new \DateTimeImmutable()
        ));

        $this->logger->info('Executing stage with config', [
            'pipeline_id' => $pipelineId->value,
            'stage' => $stageConfig->stage->value,
            'timeout' => $stageConfig->timeoutSeconds,
            'retries' => $stageConfig->retries
        ]);

        // Execute with retries
        $result = $this->executeWithRetries($stage, $environment, $stageConfig);

        // Update status store
        $this->statusStore->updateStageStatus(
            pipelineId: $pipelineId,
            stage: $stageConfig->stage,
            status: $result->isSuccess() ? 'success' : 'failed',
            durationMs: (int) $result->duration->toMilliseconds(),
            error: $result->error
        );

        $this->eventBus->dispatch(new StageCompleted(
            pipelineId: $pipelineId,
            stage: $stageConfig->stage,
            success: $result->isSuccess(),
            duration: $result->duration,
            timestamp: new \DateTimeImmutable()
        ));

        return $result;
    }

    /**
     * Execute stage with retry logic
     */
    private function executeWithRetries(
        PipelineStageInterface $stage,
        DeploymentEnvironment $environment,
        StageConfig $config
    ): StageResult {
        $attempt = 0;
        $maxAttempts = $config->retries + 1; // Initial attempt + retries

        while ($attempt < $maxAttempts) {
            $attempt++;

            if ($attempt > 1) {
                $this->logger->info('Retrying stage', [
                    'stage' => $config->stage->value,
                    'attempt' => $attempt,
                    'max_attempts' => $maxAttempts
                ]);
            }

            $result = $stage->execute($environment);

            if ($result->isSuccess()) {
                return $result;
            }

            if ($attempt < $maxAttempts) {
                // Wait before retry (exponential backoff)
                $waitSeconds = min(2 ** ($attempt - 1), 30); // Max 30 seconds
                sleep($waitSeconds);
            }
        }

        // All retries failed
        return $result ?? StageResult::failure(
            stage: $config->stage,
            duration: Duration::fromMilliseconds(0),
            error: 'Stage execution failed after all retry attempts'
        );
    }

    /**
     * Resolve stage implementation from container
     */
    private function resolveStage(PipelineStage $stage): ?PipelineStageInterface
    {
        // Map stage enum to implementation class
        $stageClass = match ($stage) {
            PipelineStage::BUILD => \App\Framework\Deployment\Pipeline\Stages\BuildStage::class,
            PipelineStage::TEST => \App\Framework\Deployment\Pipeline\Stages\TestStage::class,
            PipelineStage::SECURITY_CHECK => \App\Framework\Deployment\Pipeline\Stages\SecurityCheckStage::class,
            PipelineStage::BACKUP => \App\Framework\Deployment\Pipeline\Stages\BackupStage::class,
            PipelineStage::DEPLOY => \App\Framework\Deployment\Pipeline\Stages\DeployStage::class,
            PipelineStage::HEALTH_CHECK => \App\Framework\Deployment\Pipeline\Stages\HealthCheckStage::class,
            PipelineStage::CLEANUP => \App\Framework\Deployment\Pipeline\Stages\CleanupStage::class,
            default => null
        };

        if ($stageClass === null) {
            return null;
        }

        try {
            return $this->container->get($stageClass);
        } catch (\Exception $e) {
            $this->logger->error('Failed to resolve stage from container', [
                'stage' => $stage->value,
                'class' => $stageClass,
                'error' => $e->getMessage()
            ]);
            return null;
        }
    }

    /**
     * Handle successful pipeline completion
     *
     * @param array<StageResult> $stageResults
     */
    private function handleSuccess(
        PipelineId $pipelineId,
        PipelineConfig $config,
        array $stageResults,
        Duration $totalDuration,
        \DateTimeImmutable $startedAt
    ): PipelineResult {
        $status = PipelineStatus::SUCCESS;
        $completedAt = new \DateTimeImmutable();

        $this->statusStore->updatePipelineStatus($pipelineId, $status);

        $this->eventBus->dispatch(new PipelineCompleted(
            pipelineId: $pipelineId,
            status: $status,
            duration: $totalDuration,
            timestamp: $completedAt
        ));

        $this->logger->info('Pipeline completed successfully', [
            'pipeline_id' => $pipelineId->value,
            'pipeline_name' => $config->name,
            'duration_ms' => $totalDuration->toMilliseconds(),
            'stages_completed' => count($stageResults)
        ]);

        $result = new PipelineResult(
            pipelineId: $pipelineId,
            environment: $config->environment,
            status: $status,
            totalDuration: $totalDuration,
            stageResults: $stageResults
        );

        $this->historyService->recordPipeline($result, $startedAt, $completedAt);

        return $result;
    }

    /**
     * Handle pipeline failure with optional rollback
     *
     * @param array<StageResult> $completedStages
     */
    private function handleFailure(
        PipelineId $pipelineId,
        PipelineConfig $config,
        StageResult $failedStage,
        array $completedStages,
        Duration $totalDuration,
        \DateTimeImmutable $startedAt
    ): PipelineResult {
        $this->logger->error('Pipeline failed', [
            'pipeline_id' => $pipelineId->value,
            'pipeline_name' => $config->name,
            'failed_stage' => $failedStage->stage->value,
            'error' => $failedStage->error,
            'rollback_enabled' => $config->enableRollback
        ]);

        $rollbackSuccessful = false;

        // Attempt rollback if enabled
        if ($config->enableRollback) {
            $rollbackStartTime = microtime(true);
            $rollbackSuccessful = $this->rollback($pipelineId, $config, $completedStages);
            $rollbackDuration = Duration::fromSeconds(microtime(true) - $rollbackStartTime);

            if ($rollbackSuccessful) {
                $this->eventBus->dispatch(new PipelineRolledBack(
                    pipelineId: $pipelineId,
                    failedStage: $failedStage->stage,
                    reason: $failedStage->error ?? 'Unknown error',
                    rollbackDuration: $rollbackDuration,
                    timestamp: new \DateTimeImmutable()
                ));
            }
        }

        $status = $rollbackSuccessful ? PipelineStatus::ROLLED_BACK : PipelineStatus::FAILED;
        $completedAt = new \DateTimeImmutable();

        $this->statusStore->updatePipelineStatus($pipelineId, $status);

        $this->eventBus->dispatch(new PipelineCompleted(
            pipelineId: $pipelineId,
            status: $status,
            duration: $totalDuration,
            timestamp: $completedAt
        ));

        $result = new PipelineResult(
            pipelineId: $pipelineId,
            environment: $config->environment,
            status: $status,
            totalDuration: $totalDuration,
            stageResults: $completedStages,
            error: $failedStage->error
        );

        $this->historyService->recordPipeline($result, $startedAt, $completedAt);

        return $result;
    }

    /**
     * Rollback completed stages
     *
     * @param array<StageResult> $completedStages
     */
    private function rollback(
        PipelineId $pipelineId,
        PipelineConfig $config,
        array $completedStages
    ): bool {
        $rollbackSuccessful = true;

        // Reverse order: most recent stages first
        $reversedStages = array_reverse($config->getEnabledStages());

        foreach ($reversedStages as $stageConfig) {
            $stage = $this->resolveStage($stageConfig->stage);

            if ($stage === null || !$stage->canRollback()) {
                continue;
            }

            // Check if stage was executed successfully
            $wasExecuted = false;
            foreach ($completedStages as $stageResult) {
                if ($stageResult->stage === $stageConfig->stage && $stageResult->isSuccess()) {
                    $wasExecuted = true;
                    break;
                }
            }

            if (!$wasExecuted) {
                continue;
            }

            $this->logger->info('Rolling back stage', [
                'pipeline_id' => $pipelineId->value,
                'stage' => $stageConfig->stage->value
            ]);

            try {
                $rollbackResult = $stage->rollback($config->environment);

                if ($rollbackResult->isFailed()) {
                    $rollbackSuccessful = false;
                }
            } catch (\Throwable $e) {
                $this->logger->error('Stage rollback failed', [
                    'pipeline_id' => $pipelineId->value,
                    'stage' => $stageConfig->stage->value,
                    'error' => $e->getMessage()
                ]);
                $rollbackSuccessful = false;
            }
        }

        return $rollbackSuccessful;
    }
}
