<?php
namespace nur\b\authnz;

use nur\A;
use nur\base;
use nur\config;
use nur\data\types\Metadata;
use nur\txt;

/**
 * Class UserManager: gestionnaire d'utilisateurs et de groupes
 *
 * cette implémentation lit la clé conf.authnz de la configuration
 * - users: liste des utilisateurs
 * - groups: liste des groupes
 * - default_role: rôle principal par défaut
 * - user_perms, user_role, user_groups: associations par utilisateur
 * - group_perms, group_role: associations par groupe
 */
class SimpleUserManager implements IUserManager {
  const USER_SCHEMA = [
    "username" => ["string", null, "nom d'utilisateur"],
    "password" => ["?string", null, "mot de passe"],
    "mail" => ["?string", null, "mail de l'utilisateur"],
    "role" => ["?string", null, "role principal de l'utilisateur"],
    "perms" => ["?array", null, "permissions attribuées à l'utilisateur"],
    "groups" => ["?array", null, "groupes dont l'utilisateur fait partie"],
    "display_name" => ["?string", null, "nom d'affichage long"],
    "short_name" => ["?string", null, "nom d'affichage court"],
    "disabled" => ["bool", false, "ce compte est-il désactivé?"],
  ];
  
  const GROUP_SCHEMA = [
    "groupname" => ["string", null, "identifiant", "required" => true],
    "lib" => ["?string", null, "libellé"],
    "role" => ["?string", null, "rôle par défaut attribué aux membres de ce groupe"],
    "perms" => ["?array", null, "permissions supplémentaires associées à ce groupe"],
  ];

  const OVERRIDES_SCHEMA = [
    "cas_attrs" => ["?array", null, "attributs CAS"],
  ];

  private static $user_md;
  protected static function user_md(): Metadata {
    if (self::$user_md === null) self::$user_md = new Metadata(self::USER_SCHEMA);
    return self::$user_md;
  }
  
  function _getUser(string $username): ?array {
    return config::k("users.$username");
  }

  function getUser(string $username, ?array $overrides, ?bool &$found=null): array {
    $user = $this->_getUser($username);
    $found = $user !== null;
    self::user_md()->ensureSchema($user, $username);
    # support des attributs CAS
    $cas_attrs = A::get($overrides, "cas_attrs");
    if ($cas_attrs !== null) {
      # nom et prénom
      $sn = A::first(explode(",", A::get($cas_attrs, "sn")));
      $givenName = A::get($cas_attrs, "givenname");
      if ($sn !== null && $givenName !== null) {
        $sn = txt::upperw($sn);
        $givenName = txt::upperw($givenName);
        $parts = preg_split('/[- ]+/', $givenName);
        $gn = "";
        foreach ($parts as $part) {
          $gn .= mb_substr($part, 0, 1);
        }
        $gn = txt::upper($gn);

        $displayName = "$givenName $sn";
        $shortName = "$gn.$sn";
        A::replace_n($user, "display_name", $displayName);
        A::replace_n($user, "short_name", $shortName);
      }
      # mail
      A::replace_n($user, "mail", A::get($cas_attrs, "mail"));
    }
    return $user;
  }

  private static $group_md;
  protected static function group_md(): Metadata {
    if (self::$group_md === null) self::$group_md = new Metadata(self::GROUP_SCHEMA);
    return self::$group_md;
  }

  function _getGroup(string $groupname): ?array {
    return config::k("groups.$groupname");
  }

  function getGroup(string $groupname, ?array $overrides, ?bool &$found=null): array {
    $group = $this->_getGroup($groupname);
    $found = $group !== null;
    self::group_md()->ensureSchema($group, $groupname);
    # support des attributs CAS
    $cas_attrs = A::get($overrides, "cas_attrs");
    if ($cas_attrs !== null) {
    }
    return $group;
  }

  protected function getDefaultRole(): ?string {
    return config::k("default_role");
  }

  /** retourner la liste des permissions de l'utilisateur */
  protected function getUserPerms(array $user): array {
    $perms = $user["perms"];
    A::merge($perms, config::k("user_perms.$user[username]"));
    return $perms;
  }
  /** retourner le rôle de l'utilisateur */
  protected function getUserRole(array $user): ?string {
    $role = $user["role"];
    base::update_n($role, config::k("user_role.$user[username]"));
    return $role;
  }
  /** retourner les noms des groupes dont fait partie l'utilisateur */
  protected function getUserGroupnames(array $user): array {
    $groups = $user["groups"];
    A::merge($groups, config::k("user_groups.$user[username]"));
    return $groups;
  }

  /** retourner le rôle par défaut attribué aux membres du groupe */
  protected function getGroupRole(array $group): ?string {
    $role = $group["role"];
    base::update_n($role, config::k("group_role.$group[groupname]"));
    return $role;
  }
  /** retourner les permissions supplémentaires attribuées aux membres du groupe */
  protected function getGroupPerms(array $group): ?array {
    $perms = $group["perms"];
    A::merge($perms, config::k("group_perms.$group[groupname]"));
    return $perms;
  }

  /** retourner les permissions associées au rôle */
  protected function getRolePerms(string $role): ?array {
    return config::k("role_perms.$role");
  }

  protected function mergePerms(array &$userPerms, ?array $perms): void {
    if ($perms === null) return;
    foreach ($perms as $perm) {
      $userPerms[$perm] = true;
    }
  }
  protected function mergeRolePerms(array &$userPerms, ?string $role): void {
    if ($role === null) return;
    $perms = $this->getRolePerms($role);
    if ($perms === null) return;
    foreach ($perms as $perm) {
      $userPerms[$perm] = true;
    }
  }

  function getAuthzUser(string $username, ?array $overrides): ?IAuthzUser {
    $user = $this->getUser($username, $overrides, $founduser);

    $role = null;
    # permissions par défaut, ajoutées uniquement si l'utilisateur a déjà des
    # permissions explicites
    $defaultPerms = ["^$username" => true];
    # tout d'abord calculer les permissions et rôles associées à l'utilisateur
    $perms = array_fill_keys($this->getUserPerms($user), true);
    $userRole = $this->getUserRole($user);
    if ($role === null) $role = $userRole;
    $this->mergeRolePerms($perms, $userRole);
    # puis calculer les permissions et rôles associés aux groupes
    $groupnames = $this->getUserGroupnames($user);
    foreach ($groupnames as $groupname) {
      # permission automatique basée sur le groupe
      A::merge($defaultPerms, ["@$groupname" => true]);

      $group = $this->getGroup($groupname, $overrides, $foundgroup);
      $this->mergePerms($perms, $this->getGroupPerms($group));
      $groupRole = $this->getGroupRole($group);
      if ($role === null) $role = $groupRole;
      $this->mergeRolePerms($perms, $groupRole);
      # rôle automatique du fait de l'appartenance au groupe
      $this->mergeRolePerms($perms, "@$groupname");
    }

    # si un role ou des permissions existent, considérer que l'utilisateur
    # existe, même s'il n'avait pas été trouvé au départ
    if (!$founduser && !$role && !$perms) return null;

    # permissions par défaut
    A::merge($perms, $defaultPerms);
    # rôle par défaut
    if ($role === null) {
      $role = $this->getDefaultRole();
      $this->mergeRolePerms($perms, $role);
    }

    $user["role"] = $role;
    $user["perms"] = array_keys($perms);
    $user["groups"] = $groupnames;
    return new SimpleUser($user);
  }
}
