<?php

/*
 * This file is part of the AuthStack project.
 *
 * (c) Buckhill Ltd <development@buckhill.co.uk>
 *
 * This source file is subject to the Buckhill license that is bundled
 * with this source code in the file LICENSE.
 */

namespace AuthStack\Driver\LDAP;

use AuthStack\Driver\LDAP\User;
//use AuthStack\Driver\Model\SecurityKey;
use AuthStack\TwoFactor\Model\SecurityKey;

use AuthStack\Driver\Errors\NoUserObjectError;

//use AuthStack\Driver\Collection\TwoFactorKeys;
use AuthStack\TwoFactor\Collection\KeyCollection;

use AuthStack\Driver\Driver\AuthDriverInterface;
use AuthStack\Driver\Traits\TraitTemplateVariables;
use Buckhill\LDAP\Entry;
use Buckhill\LDAP\EntryCollection;

class LDAP extends AbstractLDAP implements AuthDriverInterface
{
    use TraitTemplateVariables;

    /**
     * @param array $credentials
     *
     * @return bool
     *
     * @throws \AuthStack\Driver\Errors\ConnectionError
     * @throws \AuthStack\Driver\Errors\InsufficientCredentialsError
     */
    public function authenticate(array $credentials)
    {
        $this->check($credentials);

        $ou         = $this->config('user_auth.ou', '');
        $uid        = $this->config('user_auth.uid');
        $dc         = $this->config('user_auth.dc');
        $password   = trim($credentials['password']);

        //--------------------------------------------------------------------------------------------------------------
        // If OU parameter is NOT an array, then we don't have to loop through all OU's and bind against them
        //--------------------------------------------------------------------------------------------------------------
        if(!is_array($ou))
        {
            $username = $this->getUsername($credentials['username'], $uid, $ou, $dc);

            $result = $this->getConnection()->authenticate($username, $password);

            if ($result && !isset($credentials['test'])) {
                $this->retrieveUser($credentials['username']);
            }

            return $result;
        }

        //--------------------------------------------------------------------------------------------------------------
        // OU is an array, that means there's multiple OU's to bind against
        //--------------------------------------------------------------------------------------------------------------

        $result = false;

        foreach($ou as $org_unit)
        {
            $username = $this->getUsername($credentials['username'], $uid, $org_unit, $dc);

            $result = $this->getConnection()->authenticate($username, $password);

            if($result)
            {
                // If there's 'test' key present, it means function was being tested and user provisioning should not happen
                if(!isset($credentials['test']))
                {
                    $this->retrieveUser($credentials['username'], $org_unit);
                }

                break;
            }
        }

        return $result;
    }


    /**
     * @return bool
     */
    public function elevate()
    {
        $conn   = $this->getLDAPConnection();

        return $conn->authenticate($this->config('connection.username'), $this->config('connection.password'));
    }


    /**
     * @return array
     */
    public static function install()
    {
        return [
            'title'         => 'LDAP',
            'description'   => 'Configurable LDAP driver',
            'class_path'    => 'AuthStack\\Driver\\LDAP\\LDAP'
        ];
    }

    /**
     * @return User
     *
     * @throws NoUserObjectError
     */
    public function getUser()
    {
        if (!$this->user instanceof User) {
            throw new NoUserObjectError('There is no user object available. You must successfully authenticate first.');
        }

        return $this->user;
    }


    /**
     * @param $username
     *
     * @return User
     * @throws NoUserObjectError
     */
    public function getUserByUsername($username)
    {
        $this->retrieveUser($username);

        return $this->getUser();
    }


    /**
     * @param $id
     *
     * @return User
     * @throws NoUserObjectError
     */
    public function getUserByPrimaryKey($id)
    {
        $this->retrieveUser($id);

        return $this->getUser();
    }


    /**
     * @return bool
     */
    public function passwordChangeAllowed()
    {
        return false;
    }


    /**
     * @param array $data
     *
     * @return bool
     */
    public function changePassword(array $data)
    {
        return false;
    }


    /**
     * @param $username
     * @param null $ou
     */
    protected function retrieveUser($username, $ou = null)
    {
        // Retrieve the object which holds user information
        $this->fetchUser($username, $ou);

        // Run the filters for additional user attributes
        $this->runFilters($username);

        // Check if admin, and set admin if yes
        $this->getUser()->checkIfAdmin();

        // Provision to AuthStack's database so we can associate security keys
        // with people from various authsources
        //$this->provision($this->username($username));
        $this->provision($username);

        // Associate the user object with the model that handles provisioned users in AuthStack's database
        $this->getUser()->setProvisionedUser($this->provisioned);

        // AuthStack Id comes from ProvisionedUser model, so we'll set it now
        $this->getUser()->setAuthstackID($this->provisioned->id);

        $this->getUser()->setAuthsourceID($this->getAuthSourceId());

        $keys = KeyCollection::loadForUser($this->getUser()->getProvisionedUser()->id);

        // Set the key collection for the user
        $this->getUser()->setKeyCollection($keys);
    }

    /**
     * @return $this
     */
    protected function runFilters($username)
    {
        $filters = $this->config('filters', []);

        if(empty($filters)) return $this;

        $username = ldap_escape($username, null, LDAP_ESCAPE_FILTER);

        foreach($filters as $record)
        {
            if(isset($record['baseDN']) && isset($record['filter']))
            {
                $base_dn    = $record['baseDN'];
                $attributes = isset($record['attributes']) ? $record['attributes']: [];
                $filter     = $this->getFilterString($record['filter']);

                if(strpos($filter, '{{ uid }}') !== false)
                {
                    $filter = str_replace('{{ uid }}', $username, $filter);
                }

                $this->filter($record['id'], $base_dn, $filter, $attributes);
            }
        }

        return $this;
    }

    /**
     * @param $filter_id
     * @param $base_dn
     * @param $filter
     * @param array $attr
     * @return $this
     */
    protected function filter($filter_id, $base_dn, $filter, array $attr = [])
    {
        $conn = $this->getConnection();

        if(!empty($attr))
        {
            $attributes = $conn->search($base_dn, $filter, $attr);
        }
        else
        {
            $attributes = $conn->search($base_dn, $filter);
        }

        $mapping = $this->config('attributes', []);

        // Fetch mappings that are not related to user entry

        $filtermap = [];

        foreach($mapping as $record)
        {
            if(isset($record['source_id']) && $record['source_id'] == $filter_id)
            {
                $filtermap[] = $record;
            }
        }


        if($attributes instanceof EntryCollection)
        {
            $collection     = $this->remapAttributes($filter_id, $attributes, $filtermap);
            $usercollection = $this->getUser()->getAttributeCollection();

            $arr = $collection->all();

            foreach($arr as $record)
            {
                $usercollection->add($record);
            }
        }
        elseif(is_array($attributes) && sizeof($attributes))
        {
            $attrs = [];

            foreach($attributes as $coll)
            {
                $collection = $this->remapAttributes($filter_id, $coll, $filtermap);

                $attributes = $collection->toArray();

                foreach($attributes as $key => $value)
                {
                    if(isset($attrs[$key]))
                    {
                        if(is_array($attrs[$key]))
                        {
                            $attrs[$key][] = $value;
                        }
                        else
                        {
                            $tmp = $attrs[$key];

                            $attrs[$key] = [];

                            $attrs[$key][] = $tmp;
                            $attrs[$key][] = $value;
                        }
                    }
                    else
                    {
                        $attrs[$key] = $value;
                    }
                }
            }

            $usercollection = $this->getUser()->getAttributeCollection();

            foreach($attrs as $name => $value)
            {
                $entry = new Entry();

                $entry->setName($name);
                $entry->setValue($value);
                $entry->setCount(is_array($value) ? sizeof($value) : 1);

                $usercollection->add($entry);
            }
        }

        return $this;
    }

    /**
     * @param $filter
     * @return string
     */
    protected function getFilterString($filter)
    {
        $vars = $this->getTemplateVariables();

        foreach($vars as $key => $value)
        {
            $filter = str_replace(sprintf("{{ %s }}", $key), $value, $filter);
        }

        return $filter;
    }

    /**
     * @param $filter_id
     * @param EntryCollection $coll
     * @param array $map
     * @return EntryCollection
     */
    protected function remapAttributes($filter_id, EntryCollection $coll, array $map)
    {
        $collection = new EntryCollection;

        $usrcollection = $this->getUser()->getAttributeCollection();

        foreach($map as $record)
        {
            if(isset($record['source_id']) && $record['source_id'] == $filter_id)
            {
                $entry = $coll->get($record['name']);

                if($entry instanceof Entry)
                {
                    if($usrcollection->has($entry->getName()))
                    {
                        $collection->add((new Entry())
                            ->setName($record['renamed'])
                            ->setValue($entry->getValue())
                            ->setCount($entry->count())
                        );
                    }
                }
            }
        }

        return $collection;
    }
}
