<?php

declare(strict_types=1);

namespace App\Framework\Deployment\Pipeline\Stages;

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Deployment\Pipeline\ValueObjects\DeploymentEnvironment;
use App\Framework\Deployment\Pipeline\ValueObjects\PipelineStage;
use App\Framework\Deployment\Pipeline\ValueObjects\StageResult;
use App\Framework\Filesystem\ValueObjects\FilePath;
use App\Framework\Logging\Logger;
use App\Framework\Process\Process;
use App\Framework\Process\ValueObjects\Command;

/**
 * Ansible Deploy Stage
 *
 * Deploys the application using Ansible playbooks for production deployments.
 * Provides zero-downtime deployments with rollback capability.
 *
 * Uses the framework's Process module for secure and monitored command execution.
 */
final readonly class AnsibleDeployStage implements PipelineStageInterface
{
    public function __construct(
        private Process $process,
        private Logger $logger,
        private FilePath $ansibleInventoryPath,
        private FilePath $ansiblePlaybookPath
    ) {}

    public function getStage(): PipelineStage
    {
        return PipelineStage::DEPLOY;
    }

    public function execute(DeploymentEnvironment $environment): StageResult
    {
        $startTime = microtime(true);

        $this->logger->info('Starting Ansible deployment', [
            'environment' => $environment->value,
            'playbook' => $this->ansiblePlaybookPath->toString(),
        ]);

        try {
            // Verify ansible-playbook command exists
            if (!$this->process->commandExists('ansible-playbook')) {
                throw new \RuntimeException('ansible-playbook command not found. Please install Ansible.');
            }

            // Build Ansible command
            $command = $this->buildAnsibleCommand($environment);
            $workingDir = FilePath::fromString(dirname($this->ansiblePlaybookPath->toString()));

            $this->logger->debug('Executing Ansible command', [
                'command' => $command->toString(),
                'working_dir' => $workingDir->toString(),
            ]);

            // Execute Ansible playbook via Process module
            $result = $this->process->run(
                command: $command,
                workingDirectory: $workingDir,
                timeout: Duration::fromMinutes(15) // Ansible deployment timeout
            );

            if ($result->isFailed()) {
                $errorMessage = "Ansible deployment failed with exit code {$result->exitCode->value}";
                if ($result->hasErrors()) {
                    $errorMessage .= ":\n{$result->getErrorOutput()}";
                }
                if ($result->hasOutput()) {
                    $errorMessage .= "\n\nOutput:\n{$result->getOutput()}";
                }

                throw new \RuntimeException($errorMessage);
            }

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

            $this->logger->info('Ansible deployment completed', [
                'duration' => $duration->toSeconds(),
                'environment' => $environment->value,
                'runtime_ms' => $result->runtime->toMilliseconds(),
            ]);

            return StageResult::success(
                stage: $this->getStage(),
                duration: $duration,
                output: "Ansible deployment completed successfully\n\n" . $result->getOutput()
            );

        } catch (\Throwable $e) {
            $duration = Duration::fromSeconds(microtime(true) - $startTime);

            $this->logger->error('Ansible deployment failed', [
                'error' => $e->getMessage(),
                'environment' => $environment->value,
                'duration' => $duration->toSeconds(),
            ]);

            return StageResult::failure(
                stage: $this->getStage(),
                duration: $duration,
                error: $e->getMessage()
            );
        }
    }

    public function canRollback(): bool
    {
        return true; // Ansible playbooks support rollback
    }

    public function rollback(DeploymentEnvironment $environment): StageResult
    {
        $startTime = microtime(true);

        $this->logger->warning('Rolling back Ansible deployment', [
            'environment' => $environment->value,
        ]);

        try {
            // Build rollback command
            $rollbackPlaybook = $this->getRollbackPlaybookPath();
            $command = $this->buildRollbackCommand($environment, $rollbackPlaybook);
            $workingDir = FilePath::fromString(dirname($rollbackPlaybook->toString()));

            $this->logger->debug('Executing Ansible rollback command', [
                'command' => $command->toString(),
            ]);

            // Execute rollback via Process module
            $result = $this->process->run(
                command: $command,
                workingDirectory: $workingDir,
                timeout: Duration::fromMinutes(10) // Rollback timeout
            );

            if ($result->isFailed()) {
                $errorMessage = "Ansible rollback failed with exit code {$result->exitCode->value}";
                if ($result->hasErrors()) {
                    $errorMessage .= ":\n{$result->getErrorOutput()}";
                }

                throw new \RuntimeException($errorMessage);
            }

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

            $this->logger->info('Ansible rollback completed', [
                'duration' => $duration->toSeconds(),
                'runtime_ms' => $result->runtime->toMilliseconds(),
            ]);

            return StageResult::success(
                stage: $this->getStage(),
                duration: $duration,
                output: "Rollback completed successfully\n\n" . $result->getOutput()
            );

        } catch (\Throwable $e) {
            $duration = Duration::fromSeconds(microtime(true) - $startTime);

            $this->logger->error('Ansible rollback failed', [
                'error' => $e->getMessage(),
            ]);

            return StageResult::failure(
                stage: $this->getStage(),
                duration: $duration,
                error: $e->getMessage()
            );
        }
    }

    public function getDescription(): string
    {
        return 'Deploy application using Ansible';
    }

    public function shouldSkip(DeploymentEnvironment $environment): bool
    {
        // Only use Ansible for production deployments
        return $environment !== DeploymentEnvironment::PRODUCTION;
    }

    private function buildAnsibleCommand(DeploymentEnvironment $environment): Command
    {
        $inventoryPath = $this->getInventoryPath($environment);
        $playbookPath = $this->ansiblePlaybookPath->toString();

        return Command::fromString(
            "ansible-playbook -i {$inventoryPath} {$playbookPath}"
        );
    }

    private function buildRollbackCommand(DeploymentEnvironment $environment, FilePath $rollbackPlaybook): Command
    {
        $inventoryPath = $this->getInventoryPath($environment);

        return Command::fromString(
            "ansible-playbook -i {$inventoryPath} {$rollbackPlaybook->toString()}"
        );
    }

    private function getInventoryPath(DeploymentEnvironment $environment): string
    {
        $inventoryBase = dirname($this->ansibleInventoryPath->toString());

        return match ($environment) {
            DeploymentEnvironment::PRODUCTION => $inventoryBase . '/production/hosts.yml',
            DeploymentEnvironment::STAGING => $inventoryBase . '/staging/hosts.yml',
            DeploymentEnvironment::DEVELOPMENT => $inventoryBase . '/development/hosts.yml',
        };
    }

    private function getRollbackPlaybookPath(): FilePath
    {
        $playbookDir = dirname($this->ansiblePlaybookPath->toString());
        return FilePath::fromString($playbookDir . '/rollback-git-based.yml');
    }
}
