<?php
namespace nur\b\authnz;

use nur\authz;
use nur\b\IllegalAccessException;
use nur\b\ValueException;
use nur\config;
use nur\cookie;
use nur\F;
use nur\msg;
use nur\P;
use nur\session;
use nur\v\fo;
use nur\v\ly;
use nur\v\page;
use nur\v\v;
use nur\v\vo;
use nur\v\vp\AInitAuthzPage;

/**
 * Interface IAuthzManager: gestionnaire d'authentification et d'autorisation
 */
class AuthzManager {
  #############################################################################
  # Auth

  const COOKIE_KEY_AUTH = "auth:app:";
  /** durée du cookie d'authentification: 17 jours par défaut */
  const COOKIE_DURATION = 17 * 24 * 60;

  const SESSION_KEY_COOKIE = "authz:cookie";
  const SESSION_KEY_STATUS = "authz:status";
  const SESSION_KEY_USERNAME = "authz:username";
  const SESSION_KEY_USER = "authz:user";
  const SESSION_KEY_SULOGIN = "authz:sulogin";

  /** redirection vers la page de login pour une connexion standard */
  const REASON_LOGIN = 1;
  /** redirection vers la page de login pour cause de session expirée */
  const REASON_SESSION = 2;
  /** redirection vers la page de login pour accès interdit */
  const REASON_UNAUTHORIZED = 3;

  /** pas de statut en particulier */
  const STATUS_NONE = 0;
  /** session fraichement démarrée */
  const STATUS_INITIAL = 1;
  /** session déconnecté */
  const STATUS_DISCONNECTED = 2;
  /** accès non autorisé */
  const STATUS_UNAUTHORIZED = 3;

  protected function getCookieKey(): string {
    return self::COOKIE_KEY_AUTH.config::get_appcode();
  }

  protected function getCookieDuration(): int {
    return static::COOKIE_DURATION;
  }

  /** vérifier que le cookie d'authentification est posé et valide. */
  function checkCookie(?string &$username=null, ?string &$authType=null): bool {
    $value = cookie::get($this->getCookieKey(), false);
    if ($value === false) return false;
    if (!preg_match('/^(cas|form|ext|):(.*)$/', $value, $ms)) return false;
    $authType = $ms[1];
    $username = $ms[2];
    return true;
  }

  protected function setCookie(string $username, string $authType): void {
    cookie::setd($this->getCookieKey(), "$authType:$username", $this->getCookieDuration());
  }

  protected function resetCookie(): void {
    cookie::set($this->getCookieKey(), false);
  }

  #############################################################################
  # Session

  /** @var int */
  private $status = self::STATUS_NONE;

  function getStatus(): int {
    return $this->status;
  }

  /**
   * Vérifier que la session est valide.
   *
   * comme raccourci, si la session est valide, initialiser $username avec le
   * nom d'utilisateur *du cookie*
   */
  function checkSession(?string &$username=null, ?string &$authType=null): bool {
    if (!$this->checkCookie($username, $authType)) return false;
    session::start();
    $this->status = session::get(self::SESSION_KEY_STATUS, self::STATUS_NONE);
    return session::get(self::SESSION_KEY_COOKIE) === "$authType:$username";
  }

  function isNewSession(): bool {
    return session::get(self::SESSION_KEY_COOKIE) === null;
  }

  function initSession(int $status): void {
    $this->checkCookie($username, $authType);
    session::start();
    session::set(self::SESSION_KEY_COOKIE, "$authType:$username");
    $this->status = $status;
    session::set(self::SESSION_KEY_STATUS, $status);
    session::set(self::SESSION_KEY_USERNAME, null);
    session::set(self::SESSION_KEY_USER, null);
  }

  function resetSession(int $status=self::STATUS_DISCONNECTED): void {
    if ($this->checkCookie()) {
      session::start();
      session::destroy(true);
      $this->initSession($status);
    } elseif (session::started()) {
      session::destroy(true);
      $this->initSession($status);
    }
  }

  /** @var string */
  private $auth;

  /** tester si l'utilisateur est connecté */
  function _isAuth(): bool {
    if (!session::started() && session::started_once()) {
      # la session a déjà été démarrée, pas besoin de la démarrer de nouveau
    } else if (!$this->checkSession()) {
      return false;
    }
    return session::get(self::SESSION_KEY_USERNAME) !== null;
  }

  /**
   * obtenir le compte de l'utilisateur connecté
   *
   * @throws ValueException si aucun utilisateur n'est connecté
   */
  function _getAuth(): string {
    if (!session::started() && session::started_once()) {
      # la session a déjà été démarrée, pas besoin de la démarrer de nouveau
      $username = session::get(self::SESSION_KEY_USERNAME);
      if ($username !== null) return $username;
    } elseif ($this->checkSession()) {
      $username = session::get(self::SESSION_KEY_USERNAME);
      if ($username !== null) return $username;
    }
    throw new ValueException("not authenticated");
  }

  function isAuth(): bool {
    $auth = $this->auth;
    if ($auth !== null) return true;
    else return $this->_isAuth();
  }

  function getAuth(): string {
    $auth = $this->auth;
    if ($auth !== null) return $auth;
    else return $this->_getAuth();
  }

  #############################################################################
  # Authz

  const USER_MANAGER_CLASS = SimpleUserManager::class;

  /** @var IUserManager */
  private static $user_manager;

  protected function getUserManager(): IUserManager {
    if (self::$user_manager === null) {
      $class = static::USER_MANAGER_CLASS;
      self::$user_manager = new $class();
    }
    return self::$user_manager;
  }

  function redirect(int $reason, string $destUrl, string $loginUrl): void {
    $params = ["d" => $destUrl];
    switch ($reason) {
    case self::REASON_LOGIN:
      $params["a"] = 1; // autologin si possible
      break;
    case self::REASON_SESSION:
      $this->resetSession(self::STATUS_DISCONNECTED);
      break;
    case self::REASON_UNAUTHORIZED:
      session::set(self::SESSION_KEY_STATUS, self::STATUS_UNAUTHORIZED);
      break;
    default:
      throw IllegalAccessException::unexpected_state();
    }
    page::redirect(page::bu($loginUrl, $params));
  }

  function extLogin(string $username): bool {
    # l'utilisateur doit exister
    $user = $this->getUserManager()->getAuthzUser($username, null);
    # ce doit être un utilisateur valide
    if ($user !== null && $user->isValid()) {
      $this->setCookie($username, "ext");
      $this->initSession(self::STATUS_INITIAL);
      session::set(self::SESSION_KEY_USERNAME, $username);
      $this->onAuthOk($username);
      session::set(self::SESSION_KEY_USER, $user);
      $this->onAuthzOk($user);
      return true;
    }
    return false;
  }

  function formLogin(string $username, string $password): bool {
    # l'utilisateur doit exister
    $user = $this->getUserManager()->getAuthzUser($username, null);
    if ($user !== null) {
      # ce doit être un utilisateur valide
      if ($user->isValid()) {
        $this->setCookie($username, "form");
        # le cas échéant le mot de passe doit correspondre
        if (config::is_devel() && !$password) $password = null;
        if ($password === null || $user->validatePassword($password)) {
          # c'est bon
          $this->initSession(self::STATUS_INITIAL);
          session::set(self::SESSION_KEY_USERNAME, $username);
          $this->onAuthOk($username);
          session::set(self::SESSION_KEY_USER, $user);
          $this->onAuthzOk($user);
          return true;
        }
      }
    }
    return false;
  }

  function casLogin(string $username, ?array $overrides): bool {
    $this->setCookie($username, "cas");
    $this->initSession(self::STATUS_INITIAL);
    session::set(self::SESSION_KEY_USERNAME, $username);
    $this->onAuthOk($username);
    # l'utilisateur doit exister
    $user = $this->getUserManager()->getAuthzUser($username, $overrides);
    # ce doit être un utilisteur valide
    if ($user !== null && $user->isValid()) {
      session::set(self::SESSION_KEY_USER, $user);
      $this->onAuthzOk($user);
      return true;
    }
    return false;
  }

  /**
   * après les avoir chargées le cas échéant, retourner les informations
   * d'autorisation de l'utilisateur spécifié ou une instance invalide s'il n'y
   * en a pas. mettre à jour la session le cas échéant
   *
   * prendre par défaut l'utilisateur connecté
   */
  function _selectAuthz(?string $username=null): IAuthzUser {
    # ici, la session a peut-être déjà été fermée, mais si elle a été ouverte au
    # moins une fois, en tenir compte quand on accède aux informations
    if ($username === null) {
      if (!session::started_once() || !$this->isAuth()) return UnknownUser::with();
      $username = $this->getAuth();
      $user = session::get(self::SESSION_KEY_USER);
    } else if (session::started_once() && $this->isAuth()) {
      $user = session::get(self::SESSION_KEY_USER);
    } else {
      $user = null;
    }
    if ($user instanceof IAuthzUser && $user->getUsername() === $username) {
      return $user;
    }
    $user = $this->getUserManager()->getAuthzUser($username, null);
    if ($user === null) $user = UnknownUser::with($username);
    session::set(self::SESSION_KEY_USERNAME, $username);
    session::set(self::SESSION_KEY_USER, $user);
    return $user;
  }

  /** @var IAuthzUser */
  private $authz;

  function setConnected(): void {
    $this->auth = $this->_getAuth();
    $this->authz = $this->_selectAuthz();
  }

  function selectAuthz(?string $username=null): IAuthzUser {
    $authz = null;
    if ($username === null) $authz = $this->authz;
    if ($authz === null) $authz = $this->_selectAuthz($username);
    return $authz;
  }

  /** obtenir les informations d'autorisation de l'utilisateur effectif */
  function getUser(): IAuthzUser {
    #XXX le cas échéant, ajouter les informations de l'utilisateur effectif
    return $this->selectAuthz();
  }

  function checkAuthz(?array $roles, ?array $perms): bool {
    $authz = $this->selectAuthz();
    if ($roles !== null && !$authz->isRole($roles)) return false;
    if ($perms !== null && !$authz->isPerm($perms)) return false;
    # sinon, au moins un rôle ou une permission doivent être définis
    return $authz->isRole(IAuthzUser::ROLE_AUTHZ);
  }

  /** la connexion actuelle a-t-elle été forcée par un administrateur? */
  function isSulogin(): bool {
    return boolval(session::get(self::SESSION_KEY_SULOGIN, false));
  }

  /** marquer la connexion actuelle comme étant faite par un administrateur. */
  function setSulogin(bool $sulogin=true): void {
    if ($sulogin) {
      $user = session::get(self::SESSION_KEY_SULOGIN, false);
      if ($user === false) $user = session::get(self::SESSION_KEY_USER);
      session::set(self::SESSION_KEY_SULOGIN, $user);
    } else {
      session::set(self::SESSION_KEY_SULOGIN, false);
    }
  }

  #############################################################################
  # Méthodes surchargeables

  /** Traiter le cas où l'utilisateur s'est authentifié avec succès. */
  function onAuthOk(string $username): void {
  }

  /** Traiter le cas où l'utilisateur a été autorisé avec succès. */
  function onAuthzOk(IAuthzUser $authz): void {
  }

  #############################################################################
  # Page login

  private $username = null;
  private $authType = null;

  function beforeSetup(AInitAuthzPage $page): void {
    # initialiser la session avant setup. ainsi, dans les fonction beforeSetup(),
    # setup() et afterSetup(), la session est disponible
    $username = P::get("u");
    $password = P::get("p");
    $destPage = F::get("d", $page->getMainUrl());
    $page->_ensureFormLoginAndRedirect($username, $password, $destPage);

    $this->checkSession($this->username, $this->authType);
  }

  function print(AInitAuthzPage $page): void {
    page::no_cache();
    $username = P::get("u");
    $password = P::get("p");

    ly::row();
    vo::h1(["class" => "text-center", q($page->TITLE())]);

    ly::col(["sm" => 6, "sm-push" => 3]);
    $status = $this->getStatus();
    switch ($status) {
    case authz::DISCONNECTED:
      msg::warning("Vous avez été déconnecté. Veuillez vous reconnecter");
      break;
    case authz::UNAUTHORIZED:
      msg::error(["user" => [
        "Connecté en tant que ",
        v::b($this->getAuth()),
        ", vous n'êtes pas autorisé à accéder à la page que vous avez demandé.",
      ]]);
      break;
    }

    ly::panel("Connexion par identifiant/mot de passe");
    fo::start([
      "type" => "basic",
      "action" => "",
      "method" => "post",
    ]);
    fo::text("Identifiant", "u", $username?: $this->username, [
      "accesskey" => "q",
      "placeholder" => "Votre identifiant",
    ]);
    fo::password("Mot de passe", "p", $password, [
      "placeholder" => "Votre mot de passe",
    ]);
    if ($username || $password) {
      msg::error("$username: Votre identifiant et/ou votre mot de passe sont incorrects");
    } elseif ($username === "") {
      msg::error("Vous devez saisir votre identifiant");
    } elseif ($password === "") {
      msg::error("Vous devez saisir votre mot de passe");
    }
    fo::submit(["Connexion", "accesskey" => "r"]);
    if ($this->isAuth() && $this->authType === "form") {
      if ($status != authz::UNAUTHORIZED) {
        msg::warning(["user" => [
          "Connecté en tant que ",
          v::b($this->getAuth()),
          ", vous n'êtes pas autorisé à accéder à cette application.",
        ]]);
      }
      fo::submit([
        "Vous déconnecter", "accesskey" => "z",
        "formmethod" => "get", "formaction" => $page->getLogoutUrl(),
      ]);
    }
    fo::end();

    ly::end();
  }
}
