<?php
require_once implode(DIRECTORY_SEPARATOR, [__DIR__, 'vendor', 'autoload.php']);

class TableRepository
{
    public function __construct(protected Incoviba\Common\Define\Connection $connection, protected string $database) {}

    public function getTables(): array
    {
        if (!isset($this->tables)) {
            $results = $this->connection->query("SHOW TABLES");
            $rows = $results->fetchAll(PDO::FETCH_ASSOC);
            $this->tables = array_map(function($row) {
                return $row["Tables_in_{$this->database}"];
            }, $rows);
        }
        return $this->tables;
    }
    public function getTableDefinition(string $table): string
    {
        if (!isset($this->definitions[$table])) {
            $results = $this->connection->query("SHOW CREATE TABLE {$table}");
            $row = $results->fetch(PDO::FETCH_ASSOC);
            $this->definitions[$table] = $row['Create Table'];
        }
        return $this->definitions[$table];
    }
    public function parseTableDefinition(string $table, int $offset = 2): string
    {
        $this->extractLines($table);
        $tableLine = "\$this->table('{$table}'";
        if (count($this->primary) === 1) {
            $tableLine .= ", ['id' => '{$this->primary[0]}']";
        } elseif (count($this->primary) > 1) {
            $primaryString = implode(', ', array_map(function($key) {return "'{$key}'";}, $this->primary));
            $tableLine .= ", ['id' = false, 'primary_key' => [{$primaryString}]]";
        }
        $tableLine .= ')';
        $output = [$this->prefixLine($tableLine, $offset)];
        foreach ($this->columns as $column) {
            $output []= $this->prefixLine($column, $offset + 1);
        }
        foreach ($this->constraints as $constraint) {
            $output []= $this->prefixLine($constraint, $offset + 1);
        }
        $output []= $this->prefixLine('->create();', $offset + 1);
        return implode(PHP_EOL, $output);
    }
    public function getTableIndex(string $table): int
    {
        return array_search($table, $this->getTables());
    }

    protected array $tables;
    protected array $definitions;
    protected array $primary;
    protected array $columns;
    protected array $constraints;

    protected function extractLines(string $table): void
    {
        $this->primary = [];
        $this->columns = [];
        $this->constraints = [];
        $lines = explode(PHP_EOL, $this->definitions[$table]);

        array_shift($lines);
        foreach ($lines as $line) {
            if (str_starts_with(trim($line), ') ENGINE')) {
                break;
            }
            if (str_starts_with(trim($line), '`id`') or str_starts_with(trim($line), 'KEY')) {
                continue;
            }
            if (str_starts_with(trim($line), 'PRIMARY KEY')) {
                if (!str_contains($line, '`id`')) {
                    $ini = strpos($line, '(') + 1;
                    $end = strpos($line, ')', $ini);
                    $this->primary []= substr($line, $ini, $end - $ini);
                }
                continue;
            }
            if (str_starts_with(trim($line), 'CONSTRAINT')) {
                $this->constraints []= (new ConstraintParser($line))->parse();
                continue;
            }
            $this->columns []= (new ColumnParser($line))->parse();
        }
    }
    protected function prefixLine(string $line, int $length, string $character = "\t"): string
    {
        if ($length === 0) {
            return $line;
        }
        return implode('', array_fill(0, $length, $character)) . $line;
    }
}
class DataRepository
{
    public function __construct(protected Incoviba\Common\Define\Connection $connection) {}

    public int $size;

    public function getData(string $table): Generator
    {
        $query = $this->connection->getQueryBuilder()
            ->select()
            ->from($table);
        $results = $this->connection->query($query);
        $this->size = $results->rowCount();
        while ($row = $results->fetch(PDO::FETCH_ASSOC)) {
            yield $row;
        }
    }
}
class ColumnParser
{
    public function __construct(protected string $line) {}

    public function parse(): string
    {
        return "->addColumn('{$this->getName()}', '{$this->getType()}'{$this->getOptions()})";
    }

    protected int $ini;
    protected int $end;
    protected array $parts;
    protected array $options;

    protected function getName(): string
    {
        $ini = $this->getIni();
        $end = $this->getEnd();
        return substr($this->line, $ini, $end - $ini);
    }
    protected function getType(): string
    {
        $parts = $this->getParts();
        $type = array_shift($parts);

        if (str_contains($type, '(')) {
            list($type, $length) = explode('(', $type);
            $this->options []= "'length' => " . rtrim($length, ')');
        }
        return match($type) {
            'int' => 'integer',
            'tinyint' => 'boolean',
            'varchar' => 'string',
            default => $type
        };
    }
    protected function getOptions(): string
    {
        $parts = $this->getParts();
        array_shift($parts);
        $validOptions = [
            'default' => function($parts) {
                $i = array_search('default', array_map(function($part) {return strtolower($part);}, $parts));
                return ['default', $parts[$i + 1]];
            },
            'null' => function($parts) {
                $value = true;
                $i = array_search('null', array_map(function($part) {return strtolower($part);}, $parts));
                if (key_exists($i - 1, $parts) and strtolower($parts[$i - 1]) === 'not') {
                    $value = false;
                }
                return ['null', $value];
            },
            'unsigned' => function($parts) {
                return ['signed', false];
            },
            'auto_increment' => function($parts) {
                return ['auto_increment', true];
            }
        ];
        foreach ($validOptions as $validOption => $callable) {
            if (str_contains(strtolower($this->line), $validOption)) {
                list($option, $value) = $callable($parts);
                if (strtolower($value) === 'null') {
                    $value = 'null';
                }
                if ($value !== 'null' and !is_bool($value) and !ctype_digit($value)) {
                    $value = trim($value, "'");
                    $value = "'{$value}'";
                }
                if (is_bool($value)) {
                    $value = $value ? 'true' : 'false';
                }
                $this->options []= "'{$option}' => {$value}";
            }
        }
        if (count($this->options) === 0) {
            return '';
        }
        return ', [' . implode(', ', $this->options) . ']';
    }
    protected function getIni(): int
    {
        if (!isset($this->ini)) {
            $this->ini = strpos($this->line, '`') + 1;
        }
        return $this->ini;
    }
    protected function getEnd(): int
    {
        if (!isset($this->end)) {
            $this->end = strpos($this->line, '`', $this->getIni());
        }
        return $this->end;
    }
    protected function getParts(): array
    {
        if (!isset($this->parts)) {
            $this->parts = explode(' ', trim(substr($this->line, $this->getEnd() + 1), ' ,'));
        }
        return $this->parts;
    }
}
class ConstraintParser
{
    public function __construct(protected string $line) {}

    public function parse(): string
    {
        return "->addForeignKey({$this->getColumns()}, '{$this->getTable()}', {$this->getReferences()}, ['delete' => 'cascade', 'update' => 'cascade'])";
    }

    protected function getColumns(): string
    {
        $ini = strpos(strtolower($this->line), 'foreign key') + strlen('FOREIGN KEY ');
        $ini = strpos($this->line, '(', $ini) + 1;
        $end = strpos(strtolower($this->line), ' references', $ini + 1) - strlen(')');
        return $this->getNames($ini, $end);
    }
    protected function getTable(): string
    {
        $ini = strpos(strtolower($this->line), 'references') + strlen('REFERENCES ') + 1;
        $end = strpos($this->line, '`', $ini + 1);
        return substr($this->line, $ini, $end - $ini);
    }
    protected function getReferences(): string
    {
        $ini = strpos($this->line, '(', strpos(strtolower($this->line), 'references')) + 1;
        $end = strpos($this->line, ')', $ini);
        return $this->getNames($ini, $end);
    }
    protected function getNames($ini, $end): string
    {
        $names = substr($this->line, $ini, $end - $ini);
        if (!str_contains($names, ',')) {
            return str_replace('`', "'", $names);
        }
        $names = explode(', ', $names);
        $columns = array_map(function($name) {return str_replace('`', "'", $name);}, $names);
        return '[' . implode(', ', $columns) . ']';
    }
}
class MigrationGenerator
{
    public function __construct(protected Config $settings)
    {
        $this->databaseName = $settings->getEnv('DB_DATABASE');
        $this->migrationsPath = $settings->getMigrationsPath();
        $this->startDate = $settings->getStartDate();
        $this->database = new Incoviba\Common\Implement\Database\MySQL($settings->getEnv('DB_HOST'), $settings->getEnv('DB_DATABASE'), $settings->getEnv('DB_USER'), $settings->getEnv('DB_PASSWORD'));
        $this->connection = new Incoviba\Common\Implement\Connection($this->database, new Incoviba\Common\Implement\Database\Query\Builder());
        $this->tableRepository = new TableRepository($this->connection, $this->databaseName);
        $this->logger = $settings->getLogger();
    }

    public function run(bool $execute = true): void
    {
        $this->logger->output('Running generate migrations' . (($execute) ? '' : ' [dry-run]'), 250);
        foreach ($this->tableRepository->getTables() as $tableName) {
            if (in_array($tableName, $this->settings->getSkip())) {
                continue;
            }
            $this->logger->output("Table: {$tableName}");
            $filename = $this->buildFilename($tableName);
            if ($execute) {
                $this->logger->debug("Filename: {$filename}");
            } else {
                $this->logger->output("Filename: {$filename}");
            }
            $content = $this->buildFile($tableName);
            $this->logger->debug("Content: {$content}");

            if ($execute) {
                $status = file_put_contents($filename, $content);
                $this->logger->debug("Saved: " . var_export($status, true));
                try {
                    $this->registerMigration($tableName);
                } catch (PDOException $exception) {
                    $this->logger->warning($exception);
                }
            }
        }
        $this->logger->output("Total tables migrated: " . count($this->tableRepository->getTables()), 250);
    }

    protected Incoviba\Common\Define\Database $database;
    protected Incoviba\Common\Define\Connection $connection;
    protected string $databaseName;
    protected string $migrationsPath;
    protected DateTimeInterface $startDate;
    protected TableRepository $tableRepository;
    protected Logger $logger;

    protected function buildFilename(string $table): string
    {
        $i = $this->tableRepository->getTableIndex($table);
        $time = $this->startDate->add(new DateInterval("PT{$i}S"));
        return implode(DIRECTORY_SEPARATOR, [
            $this->migrationsPath,
            "{$time->format('YmdHis')}_create_{$table}.php"
        ]);
    }
    protected function buildClassName(string $table): string
    {
        return 'Create' . str_replace(' ', '', ucwords(str_replace('_', ' ', $table)));
    }
    protected function buildHeader(): string
    {
        return "<?php

use Phinx\Db\Adapter\MysqlAdapter;
";
    }
    protected function buildClass(string $table): string
    {
        return "class {$this->buildClassName($table)} extends Phinx\Migration\AbstractMigration";
    }
    protected function buildFunction(string $table): string
    {
        $output = ["{", "\tpublic function change(): void", "\t{"];
        $output []= $this->buildInitialSetup();
        $this->tableRepository->getTableDefinition($table);
        $output []= $this->tableRepository->parseTableDefinition($table);
        $output []= $this->buildFinalSetup();
        $output []= "\t}";
        return implode(PHP_EOL, $output);
    }
    protected function buildInitialSetup(): string
    {
        return implode(PHP_EOL, [
            "\t\t\$this->execute('SET unique_checks=0; SET foreign_key_checks=0;');",
            "\t\t\$this->execute(\"ALTER DATABASE CHARACTER SET 'utf8mb4';\");",
            "\t\t\$this->execute(\"ALTER DATABASE COLLATE='utf8mb4_general_ci';\");",
            ''
        ]);
    }
    protected function buildFinalSetup(): string
    {
        return "\t\t\$this->execute('SET unique_checks=1; SET foreign_key_checks=1;');";
    }
    protected function buildFile(string $table): string
    {
        return implode(PHP_EOL, [
            $this->buildHeader(),
            $this->buildClass($table),
            $this->buildFunction($table),
            '}',
            ''
        ]);
    }
    protected function registerMigration(string $table): void
    {
        $i = $this->tableRepository->getTableIndex($table);
        $time = $this->startDate->add(new DateInterval("PT{$i}S"));
        $this->logger->output("Registering migration: {$time->format('Y-m-d H:i:s')}");
        $migrationName = $this->buildClassName($table);

        $query = $this->connection->getQueryBuilder()
            ->insert()
            ->into('phinxlog')
            ->columns(['version', 'migration_name', 'start_time', 'end_time', 'breakpoint'])
            ->values(['?', '?', '?', '?', 0]);

        $this->connection->execute($query, [
            $time->format('YmdHis'),
            $migrationName,
            $time->format('Y-m-d H:i:s'),
            $time->format('Y-m-d H:i:s')
        ]);
    }
}
class SeedGenerator
{
    public function __construct(protected Config $settings)
    {
        $this->databaseName = $settings->getEnv('DB_DATABASE');
        $this->seedsPath = $settings->getSeedsPath();
        $this->startDate = $settings->getStartDate();
        $this->database = new Incoviba\Common\Implement\Database\MySQL($settings->getEnv('DB_HOST'), $settings->getEnv('DB_DATABASE'), $settings->getEnv('DB_USER'), $settings->getEnv('DB_PASSWORD'));
        $this->connection = new Incoviba\Common\Implement\Connection($this->database, new Incoviba\Common\Implement\Database\Query\Builder());
        $this->tableRepository = new TableRepository($this->connection, $this->databaseName);
        $this->dataRepository = new DataRepository($this->connection);
        $this->logger = $settings->getLogger();
    }

    public function run(bool $execute = true): void
    {
        $this->logger->output('Running generate seeds' . (($execute) ? '' : ' [dry-run]'), 250);
        $tables = $this->tableRepository->getTables();
        foreach ($tables as $table) {
            if (in_array($table, $this->settings->getSkip())) {
                continue;
            }
            $this->logger->output("Table: {$table}");
            $filename = $this->buildFilename($table);
            if ($execute) {
                $this->logger->debug("Filename: {$filename}");
            } else {
                $this->logger->output("Filename: {$filename}");
            }
            $content = $this->buildFile($table);
            $this->logger->debug("Content: {$content}");

            if ($execute) {
                $status = file_put_contents($filename, $content);
                $this->logger->debug("Saved: " . var_export($status, true));
            }
        }
        $this->logger->output("Total tables seeded: " . count($this->tableRepository->getTables()), 250);
    }

    protected Incoviba\Common\Define\Database $database;
    protected Incoviba\Common\Define\Connection $connection;
    protected string $databaseName;
    protected string $seedsPath;
    protected DateTimeInterface $startDate;
    protected TableRepository $tableRepository;
    protected DataRepository $dataRepository;
    protected Logger $logger;

    protected function buildFilename(string $table): string
    {
        $i = $this->tableRepository->getTableIndex($table);
        $time = $this->startDate->add(new DateInterval("PT{$i}S"));
        return implode(DIRECTORY_SEPARATOR, [
            $this->seedsPath,
            "{$time->format('YmdHis')}_{$table}_seeder.php"
        ]);
    }
    protected function buildFile(string $table): string
    {
        return implode(PHP_EOL, [
            $this->buildHeader(),
            $this->buildClass($table),
            "{",
            $this->buildFunction($table),
            '}',
            ''
        ]);
    }
    protected function buildHeader(): string
    {
        return "<?php

use Phinx\Seed\AbstractSeed;
";
    }
    protected function buildClass(string $table): string
    {
        return "class {$this->buildClassName($table)} extends AbstractSeed";
    }
    protected function buildClassName(string $table): string
    {
        return str_replace(' ', '', ucwords(str_replace('_', ' ', $table))) . 'Seeder';
    }
    protected function buildFunction(string $table): string
    {
        return implode(PHP_EOL, [
            "\tpublic function run(): void",
            "\t{",
            $this->buildData($table),
            "",
            $this->buildInitialSetup(),
            "\t\t\$this->table('{$table}')",
            "\t\t\t->insert(\$data)",
            "\t\t\t->saveData();",
            $this->buildFinalSetup(),
            "\t}"
        ]);
    }
    protected function buildData(string $table): string
    {
        $output = ["\t\t\$data = ["];
        $dataGenerator = $this->dataRepository->getData($table);
        foreach ($dataGenerator as $row) {
            $output []= "\t\t\t[";
            foreach ($row as $key => $value) {
                if (is_bool($value)) {
                    $value = $value ? 1 : 0;
                }
                if (!ctype_digit("{$value}") and $value !== null) {
                    if (str_contains($value, "'")) {
                        $value = str_replace("'", "\'", $value);
                    }
                    $value = "'{$value}'";
                }
                if ($value === null) {
                    $value = 'null';
                }
                if (strlen($value) > 2 and str_starts_with($value, '0')) {
                    $value = "'{$value}'";
                }
                $output []= "\t\t\t\t'{$key}' => {$value},";
            }
            $output []= "\t\t\t],";
        }
        $output []= "\t\t];";
        $this->logger->debug("Total data: {$this->dataRepository->size}");
        return implode(PHP_EOL, $output);
    }
    protected function buildInitialSetup(): string
    {
        return "\t\t\$this->execute('SET unique_checks=0; SET foreign_key_checks=0;');";
    }
    protected function buildFinalSetup(): string
    {
        return "\t\t\$this->execute('SET unique_checks=1; SET foreign_key_checks=1;');";
    }
}

class Logger
{
    public function __construct(protected bool $verbose = false, protected $quiet = false) {}

    protected array $loggers;

    public function registerLog(Psr\Log\LoggerInterface $logger): Logger
    {
        $this->loggers['log'] = $logger;
        return $this;
    }
    public function registerOutput(Psr\Log\LoggerInterface $logger): Logger
    {
        $this->loggers['output'] = $logger;
        return $this;
    }
    public function log(string $message, int $level = 200): void
    {
        if ($this->canLogLevel($level)) {
            $this->loggers['log']->log($level, $message);
        }
    }
    public function output(string $message, int $level = 200): void
    {
        if ($this->canLogLevel($level)) {
            $this->loggers['output']->log($level, $message);
            $this->log($message, $level);
        }
    }
    public function __call(string $name, array $arguments)
    {
        if (method_exists($this->loggers['log'], $name)) {
            $levelMap = [
                'debug' => 100,
                'info' => 200,
                'notice' => 250,
                'warning' => 300,
                'error' => 400,
                'critical' => 500,
                'alert' => 550,
                'emergency' => 600
            ];
            $level = $levelMap[strtolower($name)];
            $this->log($arguments[0], $level);
        }
    }

    protected function canLogLevel(int $level): bool
    {
        $minQuiet = 300;
        $maxVerbose = 250;

        if ($this->quiet) {
            return $level >= $minQuiet;
        }
        if ($this->verbose) {
            return true;
        }
        return $level >= $maxVerbose;
    }
}
class Config
{
    public function __construct()
    {
        $this->opts = getopt('d:m:s:r:vq', ['date:', 'migrations:', 'seeds:', 'run:', 'verbose', 'quiet']);
    }

    public function getStartDate(): DateTimeInterface
    {
        $dateString = '20141101080000';
        if (isset($this->opts['d']) or isset($this->opts['date'])) {
            $dateString = $this->opts['d'] ?? $this->opts['date'];
        }
        return new DateTimeImmutable($dateString);
    }
    public function getMigrationsPath(): string
    {
        $migrationsPath = implode(DIRECTORY_SEPARATOR, [__DIR__, 'resources', 'database', 'migrations']);
        if (isset($this->opts['m']) or isset($this->opts['migrations'])) {
            $migrationsPath = $this->opts['m'] ?? $this->opts['migrations'];
        }
        return $migrationsPath;
    }
    public function getSeedsPath(): string
    {
        $seedsPath = implode(DIRECTORY_SEPARATOR, [__DIR__, 'resources', 'database', 'seeds']);
        if (isset($this->opts['s']) or isset($this->opts['seeds'])) {
            $seedsPath = $this->opts['s'] ?? $this->opts['seeds'];
        }
        return $seedsPath;
    }
    public function getRun(): int
    {
        $option = 0;
        if (isset($this->opts['r']) or isset($this->opts['run'])) {
            $option = $this->opts['r'] ?? $this->opts['run'];
        }
        if (ctype_digit("{$option}")) {
            return (int) $option;
        }
        return match($option) {
            'm', 'migrations' => 1,
            's', 'seeds' => 2,
            'a', 'all' => 3,
            default => 0
        };
    }
    public function getEnv(string $name): mixed
    {
        return $_ENV[$name];
    }
    public function getLogger(): Logger
    {
        return (new Logger($this->isVerbose(), $this->isQuiet()))
            ->registerLog($this->getPsrLogger())
            ->registerOutput(new Monolog\Logger('output', [
                new Monolog\Handler\FilterHandler(
                    (new Monolog\Handler\StreamHandler(STDOUT))
                        ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, false, true)),
                    Monolog\Level::Info,
                    Monolog\Level::Notice
                )
            ], [
                new Monolog\Processor\ProcessIdProcessor(),
                new Monolog\Processor\MemoryUsageProcessor(),
                new Monolog\Processor\MemoryPeakUsageProcessor(),
                new Monolog\Processor\PsrLogMessageProcessor()
            ]));
    }
    public function getPsrLogger(): Psr\Log\LoggerInterface
    {
        return new Monolog\Logger('migrations',
            [
                (new Monolog\Handler\RotatingFileHandler('/logs/migrations.log'))
                    ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, false, false, true)),
            ], [
                new Monolog\Processor\ProcessIdProcessor(),
                new Monolog\Processor\MemoryUsageProcessor(),
                new Monolog\Processor\MemoryPeakUsageProcessor(),
                new Monolog\Processor\PsrLogMessageProcessor()
            ]
        );
    }
    public function setSkip(string|array $skip): Config
    {
        if (is_string($skip)) {
            $skip = [$skip];
        }
        $this->skip = [...$this->skip ?? [], ...$skip];
        return $this;
    }
    public function getSkip(): array
    {
        return $this->skip;
    }
    public function isVerbose(): bool
    {
        return isset($this->opts['v']) or isset($this->opts['verbose']);
    }
    public function isQuiet(): bool
    {
        return isset($this->opts['q']) or isset($this->opts['quiet']);
    }

    protected array $opts;
    protected array $skip;
}

$config = new Config();
$config->setSkip([
    'monolog',
    'phinxlog',
    'personas',
    'datos_personas',
    'proveedores',
    'datos_proveedores'
]);

Monolog\ErrorHandler::register($config->getPsrLogger());
try {
    $generator = new MigrationGenerator($config);
    $seeder = new SeedGenerator($config);

    switch ($config->getRun()) {
        case 0:
            $generator->run(false);
            $seeder->run(false);
            break;
        case 3:
            $generator->run();
            $seeder->run();
            break;
        case 1:
            $generator->run();
            break;
        case 2:
            $seeder->run();
            break;
    }
} catch (Exception $exception) {
    $config->getLogger()->warning($exception);
} catch (Error $error) {
    $config->getLogger()->error($error);
}
