<?php

namespace apexl\Io\includes;

use apexl\Config\Singleton as Config;
use apexl\Io\enums\HttpMethod;
use apexl\Io\interfaces\IoModuleInterface;
use apexl\Io\services\Database;
use apexl\Io\services\pathUtility;
use apexl\Vault\exceptions\ExecutionException;
use apexl\Vault\Vault;
use Exception;
use Slim\App;

abstract class Module implements IoModuleInterface
{
    protected App $Io;
    protected pathUtility $path;
    protected Config $config;
    protected Routes $route;
    protected Vault $database;

    protected string $baseRoute = '';
    protected array $defaultEntityPatterns;
    protected bool $patternsOverridden = false;

    protected array $entityPatterns = [];
    protected array $entityControllers = [];

    protected ?bool $isInstalled = null;

    public function __construct()
    {
        $this->setBaseRoute(strtolower($this->getName()));
        $this->isInstalled();
    }

    public function setBaseRoute($route): static
    {
        $this->baseRoute = $route;

        return $this;
    }

    public function getName(): string
    {
        return $this->getNameFromNamespace(get_class($this));
    }

    public function getNameFromNamespace($namespace): string
    {
        $moduleSpaces = explode('\\', $namespace);

        return str_replace('Module', '', array_pop($moduleSpaces));
    }

    public function isInstalled($forceRefresh = false)
    {
        if (!$forceRefresh && !is_null($this->isInstalled)) {
            return $this->isInstalled;
        }

        if (!$this->requiresInstall()) {
            $this->isInstalled = true;

            return $this->isInstalled;
        }

        //system is installing? Then there's no DB to connect to, so this one isn't installed.
        if (System::$installing) {
            $this->isInstalled = false;

            return $this->isInstalled;
        }
        try {
            //we might not have a DB to connect to, so we need to make sure we catch that.
            $module = Database::getModule(sprintf('\\%s', $this::class));
            if (isset($module['installed'])) {
                $this->isInstalled = $module['installed'];

                return $this->isInstalled;
            }
        } catch (Exception) {
        }
        $this->isInstalled = false;

        return $this->isInstalled;
    }

    public function requiresInstall(): bool
    {
        return !empty($this->schema());
    }

    /**
     * @return mixed
     */
    public function schema(): object
    {
        return (object) [];
    }

    public function initialise(Config $config, Vault $database)
    {
        $this->Io = System::$app;
        $this->config = $config;
        $this->database = $database;
        $this->route = Routes::getInstance();
        $this->path = pathUtility::getInstance();
    }

    /**
     * Method called on instantiation to add available routes to SlimRouter.
     */
    public function routes(): void
    {
    }

    public function install(): bool
    {
        return !$this->requiresInstall();
    }

    public function uninstall(): void
    {
    }

    /**
     * @throws ExecutionException
     */
    public function markInstalled($namespace): void
    {
        //update the installation status.
        $vault = Vault::getInstance();

        if ($this->checkExistsInDB($namespace)) {
            $vault->update('modules')
                ->fields(['installed' => 1])
                ->where('namespace', sprintf('\\%s', $namespace))
                ->execute();

            return;
        }
        $vault->insert('modules')->fields(
            [
                'name' => $this->getNameFromNamespace($namespace),
                'namespace' => sprintf('\\%s', $namespace),
                'weight' => 1,
                'installed' => 1,
                'lastInstallDate' => time(),
            ]
        )->execute();

    }

    protected function checkExistsInDB($namespace): bool
    {
        //we need to check if we know about this module. If we don't then this is an insert.
        //update the installation status.
        $module = Database::getModule('\\'.$namespace);

        return !empty($module);
    }

    public function apiVersion(): int
    {
        return VERSION;
    }

    /**
     * Declare intention to build routes for this entity. Will take defaults if not previously defined.
     */
    protected function addEntityRoutes(Entity $entity, ?array $callables = null, array $patterns = []): static
    {
        $this->setEntityPatterns($entity, $patterns);

        foreach ($this->entityPatterns[$entity->getEntityName()] as $verb => $pattern) {
            $this->setEntityCallable($entity, $verb, ($callables[$verb] ?? Controller::class.':'.$verb));
        }

        $this->buildEntityRoutes($entity);

        foreach ($this->entityPatterns[$entity->getEntityName()] as $verb => $pattern) {
            // Pass the instance of the entity to the route so we don't have to reload it.
            $this->route::addRouteArg(
                sprintf('%s.%s', $entity->getEntityName(), strtolower((string) $verb)),
                'entity',
                $entity::class
            );
        }

        return $this;
    }

    protected function setEntityPatterns(Entity $entity, array $patterns = []): static
    {
        $this->defaultEntityPatterns = [
            'get' => '[/{'.$entity->primaryKey.'}]',
            'put' => '[/{'.$entity->primaryKey.'}]',
            'post' => '', //no pattern here as we should POST to the base entity path (by default)
            'delete' => '[/{'.$entity->primaryKey.'}]',
        ];
        $this->patternsOverridden = $patterns !== [];
        $this->entityPatterns[$entity->getEntityName()] = array_merge($this->defaultEntityPatterns, $patterns);

        return $this;
    }

    /**
     * Allows us to set the callables for each entity declared
     */
    protected function setEntityCallable(Entity $entity, string $verb, string $callable): static
    {
        $this->entityControllers[$entity->getEntityName()][$verb] = $callable;

        return $this;
    }

    /**
     * Normal structure is /ModuleName/EntityName/{pattern}
     * Build any default entity routes.
     */
    protected function buildEntityRoutes(Entity $entity, bool $rootRoutes = false): void
    {
        $method = $rootRoutes ? 'rootRoute' : 'addRoute';
        foreach ($this->entityPatterns[$entity->getEntityName()] as $verb => $pattern) {
            $this->$method(
                $verb,
                sprintf('%s.%s', $entity->getEntityName(), strtolower((string) $verb)),
                sprintf(
                    '%s%s',
                    str_replace(
                        'entity',
                        '',
                        sprintf(
                            'entities/%s',
                            strtolower($entity->getEntityName())
                        )
                    ),
                    $pattern
                ),
                $this->entityControllers[$entity->getEntityName()][$verb]
            );
        }
    }

    protected function rootRoute(string $verb, string $name, string $pattern, mixed $callable): static
    {
        $method = HttpMethod::from(strtoupper($verb));

        $this->route::addRoute(
            $method,
            $name,
            sprintf(
                '/%s',
                ltrim($pattern, '/')
            ),
            $callable
        );

        $this->addModuleNameToRoute($name);

        return $this;
    }

    protected function addRoute($verb, string $name, string $pattern, mixed $callable): static
    {
        $method = HttpMethod::from(strtoupper($verb));

        $this->route::addRoute(
            $method,
            $name,
            sprintf(
                '%s/%s',
                rtrim($this->baseRoute, '/'),
                ltrim($pattern, '/')
            ),
            $callable
        );

        $this->addModuleNameToRoute($name);

        return $this;
    }

    protected function addModuleNameToRoute($name): Module
    {
        //add the declared module name as a route arg for use later.
        Routes::addRouteArg($name, 'moduleName', strtolower($this->getName()));

        return $this;
    }
}
