<?php

declare(strict_types=1);

namespace App\Framework\Process;

use App\Framework\Console\ExitCode;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DI\Attributes\DefaultImplementation;
use App\Framework\Filesystem\ValueObjects\FilePath;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Process\Exceptions\ProcessException;
use App\Framework\Process\ValueObjects\Command;
use App\Framework\Process\ValueObjects\EnvironmentVariables;

/**
 * System-basierte Prozess-Implementation.
 *
 * Verwendet proc_open() für Prozess-Ausführung mit voller Kontrolle
 * über stdin, stdout, stderr, Umgebungsvariablen und Timeout.
 */

#[DefaultImplementation]
final readonly class SystemProcess implements Process
{
    public function __construct(
        private Logger $logger
    ) {
    }

    public function run(
        Command $command,
        ?EnvironmentVariables $env = null,
        ?FilePath $workingDirectory = null,
        ?Duration $timeout = null
    ): ProcessResult {
        $startTime = microtime(true);

        // Validate working directory if provided
        if ($workingDirectory !== null && ! $workingDirectory->isDirectory()) {
            throw ProcessException::invalidWorkingDirectory($workingDirectory->toString());
        }

        $descriptors = [
            0 => ['pipe', 'r'],  // stdin
            1 => ['pipe', 'w'],  // stdout
            2 => ['pipe', 'w'],  // stderr
        ];

        $pipes = [];
        $process = proc_open(
            command: $command->getValue(),
            descriptor_spec: $descriptors,
            pipes: $pipes,
            cwd: $workingDirectory?->toString(),
            env_vars: $env?->toArray(),
            options: ['bypass_shell' => $command->isArray()]
        );

        if (! is_resource($process)) {
            throw ProcessException::failedToStart($command, 'proc_open failed');
        }

        // Close stdin - we're not sending input
        fclose($pipes[0]);

        // Apply timeout if specified
        if ($timeout !== null) {
            $this->applyTimeout($pipes, $timeout);
        }

        // Read outputs with timeout handling
        $stdout = $this->readStream($pipes[1], $timeout);
        $stderr = $this->readStream($pipes[2], $timeout);

        fclose($pipes[1]);
        fclose($pipes[2]);

        // Get exit code
        $exitCodeValue = proc_close($process);

        // Calculate runtime
        $runtimeSeconds = microtime(true) - $startTime;
        $runtime = Duration::fromSeconds($runtimeSeconds);

        // Map exit code to ExitCode enum
        $exitCode = $this->mapExitCode($exitCodeValue);

        $result = new ProcessResult(
            command: $command,
            stdout: $stdout,
            stderr: $stderr,
            exitCode: $exitCode,
            runtime: $runtime
        );

        $this->logger->debug(
            'Process executed',
            LogContext::withData([
                'command' => $command->toString(),
                'exit_code' => $exitCodeValue,
                'runtime_ms' => $runtime->toMilliseconds(),
                'success' => $result->isSuccess(),
            ])
        );

        return $result;
    }

    public function start(
        Command $command,
        ?EnvironmentVariables $env = null,
        ?FilePath $workingDirectory = null
    ): RunningProcess {
        // Validate working directory if provided
        if ($workingDirectory !== null && ! $workingDirectory->isDirectory()) {
            throw ProcessException::invalidWorkingDirectory($workingDirectory->toString());
        }

        $descriptors = [
            0 => ['pipe', 'r'],
            1 => ['pipe', 'w'],
            2 => ['pipe', 'w'],
        ];

        $pipes = [];
        $process = proc_open(
            command: $command->getValue(),
            descriptor_spec: $descriptors,
            pipes: $pipes,
            cwd: $workingDirectory?->toString(),
            env_vars: $env?->toArray(),
            options: ['bypass_shell' => $command->isArray()]
        );

        if (! is_resource($process)) {
            throw ProcessException::failedToStart($command, 'proc_open failed for async start');
        }

        // Get process status to extract PID
        $status = proc_get_status($process);
        $pid = $status['pid'] ?? 0;

        $this->logger->debug(
            'Process started asynchronously',
            LogContext::withData([
                'command' => $command->toString(),
                'pid' => $pid,
            ])
        );

        return new SystemRunningProcess(
            process: $process,
            pipes: $pipes,
            command: $command,
            pid: $pid,
            logger: $this->logger
        );
    }

    public function commandExists(string $commandName): bool
    {
        $result = $this->run(
            command: Command::fromArray(['which', $commandName])
        );

        return $result->isSuccess();
    }

    /**
     * Wendet ein Timeout auf die Pipes an.
     *
     * @param array<int, resource> $pipes
     */
    private function applyTimeout(array $pipes, Duration $timeout): void
    {
        $timeoutSeconds = $timeout->toTimeoutSeconds();

        foreach ($pipes as $pipe) {
            if (is_resource($pipe)) {
                stream_set_timeout($pipe, $timeoutSeconds);
            }
        }
    }

    /**
     * Liest einen Stream mit optionalem Timeout.
     *
     * @param resource $stream
     */
    private function readStream($stream, ?Duration $timeout): string
    {
        if (! is_resource($stream)) {
            return '';
        }

        $content = stream_get_contents($stream);

        if ($content === false) {
            return '';
        }

        // Check for timeout
        if ($timeout !== null) {
            $metadata = stream_get_meta_data($stream);
            if ($metadata['timed_out'] ?? false) {
                return $content; // Return partial content on timeout
            }
        }

        return $content;
    }

    /**
     * Mappt einen numerischen Exit-Code zum ExitCode Enum.
     */
    private function mapExitCode(int $exitCodeValue): ExitCode
    {
        return match ($exitCodeValue) {
            0 => ExitCode::SUCCESS,
            1 => ExitCode::GENERAL_ERROR,
            2 => ExitCode::USAGE_ERROR,
            64 => ExitCode::COMMAND_NOT_FOUND,
            65 => ExitCode::INVALID_INPUT,
            66 => ExitCode::NO_INPUT,
            69 => ExitCode::UNAVAILABLE,
            70 => ExitCode::SOFTWARE_ERROR,
            71 => ExitCode::OS_ERROR,
            72 => ExitCode::OS_FILE_ERROR,
            73 => ExitCode::CANT_CREATE,
            74 => ExitCode::IO_ERROR,
            75 => ExitCode::TEMP_FAIL,
            76 => ExitCode::PROTOCOL_ERROR,
            77 => ExitCode::NO_PERMISSION,
            78 => ExitCode::CONFIG_ERROR,
            79 => ExitCode::DATABASE_ERROR,
            80 => ExitCode::RATE_LIMITED,
            126 => ExitCode::PERMISSION_DENIED,
            130 => ExitCode::INTERRUPTED,
            default => ExitCode::GENERAL_ERROR,
        };
    }
}
