<?php

namespace Openguru\OpenCartFramework\Migrations;

use Carbon\Carbon;
use Exception;
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
use Psr\Log\LoggerInterface;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RuntimeException;

class MigratorService
{
    private ConnectionInterface $connection;
    private LoggerInterface $logger;
    private string $migrationsPath;

    public function __construct(
        ConnectionInterface $connection,
        LoggerInterface $logger,
        string $migrationsPath
    ) {
        $this->connection = $connection;
        $this->logger = $logger;
        $this->migrationsPath = $migrationsPath;
    }

    public function migrate(): void
    {
        $this->ensureMigrationsTableExists();

        $applied = $this->getAppliedMigrations();
        $migrationsToApply = $this->findMigrationsToApply($applied);
        $count = $this->applyMigrations($migrationsToApply);

        if ($count > 0) {
            $this->logger->info("$count migrations applied");
        }
    }

    private function getMigrationsTableName(): string
    {
        return 'telecart_migrations';
    }

    private function ensureMigrationsTableExists(): void
    {
        $isExists = $this->connection->tableExists($this->getMigrationsTableName());

        if (! $isExists) {
            $this->connection->statement(
                $this->createMigrationsTableSql()
            );

            $this->logger->info('Migrations table created.');
        }
    }

    private function createMigrationsTableSql(): string
    {
        return <<<SQL
            CREATE TABLE IF NOT EXISTS `{$this->getMigrationsTableName()}` (
                migration VARCHAR(191) NOT NULL,
                executed_at DATETIME NOT NULL,
                PRIMARY KEY (migration)
            ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB;
SQL;
    }

    private function findMigrationsToApply(array $applied): array
    {
        $migrations = [];
        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->migrationsPath));

        foreach ($iterator as $file) {
            if ($file->isFile() && $file->getExtension() === 'php') {
                $migrations[] = $file->getFilename();
            }
        }

        return array_diff($migrations, $applied);
    }

    private function getAppliedMigrations(): array
    {
        $migrations = $this->connection->select("SELECT migration FROM {$this->getMigrationsTableName()}");

        return array_column($migrations, 'migration');
    }

    private function applyMigrations(array $files): int
    {
        $count = 0;
        sort($files);

        foreach ($files as $file) {
            try {
                $this->connection->beginTransaction();
                $migration = require $this->migrationsPath . '/' . $file;

                if (! is_object($migration) || ! method_exists($migration, 'up')) {
                    throw new RuntimeException("Migration file $file is invalid.");
                }

                $migration->up();
                $this->writeMigration($file);
                $this->logger->info("Migrated $file");
                $count++;
                $this->connection->commitTransaction();
            } catch (Exception $e) {
                $this->connection->rollbackTransaction();
                $this->logger->error("An error occurred while applying migration.", ['exception' => $e]);
                break;
            }
        }

        return $count;
    }

    private function writeMigration(string $file): void
    {
        $this->connection->insert($this->getMigrationsTableName(), [
            'migration' => $file,
            'executed_at' => Carbon::now(),
        ]);
    }
}
