<?php

namespace AuthStack\Driver\LDAP;

use AuthStack\Driver\Errors\NoUserObjectError;
use AuthStack\Driver\Traits\TraitTemplateVariables;
use AuthStack\TwoFactor\Collection\KeyCollection;
use Buckhill\LDAP\Client;
use Buckhill\LDAP\Entry;
use Buckhill\LDAP\EntryCollection;

class LDAP extends AbstractDriver implements \AuthStack\Driver\Driver\AuthDriverInterface
{
    use TraitTemplateVariables;

    protected $connection;
    protected $username;
    protected $user;

    /**
     * Reads driver.json found in the same directory and returns it as an array
     * You can re-implement this method to return an array which conforms to JSON schema
     * JSON schema information: http://json-schema.org/
     *
     * @return mixed
     */
    public static function install()
    {
        return [];
    }

    public function authenticate(array $data)
    {
        $this->check($data);

        $dn = $this->getDN($data['username']);

        //--------------------------------------------------------------------------------------------------------------
        // We didn't get the distinguished name, simply return false. It's rendered as "Invalid Credentials" at frontend
        //--------------------------------------------------------------------------------------------------------------
        if(!$dn) return false;

        $authenticated = $this->getConnection()->authenticate($dn, $data['password']);

        if($authenticated && !isset($data['test']))
        {
            $this->retrieveFromLDAP($data['username']);
        }

        return $authenticated;
    }

    public function passwordChangeAllowed()
    {
        // Indicates whether this driver allows password change through Authstack UI or not
        // Expects a boolean return. If false is returned, AuthStack won't render password change menu

        return false;
    }


    public function changePassword(array $data)
    {
        // This method handles password change, if it's allowed.
        // Password is changed through AuthStack UI, when user is authenticated via this driver
        // If you don't want to implement password change, simply return true from this method

        return true;
    }

    /**
     * @return User
     * @throws NoUserObjectError
     */
    public function getUser()
    {
        // This method must return an instance of User, the class created in the same directory / namespace as this one
        // When user is authenticated, User object must be available since it deals with attributes and user provisioning

        if(!$this->user instanceof User)
        {
            throw new NoUserObjectError('No user object available.');
        }

        return $this->user;
    }

    /**
     * @param $id
     * @return bool
     */
    public function getUserByPrimaryKey($id)
    {
        return $this->getUserByUsername($id);
    }

    /**
     * @param $username
     * @return bool
     */
    public function getUserByUsername($username)
    {
        $dn = $this->getDN($username);

        //--------------------------------------------------------------------------------------------------------------
        // We didn't get the distinguished name, simply return false. It's rendered as "Invalid Credentials" at frontend
        //--------------------------------------------------------------------------------------------------------------
        if(!$dn) return false;

        return $this->retrieveFromLDAP($username);
    }

    /**
     * @param $username
     * @return bool
     */
    protected function retrieveFromLDAP($username)
    {
        //--------------------------------------------------------------------------------------------------------------
        // Read the record from LDAP
        //--------------------------------------------------------------------------------------------------------------
        $this->readFromLDAP();


        //--------------------------------------------------------------------------------------------------------------
        // Run filters and collect attributes
        //--------------------------------------------------------------------------------------------------------------
        $this->runFilters($username);

        //--------------------------------------------------------------------------------------------------------------
        // Check if user is admin
        //--------------------------------------------------------------------------------------------------------------
        $this->getUser()->checkIfAdmin();

        //--------------------------------------------------------------------------------------------------------------
        // Provision user to Authstack's database. If the user is already there, model is returned with info from MySQL
        //--------------------------------------------------------------------------------------------------------------
        $this->provision($username);

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

        //--------------------------------------------------------------------------------------------------------------
        // AuthStack ID comes from ProvisionedUser model, set it correctly
        //--------------------------------------------------------------------------------------------------------------
        $this->getUser()->setAuthstackID($this->getProvisionedUser()->getAttribute('id'));

        //--------------------------------------------------------------------------------------------------------------
        // Set correct auth source ID
        //--------------------------------------------------------------------------------------------------------------
        $this->getUser()->setAuthsourceID($this->getAuthSourceId());

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

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

        return true;
    }

    /**
     * @return bool
     */
    protected function readFromLDAP()
    {
        $collection = $this->elevated(function()
        {
            /**
             * This is the collection of attributes that we *want* to fetch from LDAP
             */
            $collection = Attributes::fromArray($this->config('attributes'));

            /**
             * These are the attributes that are saved in user directory entry (no filters applied yet)
             */
            $attributes = $collection->getDirectoryAttributes();

            $rdn        = $this->getUserRDN();
            $filter     = $this->getUserFilter();

            $connection = $this->getConnection();

            /**
             * Read the attributes from LDAP that are specified in $attributes array (that's the one that user ticked
             * while setting the connection up
             */
            $ldap_attribute_collection = new EntryCollection();

            try
            {
                $result = $connection->search($rdn, $filter, $attributes, 0, 1);

                $collection->rename($result->toArray(), $ldap_attribute_collection);
            }
            catch(\Exception $e)
            {
                $this->last_error = $e->getMessage();

                return false;
            }

            return $ldap_attribute_collection;
        });

        if(false === $collection)
        {
            return false;
        }

        return $this->initializeUser($collection, $this->cfg_key);
    }

    /**
     * @param EntryCollection $collection
     * @param $cfg_key
     * @return bool
     */
    protected function initializeUser(EntryCollection $collection, $cfg_key)
    {
        $this->user = User::initialize($collection, $cfg_key);

        return true;
    }

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

        //--------------------------------------------------------------------------------------------------------------
        // No filters - return
        //--------------------------------------------------------------------------------------------------------------
        if(empty($filters))
        {
            return $this;
        }

        //--------------------------------------------------------------------------------------------------------------
        // Attributes, in the connection setup, that are not present in user but filter entry
        //--------------------------------------------------------------------------------------------------------------
        foreach($filters as $data)
        {
            // Create the filter
            $filter = Filter::fromArray($data);

            // Retrieve result
            // Result can be an array or EntryCollection. If empty array, filter did nothing
            $result = $filter->run($this->getConnection(), $username, $this->getTemplateVariables());
            
            if(!empty($result) || ($result instanceof EntryCollection))
            {
                // We got a result back, populate user attribute collection
                $this->populateFromFilter($filter, $result);
            }
        }
    }

    /**
     * @param Filter $filter
     * @param $result
     */
    protected function populateFromFilter(Filter $filter, $result)
    {
        if(is_array($result))
        {
            foreach($result as $collection)
            {
                if($collection instanceof EntryCollection)
                {
                    $this->addToUserCollection($filter, $collection);
                }
            }
        }
        elseif($result instanceof EntryCollection)
        {
            $this->addToUserCollection($filter, $result);
        }
    }

    /**
     * @param Filter $filter
     * @param EntryCollection $collection
     * @return bool
     */
    protected function addToUserCollection(Filter $filter, EntryCollection $collection)
    {
        $attributes = Attributes::fromArray($this->config('attributes'));

        $map = $attributes->getFilterAttributes($filter->getID());

        //--------------------------------------------------------------------------------------------------------------
        // There are no attributes provided by this filter, just return false
        //--------------------------------------------------------------------------------------------------------------
        if(!sizeof($map))
        {
            return false;
        }

        $user = $this->getUser();

        //--------------------------------------------------------------------------------------------------------------
        // Iterate through attributes provided by this filter and populate user collection
        //--------------------------------------------------------------------------------------------------------------
        foreach($map as $record)
        {
            $entry = $collection->get($record['name']);

            if($entry instanceof Entry)
            {
                $renamed = $record['renamed'];

                //------------------------------------------------------------------------------------------------------
                // We don't have this entry, just add the existing one
                //------------------------------------------------------------------------------------------------------
                if(!$user->getCollection()->has($renamed))
                {
                    $user->getCollection()->add(
                        (new Entry())
                            ->setName($renamed)
                            ->setValue($entry->getValue())
                            ->setCount($entry->count())
                    );
                }
                else
                {
                    //--------------------------------------------------------------------------------------------------
                    // We have the entry, turn it into an array (if not one already) and re-add it
                    //--------------------------------------------------------------------------------------------------
                    /** @var $current_entry Entry */
                    $current_entry = $user->getCollection()->get($renamed);
                    $current_value = $current_entry->getValue();

                    $new_value = array_merge(
                            [],
                            !is_array($current_value) ? [$current_value] : $current_value,
                            !is_array($entry->getValue()) ? [$entry->getValue()] : $entry->getValue()
                        );


                    $user->getCollection()->add((new Entry())
                        ->setName($renamed)
                        ->setValue($new_value)
                        ->setCount(sizeof($new_value))
                    );

                }
            }
        }

        return true;
    }

}