<?php

namespace Ltb\Directory;
use \DateTime;
use \DateInterval;

class OpenLDAP implements \Ltb\Directory
{

    private $operationalAttributes = array(
                                           'pwdaccountlockedtime',
                                           'pwdaccountdisabled',
                                           'pwdchangedtime',
                                           'pwdstarttime',
                                           'pwdendtime',
                                           'pwdpolicysubentry'
                                          );

    public function getOperationalAttributes() : array {
        return $this->operationalAttributes;
    }

    public function isLocked($entry, $pwdPolicyConfiguration) : bool {

        if (!$pwdPolicyConfiguration['lockout_enabled']) {
            return false;
        }

        # Get pwdAccountLockedTime
        $pwdAccountLockedTime = $entry['pwdaccountlockedtime'][0] ?? null;

        if (!$pwdAccountLockedTime) {
            return false;
        }

        if ( $pwdAccountLockedTime === "000001010000Z" ) {
            return true;
        }

        if ( $this->getLockDate($entry, $pwdPolicyConfiguration)->getTimestamp() > time() ) {
            return false;
        }

        $unlockDate = $this->getUnlockDate($entry, $pwdPolicyConfiguration);

        if (!$unlockDate) {
            return true;
        }

        if ( $unlockDate and time() <= $unlockDate->getTimestamp() ) {
            return true;
        }

        return false;
    }

    public function getLockDate($entry, $pwdPolicyConfiguration) : ?DateTime {

        $lockDate = NULL;

        # Get pwdAccountLockedTime
        $pwdAccountLockedTime = $entry['pwdaccountlockedtime'][0] ?? null;

        if (!$pwdAccountLockedTime or $pwdAccountLockedTime === "000001010000Z") {
            return $lockDate;
        }

        $lockDate = \Ltb\Date::ldapDate2phpDate($pwdAccountLockedTime);
        return $lockDate;
    }

    public function getUnlockDate($entry, $pwdPolicyConfiguration) : ?DateTime {

        $unlockDate = NULL;

        # Get lock date
        $lockDate = $this->getLockDate($entry, $pwdPolicyConfiguration);

        if (!$lockDate) {
            return $unlockDate;
        }

        # Get lockout duration
        $lockoutDuration = $pwdPolicyConfiguration["lockout_duration"];

        if (isset($lockoutDuration) and ($lockoutDuration > 0)) {
            $unlockDate = date_add( $lockDate, new DateInterval('PT'.$lockoutDuration.'S'));
        }

        return $unlockDate;
    }

    public function lockAccount($ldap, $dn) : bool {

        $modification = \Ltb\PhpLdap::ldap_mod_replace($ldap, $dn, array("pwdAccountLockedTime" => array("000001010000Z")));
        $errno = ldap_errno($ldap);

        if ( $errno ) {
            error_log("LDAP - Lock account error $errno  (".ldap_error($ldap).")");
            return false;
        } else {
            return true;
        }
    }

    public function unlockAccount($ldap, $dn) : bool {

        $modification = \Ltb\PhpLdap::ldap_mod_replace($ldap, $dn, array("pwdAccountLockedTime" => array()));
        $errno = ldap_errno($ldap);

        if ( $errno ) {
            error_log("LDAP - Unlock account error $errno  (".ldap_error($ldap).")");
            return false;
        } else {
            return true;
        }
    }

    public function isPasswordExpired($entry, $pwdPolicyConfiguration) : bool {

        # Get pwdChangedTime
        $pwdChangedTime = $entry['pwdchangedtime'][0] ?? null;

        if (!$pwdChangedTime) {
            return false;
        }

        # Get password expiration date
        $expirationDate = $this->getPasswordExpirationDate($entry, $pwdPolicyConfiguration);

        if (!$expirationDate) {
            return false;
        }

        if ($expirationDate and time() >= $expirationDate->getTimestamp()) {
            return true;
        }

        return false;
    }

    public function getPasswordExpirationDate($entry, $pwdPolicyConfiguration) : ?DateTime {

        $expirationDate = NULL;

        # Get pwdChangedTime
        $pwdChangedTime = $entry['pwdchangedtime'][0] ?? null;

        if (!$pwdChangedTime) {
            return $expirationDate;
        }

        # Get pwdMaxAge
        $pwdMaxAge = $pwdPolicyConfiguration["password_max_age"];

        # Compute expiration date
        if ($pwdMaxAge) {
            $changedDate = \Ltb\Date::ldapDate2phpDate($pwdChangedTime);
            $expirationDate = date_add( $changedDate, new DateInterval('PT'.$pwdMaxAge.'S'));
        }

        return $expirationDate;
    }

    public function modifyPassword($ldap, $dn, $password, $forceReset) : bool {

        $changes = array('userPassword' => $password);

        if ($forceReset) {
            $changes['pwdReset'] = 'TRUE';
        }

        $update = \Ltb\PhpLDAP::ldap_mod_replace($ldap, $dn, $changes);
        $errno = ldap_errno($ldap);

        if ($errno) {
            error_log("LDAP - Modify password error $errno  (".ldap_error($ldap).")");
            return false;
        } else {
            return true;
        }
    }

    public function resetAtNextConnection($ldap, $dn) : bool {

        # Get entry
        $search = \Ltb\PhpLDAP::ldap_read($ldap, $dn, "(objectClass=*)", array('pwdreset'));
        $errno = \Ltb\PhpLDAP::ldap_errno($ldap);

        if ( $errno ) {
            error_log("LDAP - Search error $errno  (".ldap_error($ldap).")");
            return false;
        } else {
            $entry = \Ltb\PhpLDAP::ldap_get_entries($ldap, $search);
        }

        $pwdreset = $entry[0]['pwdreset'][0] ?? null;
        if( isset($pwdreset) and $pwdreset === "TRUE" ) {
            return true;
        } else {
            return false;
        }
    }

    public function enableAccount($ldap, $dn) : bool {

        $attrsToDelete = array( 'pwdAccountDisabled' => array() );

        $update = \Ltb\PhpLDAP::ldap_mod_replace($ldap, $dn, $attrsToDelete);
        $errno = \Ltb\PhpLDAP::ldap_errno($ldap);

        if ($errno) {
            error_log("LDAP - Enabling account error $errno  (".\Ltb\PhpLDAP::ldap_error($ldap).")");
            return false;
        } else {
            return true;
        }
    }

    public function disableAccount($ldap, $dn) : bool {

        # Date of disabling
        $currentDate = gmdate("YmdHis")."Z";

        $attrs = array( 'pwdAccountDisabled' => array($currentDate) );

        $update = \Ltb\PhpLDAP::ldap_mod_replace($ldap, $dn, $attrs);
        $errno = \Ltb\PhpLDAP::ldap_errno($ldap);

        if ($errno) {
            error_log("LDAP - Disabling account error $errno  (".\Ltb\PhpLDAP::ldap_error($ldap).")");
            return false;
        } else {
            return true;
        }
    }

    public function isAccountEnabled($entry) : bool {

        if (empty($entry['pwdaccountdisabled'][0])) {
            return true;
        } else {
            return false;
        }

    }

    public function getLdapDate($date) : string {
        return \Ltb\Date::string2ldapDate( $date->format('d/m/Y') );
    }

    public function getPhpDate($date) : ?DateTime {
        return \Ltb\Date::ldapDate2phpDate( $date );
    }

    # Function that parses all entries and returns ppolicies and user's ppolicies
    public function getPwdPolicies($ldap, $entries, $default_ppolicy_dn) : array {

        $passwordPolicies = array(); # list of unique password policies
        $userPolicies = array();     # associative array: user => associated ppolicy
        # parse entries
        foreach($entries as $entry_key => $entry)
        {

            $currentPolicyDN = "";
            # If user entry has a defined password policy
            if(isset($entry['pwdpolicysubentry'][0]))
            {
                $currentPolicyDN = $entry['pwdpolicysubentry'][0];
            }
            else
            {
                $currentPolicyDN = $default_ppolicy_dn;
            }

            # Check if the password policy has already been fetched
            $found = false;
            for( $i=0 ; $i < count($passwordPolicies) ; $i++)
            {
                if( isset($passwordPolicies[$i]['dn']) &&
                    $passwordPolicies[$i]['dn'] == $currentPolicyDN )
                {
                    # Point to the reference of existing ppolicy
                    $userPolicies[$entry['dn']] = &$passwordPolicies[$i];
                    $found = true;
                    break;
                }
            }

            # If policy has not been fetched
            if(!$found)
            {
                # Get password policy from LDAP and add it to the list of unique ppolicies
                $passwordPolicies[] = $this->getPwdPolicyConfiguration(
                                          $ldap,
                                          $entry['dn'],
                                          $default_ppolicy_dn
                                      );
                # Assign current user to corresponding password policy
                $userPolicies[$entry['dn']] =
                    &$passwordPolicies[(sizeof($passwordPolicies)-1)];

            }

        }
        # return the list of unique password policies + the mapping user => password policy
        return array( $passwordPolicies, $userPolicies);
    }

    public function getPwdPolicyConfiguration($ldap, $entry_dn, $default_ppolicy_dn) : Array {

        $ppolicyConfig = array();

        # Check pwdPolicySubEntry
        $search_user = \Ltb\PhpLDAP::ldap_read($ldap, $entry_dn, "(objectClass=*)", array('pwdpolicysubentry'));
        $errno = \Ltb\PhpLDAP::ldap_errno($ldap);

        if ( $errno ) {
            error_log("LDAP - Search error $errno  (".ldap_error($ldap).")");
            return $ppolicyConfig;
        } else {
            $user_entry = \Ltb\PhpLDAP::ldap_get_entries($ldap, $search_user);
        }

        if(isset($user_entry[0]['pwdpolicysubentry'][0]) && !empty($user_entry[0]['pwdpolicysubentry'][0]))
        {
            $ppolicy_dn = $user_entry[0]['pwdpolicysubentry'][0];
        }
        else
        {
            $ppolicy_dn = $default_ppolicy_dn;
        }

        # Get values from ppolicy
        $search = \Ltb\PhpLDAP::ldap_read($ldap, $ppolicy_dn, "(objectClass=pwdPolicy)", array('pwdLockoutDuration', 'pwdMaxAge', 'pwdLockout'));
        $errno = \Ltb\PhpLDAP::ldap_errno($ldap);

        if ( $errno ) {
            error_log("LDAP - Search error $errno  (".ldap_error($ldap).")");
            return $ppolicyConfig;
        } else {
            $entry = \Ltb\PhpLDAP::ldap_get_entries($ldap, $search);
        }

        $ppolicyConfig["dn"] = $entry[0]["dn"];
        $ppolicyConfig["lockout_duration"] = $entry[0]["pwdlockoutduration"][0] ?? 0;
        $ppolicyConfig["password_max_age"] = $entry[0]["pwdmaxage"][0] ?? 0;
        $pwdlockout = $entry[0]['pwdlockout'][0] ?? false;
        $ppolicyConfig["lockout_enabled"] = strtolower($pwdlockout) == "true" ? true : false;

        return $ppolicyConfig;
    }

    public function getDnAttribute() : string {
        return "entryDn";
    }

    public function isAccountValid($entry, $pwdPolicyConfiguration) : bool {

        $time = time();
        $startdate = $this->getStartDate($entry, $pwdPolicyConfiguration);
        $enddate = $this->getEndDate($entry, $pwdPolicyConfiguration);

        if ( isset($startdate) ) {
            if ( $time <= $startdate->getTimestamp() ) {
                return false;
            }
        }

        if ( isset($enddate) ) {
            if ( $time >= $enddate->getTimestamp() ) {
                return false;
            }
        }

        return true;
    }

    public function getStartDate($entry, $pwdPolicyConfiguration) : ?DateTime {

        $startdate = null;

        if ( isset($entry['pwdstarttime']) ) {
            $startdate = \Ltb\Date::ldapDate2phpDate($entry['pwdstarttime'][0]);
        }

        return $startdate ? $startdate : null;
    }

    public function getEndDate($entry, $pwdPolicyConfiguration) : ?DateTime {

        $enddate = null;

        if ( isset($entry['pwdendtime']) ) {
            $enddate = \Ltb\Date::ldapDate2phpDate($entry['pwdendtime'][0]);
        }

        return $enddate ? $enddate : null;
    }

    public function computePassword($ldapInstance, $dn, $password, $hash, $hash_options, $use_exop_passwd) : string {

        # If using the password extended operation, compute the password
        if (!$use_exop_passwd) {
            if ($hash === "auto") {
                $old_password_hashed = $ldapInstance->get_password_value($dn, "userPassword");
                $hash = \Ltb\Password::get_hash_type($old_password_hashed);
            }
            $password = \Ltb\Password::make_password($password, $hash, $hash_options);
        }
        # If using the password extended operation, just send the cleartext password

        return $password;
    }

    public function changePasswordData($ldapInstance, $dn, $userdata, $password, $oldpassword, $who_change_password, $use_exop_passwd, $use_ppolicy_control, $custom_pwd_field_mode, $custom_pwd_attribute, $ldap_options) : array {

        # TODO: $ldap_options are currently not implemented

        list($error_code, $error_msg, $ppolicy_error_code) = array(null, null, null);

        if ( $custom_pwd_field_mode ) {
            $pwd_attribute = $custom_pwd_attribute;
        } else {
            $pwd_attribute = "userPassword";
        }

        if ($use_exop_passwd) {
            list($error_code, $error_msg, $ppolicy_error_code) = $ldapInstance->change_password_with_exop($dn, $oldpassword, $password, $use_ppolicy_control);
            if( $error_code == 0 and !empty($userdata) )
            {
                list($error_code, $error_msg) = $ldapInstance->modify_attributes($dn, $userdata);
            }
        } else {
            # Else just replace with new password
            $userdata[$pwd_attribute] = $password;

            if ( $use_ppolicy_control ) {
                list($error_code, $error_msg, $ppolicy_error_code) = $ldapInstance->modify_attributes_using_ppolicy($dn, $userdata);
            } else {
                list($error_code, $error_msg) = $ldapInstance->modify_attributes($dn, $userdata);
            }
        }

        return array( $error_code, $error_msg, $ppolicy_error_code );
    }

    public function getAccountStatus($ldap, $errno, $extended_error = null) : array
    {
        # TODO: getAccountStatus is currently not implemented for OpenLDAP

        $accountStatus = array();

        return $accountStatus;
    }

}
