<?php
namespace nur\ldap;

use IteratorAggregate;
use nulib\output\msg;
use nur\A;
use nur\b\params\Parametrable;
use nur\b\params\Tparametrable;
use nur\b\StopException;
use nur\b\ValueException;
use nur\data\types\md_utils;
use nur\data\types\Metadata;
use nur\iter;

class LdapSearch extends Parametrable implements IteratorAggregate {
  use Tparametrable;

  static function parse_args(?array &$params, ?array $args
    , ?string $searchbase=null, ?string $searchbase_exact=null
    , ?string $scope=null): void {
    $first = true;
    $filter = null;
    $attrs = null;
    foreach ($args as $arg) {
      if ($first) {
        $first = false;
        if (strpos($arg, "=") !== false) $filter = $arg;
        else $attrs[] = $arg;
      } else {
        $attrs[] = $arg;
      }
    }
    if ($filter !== null) $params["filter"] = $filter;
    if ($attrs !== null) $params["attrs"] = $attrs;
    if ($searchbase_exact !== null) {
      $searchbase = $searchbase_exact;
      $params["suffix"] = "";
    }
    if ($searchbase !== null) $params["searchbase"] = $searchbase;
    if ($scope !== null) $params["scope"] = $scope;
  }

  const SCOPE_SUBTREE = 2, SCOPE_ONELEVEL = 1, SCOPE_BASE = 0;

  const PARAMETRABLE_PARAMS_SCHEMA = [
    "filter" => ["?content", "objectClass=*", "filtre de recherche"],
    "attrs" => ["?array", [], "attributs à retourner"],
    "searchbase" => ["?string", null, "DN de base pour la recherche"],
    "scope" => ["?string", "sub", "étendue de la recherche"],
    "suffix" => ["?string", null, "DN de base du serveur"],
    "attributes_only" => ["bool", false, "faut-il ne retourner que les attributs?"],
    "sizelimit" => ["int", -1, "limite de taille"],
    "timelimit" => ["int", -1, "limite de temps"],
    "deref" => ["int", LDAP_DEREF_NEVER, "type de déférencement"],
    "controls" => ["array", [], "contrôles de la recherche"],
  ];

  private static $search_md;

  static function search_md(): Metadata {
    return md_utils::ensure_md(self::$search_md, self::PARAMETRABLE_PARAMS_SCHEMA);
  }

  function __construct($conn, array $params) {
    $this->conn = $conn;
    parent::__construct($params);
  }

  /** @var resource */
  protected $conn;

  /** @var string */
  protected $ppSearchbase;
  
  /** @var string */
  protected $filter;

  function pp_setFilter($filter): void {
    $this->filter = filters::parse($filter);
  }
  
  /** @var array */
  protected $ppAttrs;

  /** retourner la liste des attributs demandés */
  function getAttrs(): array {
    return $this->ppAttrs;
  }
  
  /** @var int */
  protected $scope;

  function pp_setScope(string $scope): void {
    switch ($scope) {
    case self::SCOPE_SUBTREE:
    case "subtree":
    case "sub":
    case "s":
      $this->scope = self::SCOPE_SUBTREE;
      break;
    case self::SCOPE_ONELEVEL:
    case "onelevel":
    case "one":
    case "o":
      $this->scope = self::SCOPE_ONELEVEL;
      break;
    case self::SCOPE_BASE:
    case "base":
    case "b":
      $this->scope = self::SCOPE_BASE;
      break;
    default:
      throw ValueException::invalid_value($scope, "scope");
    }
  }

  /** @var string */
  protected $ppSuffix;

  /** @var bool */
  protected $ppAttributesOnly;

  /** @var int */
  protected $ppSizelimit;

  /** @var int */
  protected $ppTimelimit;

  /** @var int */
  protected $ppDeref;

  /** @var array */
  protected $ppControls;

  /** @throws LdapException */
  function getIterator() {
    $conn = $this->conn;
    $args = [$conn];
    $base = [];
    if ($this->ppSearchbase) $base[] = $this->ppSearchbase;
    if ($this->ppSuffix) $base[] = $this->ppSuffix;
    $args[] = implode(",", $base);
    A::merge($args, [
      $this->filter?: "",
      $this->ppAttrs?: [],
      $this->ppAttributesOnly,
      $this->ppSizelimit,
      $this->ppTimelimit,
      $this->ppDeref,
      $this->ppControls,
    ]);
    msg::debug("Searching searchbase=$args[1] filter=$args[2]");

    $scope = $this->scope;
    if ($scope == self::SCOPE_SUBTREE) $rr = @ldap_search(...$args);
    elseif ($scope == self::SCOPE_ONELEVEL) $rr = @ldap_list(...$args);
    elseif ($scope == self::SCOPE_BASE) $rr = @ldap_read(...$args);
    else throw ValueException::invalid_value($scope, "scope");

    $rr = LdapException::check("search", $conn, $rr, 32);
    if ($rr === false) return; // pas trouvé

    try {
      $er = ldap_first_entry($conn, $rr);
      while ($er !== false) {
        $dn = ldap_get_dn($conn, $er);
        $entry = ldap_get_attributes($conn, $er);
        yield $dn => $entry;
        $er = ldap_next_entry($conn, $er);
      }
    } catch (StopException $e) {
    } finally {
      ldap_free_result($rr);
    }
  }

  /**
   * retourner la première entrée du résultat de la recherche ou null si la
   * recherche ne retourne aucun résultat
   *
   * @throws LdapException
   */
  function first(?string &$dn=null): ?array {
    $it = $this->getIterator();
    $it->rewind();
    if (!$it->valid()) return null;
    try {
      $dn = $it->key();
      return $it->current();
    } finally {
      iter::close($it);
    }
  }

  static function cook(array $initial_names, string $dn, array $entry): array {
    # attributs demandés
    $lkey2names = ["dn" => "dn"];
    foreach ($initial_names as $name) {
      if ($name == "+" || $name == "*") continue;
      $lkey2names[strtolower($name)] = $name;
    }
    # attributs obtenus effectivement
    $count = $entry["count"];
    $attrs = ["dn" => [$dn]];
    for ($i = 0; $i < $count; $i++) {
      $name = $entry[$i];
      $attr = $entry[$name];
      unset($attr["count"]);
      $attrs[$name] = $attr;
      $lkey2names[strtolower($name)] = $name;
    }
    # ensuite, mettre à null les attributs qui n'ont pas été obtenus
    foreach ($lkey2names as $name) {
      if (!array_key_exists($name, $attrs)) {
        $attrs[$name] = null;
      }
    }
    # calculer les clés qui composent le DN
    $dn_names = names::get_dn_names($dn, $lkey2names);

    return [$attrs, $lkey2names, $dn_names];
  }
}
