<?php

namespace Core\API {

  use Core\API\Routes\GenerateCache;
  use Core\Objects\Context;
  use Core\Objects\DatabaseEntity\Route;
  use Core\Objects\Router\ApiRoute;

  abstract class RoutesAPI extends Request {

    const ROUTER_CACHE_CLASS = "\\Site\\Cache\\RouterCache";

    protected string $routerCachePath;

    public function __construct(Context $context, bool $externalCall, array $params) {
      parent::__construct($context, $externalCall, $params);
      $this->routerCachePath = WEBROOT . DIRECTORY_SEPARATOR . getClassPath(self::ROUTER_CACHE_CLASS);
    }

    protected function toggleRoute(int $id, bool $active): bool {
      $sql = $this->context->getSQL();
      $route = Route::find($sql, $id);
      if ($route === false) {
        return false;
      } else if ($route === null) {
        return $this->createError("Route not found");
      } else if ($route instanceof ApiRoute) {
        return $this->createError("This route cannot be modified");
      }

      $route->setActive($active);
      $this->success = $route->save($sql, ["active"]);
      $this->lastError = $sql->getLastError();
      return $this->success && $this->regenerateCache();
    }

    protected function regenerateCache(): bool {
      $req = new GenerateCache($this->context);
      $this->success = $req->execute();
      $this->lastError = $req->getLastError();
      return $this->success;
    }

    protected function createRoute(string $type, string $pattern, string $target,
                                   ?string $extra, bool $exact, bool $active = true): ?Route {

      $routeClass = Route::ROUTE_TYPES[$type] ?? null;
      if (!$routeClass) {
        $this->createError("Invalid type: $type");
        return null;
      }

      try {
        $routeClass = new \ReflectionClass($routeClass);
        $routeObj = $routeClass->newInstance($pattern, $exact, $target);
        $routeObj->setExtra($extra);
        $routeObj->setActive($active);
        return $routeObj;
      } catch (\ReflectionException $exception) {
        $this->createError("Error instantiating route class: " . $exception->getMessage());
        return null;
      }
    }
  }
}

namespace Core\API\Routes {

  use Core\API\Parameter\Parameter;
  use Core\API\Parameter\StringType;
  use Core\API\RoutesAPI;
  use Core\Objects\Context;
  use Core\Objects\DatabaseEntity\Group;
  use Core\Objects\DatabaseEntity\Route;
  use Core\Objects\Router\ApiRoute;
  use Core\Objects\Router\EmptyRoute;
  use Core\Objects\Router\Router;

  class Fetch extends RoutesAPI {

    public function __construct(Context $context, $externalCall = false) {
      parent::__construct($context, $externalCall, array());
    }

    public function _execute(): bool {
      $sql = $this->context->getSQL();

      $routes = Route::findAll($sql);
      $this->lastError = $sql->getLastError();
      $this->success = ($routes !== FALSE);

      if ($this->success) {
        $this->result["routes"] = $routes;
      }

      return $this->success;
    }

    public static function getDescription(): string {
      return "Allows users to fetch site routing.";
    }

    public static function getDefaultPermittedGroups(): array {
      return [Group::ADMIN];
    }
  }

  class Get extends RoutesAPI {

    public function __construct(Context $context, $externalCall = false) {
      parent::__construct($context, $externalCall, [
        "id" => new Parameter("id", Parameter::TYPE_INT)
      ]);
    }

    public function _execute(): bool {
      $sql = $this->context->getSQL();
      $routeId = $this->getParam("id");

      $route = Route::find($sql, $routeId);
      $this->lastError = $sql->getLastError();
      $this->success = ($route !== FALSE);

      if ($this->success) {
        if ($route === null) {
          return $this->createError("Route not found");
        } else {
          $this->result["route"] = $route;
        }
      }

      return $this->success;
    }

    public static function getDescription(): string {
      return "Allows users to fetch a single route";
    }

    public static function getDefaultPermittedGroups(): array {
      return [Group::ADMIN, Group::MODERATOR];
    }
  }

  class Add extends RoutesAPI {

    public function __construct(Context $context, bool $externalCall = false) {
      parent::__construct($context, $externalCall, array(
        "pattern" => new StringType("pattern", 128),
        "type" => new StringType("type"),
        "target" => new StringType("target", 128),
        "extra" => new StringType("extra", 64, true, ""),
        "exact" => new Parameter("exact", Parameter::TYPE_BOOLEAN),
        "active" => new Parameter("active", Parameter::TYPE_BOOLEAN, true, true),
      ));
    }

    public function _execute(): bool {

      $pattern = $this->getParam("pattern");
      $type = $this->getParam("type");
      $target = $this->getParam("target");
      $extra = $this->getParam("extra");
      $exact = $this->getParam("exact");
      $active = $this->getParam("active");
      $route = $this->createRoute($type, $pattern, $target, $extra, $exact, $active);
      if ($route === null) {
        return false;
      }

      $sql = $this->context->getSQL();
      $this->success = $route->save($sql) !== false;
      $this->lastError = $sql->getLastError();

      if ($this->success) {
        $this->result["routeId"] = $route->getId();
      }

      return $this->success && $this->regenerateCache();
    }

    public static function getDescription(): string {
      return "Allows users to add new routes";
    }

    public static function getDefaultPermittedGroups(): array {
      return [Group::ADMIN];
    }
  }

  class Update extends RoutesAPI {
    public function __construct(Context $context, bool $externalCall = false) {
      parent::__construct($context, $externalCall, array(
        "id" => new Parameter("id", Parameter::TYPE_INT),
        "pattern" => new StringType("pattern", 128),
        "type" => new StringType("type"),
        "target" => new StringType("target", 128),
        "extra" => new StringType("extra", 64, true, ""),
        "exact" => new Parameter("exact", Parameter::TYPE_BOOLEAN),
        "active" => new Parameter("active", Parameter::TYPE_BOOLEAN, true, true),
      ));
    }

    public function _execute(): bool {

      $id = $this->getParam("id");
      $sql = $this->context->getSQL();
      $route = Route::find($sql, $id);
      if ($route === false) {
        return $this->createError("Error fetching route: " . $sql->getLastError());
      } else if ($route === null) {
        return $this->createError("Route not found");
      }

      $target = $this->getParam("target");
      $extra = $this->getParam("extra");
      $type = $this->getParam("type");
      $pattern = $this->getParam("pattern");
      $exact = $this->getParam("exact");
      $active = $this->getParam("active");
      if ($route->getType() !== $type) {
        if (!$route->delete($sql)) {
          return false;
        } else {
          $route = $this->createRoute($type, $pattern, $target, $extra, $exact, $active);
          if ($route === null) {
            return false;
          }
        }
      } else {
        $route->setPattern($pattern);
        $route->setActive($active);
        $route->setExtra($extra);
        $route->setTarget($target);
        $route->setExact($exact);
      }

      $this->success = $route->save($sql) !== false;
      $this->lastError = $sql->getLastError();
      return $this->success && $this->regenerateCache();
    }

    public static function getDescription(): string {
      return "Allows users to update existing routes";
    }

    public static function getDefaultPermittedGroups(): array {
      return [Group::ADMIN];
    }
  }

  class Remove extends RoutesAPI {
    public function __construct(Context $context, bool $externalCall = false) {
      parent::__construct($context, $externalCall, array(
        "id" => new Parameter("id", Parameter::TYPE_INT)
      ));
    }

    public function _execute(): bool {

      $sql = $this->context->getSQL();
      $id = $this->getParam("id");
      $route = Route::find($sql, $id);
      if ($route === false) {
        return $this->createError("Error fetching route: " . $sql->getLastError());
      } else if ($route === null) {
        return $this->createError("Route not found");
      } else if ($route instanceof ApiRoute) {
        return $this->createError("This route cannot be deleted");
      }

      $this->success = $route->delete($sql) !== false;
      $this->lastError = $sql->getLastError();
      return $this->success && $this->regenerateCache();
    }

    public static function getDescription(): string {
      return "Allows users to remove routes";
    }

    public static function getDefaultPermittedGroups(): array {
      return [Group::ADMIN];
    }
  }

  class Enable extends RoutesAPI {
    public function __construct(Context $context, bool $externalCall = false) {
      parent::__construct($context, $externalCall, array(
        "id" => new Parameter("id", Parameter::TYPE_INT)
      ));
    }

    public function _execute(): bool {
      $id = $this->getParam("id");
      return $this->toggleRoute($id, true);
    }

    public static function getDescription(): string {
      return "Allows users to enable a route";
    }

    public static function getDefaultPermittedGroups(): array {
      return [Group::ADMIN];
    }
  }

  class Disable extends RoutesAPI {
    public function __construct(Context $context, bool $externalCall = false) {
      parent::__construct($context, $externalCall, array(
        "id" => new Parameter("id", Parameter::TYPE_INT)
      ));
    }

    public function _execute(): bool {
      $id = $this->getParam("id");
      return $this->toggleRoute($id, false);
    }

    public static function getDescription(): string {
      return "Allows users to disable a route";
    }

    public static function getDefaultPermittedGroups(): array {
      return [Group::ADMIN];
    }
  }

  class GenerateCache extends RoutesAPI {

    private ?Router $router;

    public function __construct(Context $context, bool $externalCall = false) {
      parent::__construct($context, $externalCall, []);
      $this->router = null;
    }

    protected function _execute(): bool {
      $sql = $this->context->getSQL();
      $routes = Route::findBy(Route::createBuilder($sql, false)
        ->whereTrue("active")
        ->orderBy("id")
        ->ascending());

      $this->success = $routes !== false;
      $this->lastError = $sql->getLastError();
      if (!$this->success) {
        return false;
      }

      $this->router = new Router($this->context);
      foreach ($routes as $route) {
        $this->router->addRoute($route);
      }

      $this->success = $this->router->writeCache($this->routerCachePath);
      if (!$this->success) {
        return $this->createError("Error saving router cache file: " . $this->routerCachePath);
      }

      return $this->success;
    }

    public function getRouter(): ?Router {
      return $this->router;
    }

    public static function getDescription(): string {
      return "Allows users to regenerate the routing cache";
    }

    public static function getDefaultPermittedGroups(): array {
      return [Group::ADMIN, Group::SUPPORT];
    }
  }

  class Check extends RoutesAPI {
    public function __construct(Context $context, bool $externalCall) {
      parent::__construct($context, $externalCall, [
        "pattern" => new StringType("pattern", 128),
        "path" => new StringType("path"),
        "exact" => new Parameter("exact", Parameter::TYPE_BOOLEAN, true, true)
      ]);
    }

    protected function _execute(): bool {
      $path = $this->getParam("path");
      $pattern = $this->getParam("pattern");
      $exact = $this->getParam("exact");
      $route = new EmptyRoute($pattern, $exact, "");
      $this->result["match"] = $route->match($path);
      return $this->success;
    }

    public static function getDescription(): string {
      return "This endpoint grants the ability to check, if a route pattern is matched with the given path for debugging purposes";
    }

    public static function getDefaultPermittedGroups(): array {
      return [Group::ADMIN, Group::MODERATOR];
    }
  }
}

