<?php

namespace EesyPHP;

use EesyPHP\Auth\User;


class Auth {

  /**
   * Initialized methods
   * @var array<string,string>
   */
  private static $methods = array();

  /**
   * Method name used to authenticate current user
   * @var string|null
   */
  private static $logged_method = null;

  /**
   * Initialized backends
   * @var array<string,string>
   */
  private static $backends = array();

  /**
   * Current authenticated user
   * @var \EesyPHP\Auth\User|null
   */
  private static $user = null;

  /**
   * Initialize
   * @return void
   */
  public static function init() {
    // Set config default values
    App :: set_default(
      'auth',
      array(
        'methods' => array(),
        'backends' => array(),
        'enabled' => self :: enabled(),
        'allow_multiple_match' => null,
        'allow_multiple_match_with_valid_password' => null,
      )
    );
    self :: $methods = array();
    foreach(App::get('auth.methods', array(), 'array') as $method) {
      if (!$method || !is_string($method)) {
        Log::warning(
          'Auth Init: Invalid auth method retreive from configuration, ignore it: %s',
          vardump($method));
        continue;
      }
      $class = (
        $method[0] == '\\'?
        $method:
        "\\EesyPHP\\Auth\\".ucfirst($method)
      );
      if (!class_exists($class)) {
        Log::warning(
          "Auth Init: Unknown auth method '%s' retreived from configuration, ignore it",
          $method
        );
        continue;
      }
      $parents = class_parents($class);
      if (!is_array($parents) || !in_array('EesyPHP\\Auth\\Method', $parents)) {
        Log::warning(
          'Auth Init: Auth method %s class (%s) do not derivate from \\EesyPHP\\Auth\\Method '.
          'class, ignore it.',
          $method, $class);
        continue;
      }

      if (!call_user_func(array($class, 'init'))) {
        Log::warning(
          'Auth Init: fail to initialize auth method %s, ignore it',
          $method, $class);
        continue;
      }
      Log::trace('Auth method %s initialized (class %s)', $method, $class);
      self :: $methods[strtolower($method)] = $class;
    }

    self :: $backends = array();
    foreach(App::get('auth.backends', array(), 'array') as $backend) {
      if (!$backend || !is_string($backend)) {
        Log::warning(
          'Auth Init: Invalid auth backend retreive from configuration, ignore it: %s',
          vardump($backend));
        continue;
      }
      $class = (
        $backend[0] == '\\'?
        $backend:
        "\\EesyPHP\\Auth\\".ucfirst($backend)
      );
      if (!class_exists($class)) {
        Log::warning(
          "Auth Init: Unknown auth backend '%s' retreived from configuration, ignore it",
          $backend
        );
        continue;
      }
      $parents = class_parents($class);
      if (!is_array($parents) || !in_array('EesyPHP\\Auth\\Backend', $parents)) {
        Log::warning(
          'Auth Init: Auth backend %s class (%s) do not derivate from \\EesyPHP\\Auth\\Backend '.
          'class, ignore it.',
          $backend, $class);
        continue;
      }

      if (!call_user_func(array($class, 'init'))) {
        Log::warning(
          'Auth Init: fail to initialize auth backend %s, ignore it',
          $backend, $class);
        continue;
      }
      Log::trace('Auth backend %s initialized (class %s)', $backend, $class);
      self :: $backends[strtolower($backend)] = $class;
    }
  }

  /**
   * Check if authentication is enabled
   * @return bool
   */
  public static function enabled() {
    if (!is_null(App::get('auth.enabled', null, 'bool')))
      return App::get('auth.enabled', false, 'bool');
    if (App::get('auth.methods', array(), 'array') && App::get('auth.backends', array(), 'array'))
      return true;
    Log :: trace('Authentication is disabled');
    return false;
  }

  /**
   * Check if a authentification method is enabled
   * @param string $method
   * @return bool
   */
  public static function method_is_enabled($method) {
    return array_key_exists(strtolower($method), self :: $methods);
  }

  /**
   * Get user by username
   * @param string $username
   * @param boolean $first Return only the first matched user (if authorized, optional, default: true)
   * @return \EesyPHP\Auth\User|array<\EesyPHP\Auth\User>|null|false The user object if found, null it not, false in case of error
   */
  public static function get_user($username, $first=true) {
    if (!self :: $backends) {
      Log :: warning("No auth backend registered, can't retreive user");
      return false;
    }
    $users = array();
    foreach (self :: $backends as $backend) {
      $user = call_user_func(array($backend, 'get_user'), $username);
      if ($user) $users[] = $user;
    }
    if (!$users) return null;
    if (count($users) > 1 && !App::get('auth.allow_multiple_match', false, 'bool')) {
      Log :: error('Multiple user found for username "%s":\n%s', $username, vardump($users));
      return false;
    }
    return $first?$users[0]:$users;
  }

  /**
   * Search user by username and check its password
   * @param string $username The username
   * @param string $password The password to check
   * @return \EesyPHP\Auth\User|null|false
   */
  public static function authenticate($username, $password) {
    $users = self :: get_user($username, false);
    if (!$users) return $users === false?false:null;

    $auth_users = array();
    foreach ($users as $user) {
      if ($user->check_password($password))
        $auth_users[] = $user;
    }
    if (!$auth_users) return null;
    if (
      count($auth_users) > 1
      && !App::get(
        'auth.allow_multiple_match_with_valid_password',
        App::get('auth.allow_multiple_match', false, 'bool'),
        'bool'
      )
    ) {
      Log :: error(
        'Multiple user match for username "%s" and provided password:\n%s',
        $username, vardump($auth_users));
      return false;
    }
    return $auth_users[0];
  }

  /**
   * Log user
   * @param bool|string $force Force user authentication: could specified desired method or true
   *                           to use the first one (optional, default: false)
   * @param null|string $with_method Specify with which method(s) login user (optional, default: all methods)
   * @return \EesyPHP\Auth\User|null|false
   */
  public static function login($force=false, $with_method=null) {
    // Check if already logged in
    if (self :: $user)
      return self :: $user;

    // Check if logged in session
    if (isset($_SESSION['user']) && isset($_SESSION['auth_method'])) {
      $user = unserialize($_SESSION['user']);
      if (is_a($user, '\\EesyPHP\\Auth\\User')) {
        self :: $user = $user;
        self :: $logged_method = (
          array_key_exists($_SESSION['auth_method'], self :: $methods)?
          $_SESSION['auth_method']:null
        );
        Log :: debug(
          'User %s authenticated from session (method %s and backend %s)',
          $user->username,
          self :: $logged_method?self :: $logged_method:'unknown',
          $user->backend);
        Hook :: trigger(
          'logged_in_from_session', array('method' => self :: $logged_method, 'user' => $user));
        return $user;
      }
      Log::warning('Invalid user data in session, drop it');
      // Otherwise, drop user in session
      unset($_SESSION['user']);
      unset($_SESSION['auth_method']);
    }

    if (!self :: $methods) {
      Log :: warning("No auth method registered, can't authenticate users");
      return false;
    }

    // Otherwise, log without enforcing by using registered methods
    foreach (self :: $methods as $method => $class) {
      if ($with_method && !in_array($method, array_map('strtolower', ensure_is_array($with_method))))
        continue;
      $user = call_user_func(array($class, 'login'));
      if ($user) {
        self :: set_user($user, $method);
        return $user;
      }
    }

    // If still not logged and force mode enable, force login using specified method (or the first one)
    if ($force) {
      $method = (
        is_string($force) && array_key_exists($force, self :: $methods)?
        $force:key(self :: $methods)
      );
      Log::debug('Force authentication using method %s', $method);
      $user = call_user_func(array(self :: $methods[$method], 'login'), true);
      if ($user) {
        self :: set_user($user, $method);
        return $user;
      }
      return false;
    }
    return null;
  }

  /**
   * Helper to set current authenticated user
   * @param \EesyPHP\Auth\User $user The current authenticated user object
   * @param string|null $method Method used to authenticate the user. If not null, user information
   *                            will be changed in session.
   * @return void
   */
  public static function set_user($user, $method=null) {
    if (!$user instanceof User)
      throw new \Exception(
        "Provided user is not an instance of \\EesyPHP\\Auth\\User: ".vardump($user)
      );
    self :: $user = $user;

    if (!$method) return;

    // Method provided: update user info in session and trigger hook
    Log :: debug(
      'User %s authenticated using method %s and backend %s',
      $user, $method, $user->backend);
    self :: $logged_method = $method;
    $_SESSION['user'] = serialize($user);
    $_SESSION['auth_method'] = $method;
    Hook :: trigger('logged_in', array('method' => $method, 'user' => $user));
  }

  /**
   * Logout
   * @return void
   */
  public static function logout() {
    $user = self :: $user;
    $method = (
      self :: $logged_method?
      self :: $logged_method:
      (isset($_SESSION['auth_method'])?$_SESSION['auth_method']:null)
    );
    self :: $user = null;
    self :: $logged_method = null;
    if (isset($_SESSION['user']))
      unset($_SESSION['user']);
    if (isset($_SESSION['auth_method']))
      unset($_SESSION['auth_method']);
    Hook :: trigger('logout', array('method' => $method, 'user' => $user));
    if ($method)
      call_user_func(array(self :: $methods[$method], 'logout'));
  }

  /**
   * Get current authenticated user
   * @return \EesyPHP\Auth\User|null
   */
  public static function user() {
    return self :: $user;
  }
}
