<?php

declare(strict_types=1);

namespace App\Command;

use App\Controller\OgAgent\CreateImageAction;
use App\Controller\DeployImageAction;
use App\Controller\OgAgent\PartitionAssistantAction;
use App\Controller\OgAgent\RunScriptAction;
use App\Dto\Input\CommandExecuteInput;
use App\Dto\Input\DeployImageInput;
use App\Dto\Input\PartitionInput;
use App\Dto\Input\PartitionPostInput;
use App\Dto\Input\MultipleClientsInput;
use App\Dto\Input\BootClientsInput;
use App\Dto\Output\ClientOutput;
use App\Dto\Output\PartitionOutput;
use App\Entity\Client;
use App\Model\ClientStatus;
use App\Entity\Image;
use App\Entity\ImageImageRepository;
use App\Entity\Partition;
use App\Entity\Trace;
use App\Model\CommandTypes;
use App\Model\TraceStatus;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Psr\Log\LoggerInterface;

#[AsCommand(name: 'opengnsys:execute-pending-traces', description: 'Execute pending traces')]
class ExecutePendingTracesCommand extends Command
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager,
        private readonly CreateImageAction $createImageAction,
        private readonly DeployImageAction $deployImageAction,
        private readonly PartitionAssistantAction $partitionAssistantAction,
        private readonly RunScriptAction $runScriptAction,
        private readonly \App\Controller\OgAgent\RebootAction $rebootAction,
        private readonly \App\Controller\OgAgent\PowerOffAction $powerOffAction,
        private readonly \App\Controller\OgAgent\LoginAction $loginAction,
        private readonly LoggerInterface $logger,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $startTime = microtime(true);

        $this->logger->info('Executing pending traces command');

        $traces = $this->entityManager->getRepository(Trace::class)
            ->createQueryBuilder('t')
            ->select('t')
            ->where('t.status = :status')
            ->setParameter('status', TraceStatus::PENDING)
            ->andWhere('t.id IN (
                SELECT MIN(t2.id) 
                FROM App\Entity\Trace t2 
                WHERE t2.status = :status 
                GROUP BY t2.client
            )')
            ->orderBy('t.id', 'ASC')
            ->getQuery()
            ->getResult();

        $count = count($traces);
        $io->info("Found $count pending trace(s) - processing the latest one");

        $processedCount = 0;
        $errorCount = 0;

        foreach ($traces as $trace) {
            try {
                $client = $trace->getClient();

                if ($client->getStatus() === ClientStatus::BUSY) {
                    continue;
                }
                
                $io->info("Processing trace {$trace->getUuid()} with command: {$trace->getCommand()}");
                
                $result = $this->executeTrace($trace);
                
                if ($result) {
                    $processedCount++;
                    $io->success("Successfully processed trace {$trace->getUuid()}");
                } else {
                    $errorCount++;
                    $io->error("Failed to process trace {$trace->getUuid()}");
                }
            } catch (\Exception $e) {
                $errorCount++;
                $io->error("Error processing trace {$trace->getUuid()}: " . $e->getMessage());
                
                $trace->setStatus(TraceStatus::FAILED);
                $trace->setOutput($e->getMessage());
                $trace->setFinishedAt(new \DateTime());
                $this->entityManager->persist($trace);
            }
        }

        $this->entityManager->flush();

        $executionTime = microtime(true) - $startTime;
        $io->success("Processed $processedCount traces successfully, $errorCount failed");
        $io->note("Execution time: " . round($executionTime, 3) . "s");

        return Command::SUCCESS;
    }

    private function executeTrace(Trace $trace): bool
    {
        $command = $trace->getCommand();
        $input = $trace->getInput() ?? [];
        $client = $trace->getClient();

        $this->logger->info('Executing trace', [
            'command' => $command,
            'input' => $input,
            'client' => $client->getUuid()
        ]);

        if (!$client) {
            throw new \Exception("No client associated with trace");
        }

        $trace->setExecutedAt(new \DateTime());
        $this->entityManager->persist($trace);
        $this->entityManager->flush();

        try {
            switch ($command) {
                case CommandTypes::CREATE_IMAGE:
                    $this->logger->info('Executing create image trace', [
                        'input' => $input,
                        'client' => $client->getUuid()
                    ]);
                    return $this->executeCreateImage($trace, $input);
                
                case CommandTypes::DEPLOY_IMAGE:
                    $this->logger->info('Executing deploy image trace', [
                        'input' => $input,
                        'client' => $client->getUuid()
                    ]);
                    return $this->executeDeployImage($trace, $input);
                
                case CommandTypes::PARTITION_AND_FORMAT:
                    $this->logger->info('Executing partition and format trace', [
                        'input' => $input,
                        'client' => $client->getUuid()
                    ]);
                    return $this->executePartitionAssistant($trace, $input);
                
                case CommandTypes::RUN_SCRIPT:
                    $this->logger->info('Executing run script trace', [
                        'input' => $input,
                        'client' => $client->getUuid()
                    ]);
                    return $this->executeRunScript($trace, $input);
                
                case CommandTypes::POWER_ON:
                    $this->logger->info('Executing power on trace', [
                        'input' => $input,
                        'client' => $client->getUuid()
                    ]);
                    return $this->executePowerOn($trace, $input);
                
                case CommandTypes::REBOOT:
                    $this->logger->info('Executing reboot trace', [
                        'input' => $input,
                        'client' => $client->getUuid()
                    ]);
                    return $this->executeReboot($trace, $input);
                
                case CommandTypes::SHUTDOWN:
                    $this->logger->info('Executing shutdown trace', [
                        'input' => $input,
                        'client' => $client->getUuid()
                    ]);
                    return $this->executeShutdown($trace, $input);
                
                case CommandTypes::LOGIN:
                    $this->logger->info('Executing login trace', [
                        'input' => $input,
                        'client' => $client->getUuid()
                    ]);
                    return $this->executeLogin($trace, $input);
                
                
                default:
                    throw new \Exception("Unsupported command type: $command");
            }
        } catch (\Exception $e) {
            $this->logger->error('Error executing trace', [
                'command' => $command,
                'input' => $input,
                'client' => $client->getUuid(),
                'error' => $e->getMessage()
            ]);
            $trace->setStatus(TraceStatus::FAILED);
            $trace->setOutput($e->getMessage());
            $trace->setFinishedAt(new \DateTime());
            $this->entityManager->persist($trace);
            $this->entityManager->flush();
            
            throw $e;
        }
    }

    private function executeCreateImage(Trace $trace, array $input): bool
    {
        $client = $trace->getClient();
        
        $image = null;
        
        if (isset($input['action']) && $input['action'] === 'create') {
            $image = new Image();
            $image->setName($input['imageName'] ?? $input['name']);
            $image->setType($input['type'] ?? 'monolithic');
            $image->setRemotePc($input['remotePc'] ?? false);
            $image->setIsGlobal($input['isGlobal'] ?? false);
            $image->setClient($client);
            $this->entityManager->persist($image);
        } elseif (isset($input['action']) && $input['action'] === 'update') {
            if (!isset($input['imageUuid'])) {
                throw new \Exception("Image UUID not found in trace input for update operation");
            }
            
            $image = $this->entityManager->getRepository(Image::class)
                ->findOneBy(['uuid' => $input['imageUuid']]);
                
            if (!$image) {
                throw new \Exception("Image not found with UUID: {$input['imageUuid']} for update operation");
            }
        } else {
            throw new \Exception("Either 'create' or 'update' field must be set to true in trace input");
        }
        
        if (isset($input['diskNumber']) && isset($input['partitionNumber'])) {
            $partitionInfo = [
                'numDisk' => $input['diskNumber'],
                'numPartition' => $input['partitionNumber'],
                'partitionCode' => $input['partitionCode'] ?? 'LINUX',
                'filesystem' => $input['filesystem'] ?? 'EXT4'
            ];
            $image->setPartitionInfo(json_encode($partitionInfo));
        }

        $partition = null;
        if (isset($input['diskNumber']) && isset($input['partitionNumber'])) {
            $partition = $this->entityManager->getRepository(Partition::class)
                ->findOneBy([
                    'client' => $client,
                    'diskNumber' => $input['diskNumber'],
                    'partitionNumber' => $input['partitionNumber']
                ]);
            
            if (!$partition) {
                throw new \Exception("Partition not found for client {$client->getUuid()} with disk {$input['diskNumber']} and partition {$input['partitionNumber']}");
            }
        }

        $this->logger->info('Creating image', [
            'image' => $image,
            'partition' => $partition,
            'client' => $client->getUuid()
        ]);

        try {
            $response = $this->createImageAction->__invoke(
                queue: false,
                image: $image,
                partition: $partition,
                client: $client,
                gitRepositoryName: $input['gitRepositoryName'] ?? null,
                existingTrace: $trace
            );

            if (json_decode($response->getContent(), true)['status'] === 200) {
                $trace->setStatus(TraceStatus::IN_PROGRESS);
                $trace->setFinishedAt(new \DateTime());
                $this->entityManager->persist($trace);
                $this->entityManager->flush();
                return true;
            }
            
            return false;
        } catch (\Exception $e) {
            
            return false;
        }
    }

    private function executeDeployImage(Trace $trace, array $input): bool
    {
        $client = $trace->getClient();
        
        if (!isset($input['imageImageRepository']) && !isset($input['image'])) {
            throw new \Exception("ImageImageRepository UUID or image UUID not found in trace input for deploy image");
        }

        $imageImageRepository = $this->entityManager->getRepository(ImageImageRepository::class)
            ->findOneBy(['uuid' => $input['imageImageRepository'] ?? $input['image']]);

        if (!$imageImageRepository) {
            throw new \Exception("ImageImageRepository not found with UUID: {$input['imageImageRepository']}");
        }

        $diskNumber = isset($input['diskNumber']) ? $input['diskNumber'] : $input['numDisk'];
        $partitionNumber = isset($input['partitionNumber']) ? $input['partitionNumber'] : $input['numPartition'];

        if (!$diskNumber || !$partitionNumber) {
            throw new \Exception("Disk number or partition number not found in trace input for deploy image");
        }

        $deployInput = new DeployImageInput();
        $deployInput->method = $input['method'] ?? 'unicast';
        $deployInput->type = $input['type'] ?? 'monolithic';
        $deployInput->diskNumber = (int) $diskNumber;
        $deployInput->partitionNumber = (int) $partitionNumber;
        $deployInput->mcastMode = $input['mcastMode'] ?? null;
        $deployInput->mcastSpeed = $input['mcastSpeed'] ?? null;
        $deployInput->mcastPort = $input['mcastPort'] ?? null;
        $deployInput->mcastIp = $input['mcastIp'] ?? null;
        $deployInput->maxClients = $input['maxClients'] ?? null;
        $deployInput->maxTime = $input['maxTime'] ?? null;
        $deployInput->p2pMode = $input['p2pMode'] ?? null;
        $deployInput->p2pTime = $input['p2pTime'] ?? null;
        $deployInput->queue = $input['queue'] ?? false;
    
        $deployInput->clients = [new ClientOutput($client)];

        $this->logger->info('Deploying image', [
            'input' => $deployInput,
            'client' => $client->getUuid()
        ]);

        $response = $this->deployImageAction->__invoke($deployInput, $imageImageRepository, $trace);
        
        $clientJobs = $response['data'];
        
        $clientKey = '/clients/' . $client->getUuid();
        $jobId = $clientJobs[$clientKey] ?? [];

        $trace->setJobId($jobId['jobId']);
        $trace->setStatus(TraceStatus::IN_PROGRESS);
        $trace->setFinishedAt(new \DateTime());
        $this->entityManager->persist($trace);
        $this->entityManager->flush();

        return true;
    }

    private function executePartitionAssistant(Trace $trace, array $input): bool
    {
        $client = $trace->getClient();
        
        $partitionInput = new PartitionPostInput();
        $partitionInput->clients = [new ClientOutput($client)];
        
        $partitions = [];
        
        foreach ($input as $item) {
            $partitionInputObj = new PartitionInput();
            $partitionInputObj->diskNumber = $item['diskNumber'];
            $partitionInputObj->partitionNumber = (int)$item['partitionNumber'];
            $partitionInputObj->partitionCode = $item['partitionCode'] ?? 'LINUX';
            $partitionInputObj->size = $item['size']; 
            $partitionInputObj->filesystem = $item['filesystem'] ?? 'EXT4';
            $partitionInputObj->format = $item['format'] ?? false;
            $partitions[] = $partitionInputObj;
        }
        
        $partitionInput->partitions = $partitions;
        $partitionInput->queue = false;

        $this->logger->info('Executing partition assistant', [
            'input' => $partitionInput,
            'client' => $client->getUuid()
        ]);

        try {
            $response = $this->partitionAssistantAction->__invoke($partitionInput, $trace);

            if (json_decode($response->getContent(), true)['status'] === 200) {
                $trace->setStatus(TraceStatus::SUCCESS);
                $trace->setFinishedAt(new \DateTime());
                $this->entityManager->persist($trace);
                $this->entityManager->flush();
                return true;
            }
            
            return false;
        } catch (\Exception $e) {
            
            return false;
        }
    }

    private function executeRunScript(Trace $trace, array $input): bool
    {
        $client = $trace->getClient();
        
        if (!isset($input['script'])) {
            throw new \Exception("Script not found in trace input");
        }

        $commandExecuteInput = new CommandExecuteInput();
        $commandExecuteInput->clients = [new ClientOutput($client)];
        $commandExecuteInput->script = $input['script'];
        $commandExecuteInput->queue = false;

        $this->logger->info('Executing run script', [
            'input' => $commandExecuteInput,
            'client' => $client->getUuid()
        ]);

        try {
            $response = $this->runScriptAction->__invoke($commandExecuteInput, $trace);
            
            if (json_decode($response->getContent(), true)['status'] === 200) {
                $trace->setStatus(TraceStatus::SUCCESS);
                $trace->setFinishedAt(new \DateTime());
                $this->entityManager->persist($trace);
                $this->entityManager->flush();
                return true;
            }
            
            return false;
        } catch (\Exception $e) {
            $this->logger->error('Error executing run script', [
                'input' => $commandExecuteInput,
                'client' => $client->getUuid(),
                'error' => $e->getMessage()
            ]);
            return false;
        }
    }

    private function executePowerOn(Trace $trace, array $input): bool
    {
        $trace->setStatus(TraceStatus::SUCCESS);
        $trace->setFinishedAt(new \DateTime());
        $this->entityManager->persist($trace);
        $this->entityManager->flush();
        return true;
    }

    private function executeReboot(Trace $trace, array $input): bool
    {
        $client = $trace->getClient();
        
        $multipleClientsInput = new MultipleClientsInput();
        $multipleClientsInput->clients = [new ClientOutput($client)];
        $multipleClientsInput->queue = false;

        try {
            $response = $this->rebootAction->__invoke($multipleClientsInput);
            
            if (json_decode($response->getContent(), true)['status'] === 200) {
                $trace->setStatus(TraceStatus::SUCCESS);
                $trace->setFinishedAt(new \DateTime());
                $this->entityManager->persist($trace);
                $this->entityManager->flush();
                return true;
            }
            
            return false;
        } catch (\Exception $e) {
            return false;
        }
    }

    private function executeShutdown(Trace $trace, array $input): bool
    {
        $client = $trace->getClient();
        
        $multipleClientsInput = new MultipleClientsInput();
        $multipleClientsInput->clients = [new ClientOutput($client)];
        $multipleClientsInput->queue = false;

        try {
            $response = $this->powerOffAction->__invoke($multipleClientsInput);
            
            if ($response->getStatusCode() === 200) {
                $trace->setStatus(TraceStatus::SUCCESS);
                $trace->setFinishedAt(new \DateTime());
                $this->entityManager->persist($trace);
                $this->entityManager->flush();
                return true;
            }
            
            return false;
        } catch (\Exception $e) {
            return false;
        }
    }

    private function executeLogin(Trace $trace, array $input): bool
    {
        $client = $trace->getClient();
        
        if (!isset($input['partition'])) {
            throw new \Exception("Partition not found in trace input");
        }

        $partition = $this->entityManager->getRepository(\App\Entity\Partition::class)
            ->findOneBy(['uuid' => $input['partition']]);

        if (!$partition) {
            throw new \Exception("Partition not found with UUID: {$input['partition']}");
        }

        $bootClientsInput = new BootClientsInput();
        $bootClientsInput->clients = [new ClientOutput($client)];
        $bootClientsInput->partition = new PartitionOutput($partition);

        try {
            $response = $this->loginAction->__invoke($bootClientsInput);
            
            if ($response->getStatusCode() === 200) {
                $trace->setStatus(TraceStatus::SUCCESS);
                $trace->setFinishedAt(new \DateTime());
                $this->entityManager->persist($trace);
                $this->entityManager->flush();
                return true;
            }
            
            return false;
        } catch (\Exception $e) {
            return false;
        }
    }


} 