<?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 Buckhill\LDAP\Client;
use function Buckhill\config;
use AuthStack\Driver\Errors\ConnectionError;
use AuthStack\Driver\Model\User as UserModel;
use AuthStack\Driver\Errors\NoAuthSourceError;
use AuthStack\Driver\Errors\InsufficientCredentialsError;
use Buckhill\LDAP\Entry;
use Buckhill\LDAP\EntryCollection;
use Buckhill\LDAP\Errors\ConnectionError as LDAPConnectionError;

abstract class AbstractLDAP
{
    /**
     * @var string
     */
    protected $cfg_key;

    /**
     * @var Client
     */
    protected $connection;

    /**
     * @var User
     */
    protected $user;

    /**
     * @var User
     */
    protected $provisioned;

    /**
     * @var null|int
     */
    protected $authsource_id = null;

    /**
     * @var string
     */
    protected $last_error = '';


    /**
     * @param      $key
     * @param int|null $authsource_id
     *
     * @return static
     */
    public static function initialize($key, $authsource_id)
    {
        $me = new static();

        $me->cfg_key = $key;

        return $me->setAuthSourceId($authsource_id);
    }


    /**
     * @return bool
     *
     * @throws ConnectionError
     * @throws \Buckhill\LDAP\Errors\BadOptionFormatError
     */
    public function connect()
    {
        try {
            return $this->getLDAPConnection()->connect();
        } catch (LDAPConnectionError $e) {
            throw new ConnectionError($e->getMessage());
        }
    }

    /**
     * @param $id
     *
     * @return $this
     */
    public function setAuthSourceId($id)
    {
        $this->authsource_id = $id;

        return $this;
    }

    /**
     * @return int|null
     *
     * @throws NoAuthSourceError
     */
    public function getAuthSourceId()
    {
        if (is_null($this->authsource_id)) {
            throw new NoAuthSourceError('No authsource_id has been set. Unable to retrieve authsource_id.');
        }

        return $this->authsource_id;
    }

    /**
     * @return string
     */
    public function lastError()
    {
        return $this->last_error;
    }

    /**
     * @return Client
     *
     * @throws ConnectionError
     */
    public function getConnection()
    {
        if (!$this->connection instanceof Client) {
            throw new ConnectionError('No LDAP connection active. Did you connect using connect() method?');
        }

        return $this->connection;
    }

    /**
     * Utility method that formats the username into a string which includes organizational unit and domain component(s).
     *
     * @param $username
     *
     * @return string
     */
    protected function username($username)
    {
        // Username mustn't have trailing / leading spaces
        $username = trim($username);

        // Escape special characters. One example is when usernames are actually emails that can contain + character
        $username = $this->getConnection()->escape($username);

        // To authenticate, we have to specify unique ID, organizational unit and domain component so LDAP knows
        // where to start looking for the user
        return sprintf('%s=%s,%s,%s',
            $this->config('user_auth.uid'),
            $username,
            $this->config('user_auth.ou'),
            $this->config('user_auth.dc')
        );
    }

    /**
     * @param array $credentials
     *
     * @throws InsufficientCredentialsError
     */
    protected function check(array $credentials)
    {
        if (!isset($credentials['username'])) {
            throw new InsufficientCredentialsError('Insufficient credentials: username is required');
        }

        if (!isset($credentials['password'])) {
            throw new InsufficientCredentialsError('Insufficient credentials: password is required');
        }
    }

    /**
     * @param $username
     *
     * @return User
     */
    protected function fetchUser($username)
    {
        $conn = $this->getConnection();

        $base_dn    = sprintf('%s,%s', $this->config('user_auth.ou'), $this->config('user_auth.dc'));
        $filter     = sprintf('%s=%s', $this->config('user_auth.uid', 'user_auth.uid'), $conn->escape(trim($username)));
        $which      = $this->getSearchableAttributes();
        $map        = $this->config('attributes');

        //--------------------------------------------------------------------------------------------------------------
        // This part is important, it uses mappings to fetch user attributes - memberof must be explicitly specified here
        //--------------------------------------------------------------------------------------------------------------
        if(!empty($which))
        {
            $attributes = $conn->search($base_dn, $filter, $which);
        }
        else
        {
            $attributes = $conn->search($base_dn, $filter);
        }

        //--------------------------------------------------------------------------------------------------------------
        // Populate the collection with attributes
        //--------------------------------------------------------------------------------------------------------------
        $collection = new EntryCollection();

        foreach($map as $record)
        {
            // source_id = 1 means the attribute comes from user entry, not filter
            if($record['source_id'] == 1)
            {
                if($attributes->has($record['name']))
                {
                    $entry = $attributes->get($record['name']);

                    $collection->add(
                        (new Entry())
                            ->setName($record['renamed'])
                            ->setValue($entry->getValue())
                            ->setCount($entry->count())
                    );
                }
            }
        }

        //--------------------------------------------------------------------------------------------------------------
        // Execute filters
        //--------------------------------------------------------------------------------------------------------------
        $this->user = User::fromEntryCollection($collection, $this->cfg_key);

        return $this->user;
    }

    /**
     * @return array
     */
    protected function getSearchableAttributes()
    {
        $attr = $this->config('attributes', []);

        $result = [];

        foreach($attr as $record)
        {
            // source_id = 1 means the attribute comes from user entry, not filter
            if(isset($record['name']) && $record['source_id'] == 1 && $record['enabled'] === true)
            {
                $result[] = $record['name'];
            }
        }

        return $result;
    }

    /**
     * @return array
     */
    protected function getUserAttributes()
    {
        $result = [];

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

        foreach($attributes as $record)
        {
            if(isset($record['attribute']))
            {
                $result[] = $record['attribute'];
            }
        }

        return $result;
    }

    /**
     * @param $username
     *
     * @throws NoAuthSourceError
     */
    protected function provision($username)
    {
        $this->provisioned = UserModel::retrieve([
            'conn_id'   => $this->getAuthSourceId(),
            'username'  => $username
        ]);
    }

    /**
     * @return Client
     */
    protected function getLDAPConnection()
    {
        if (!$this->connection instanceof Client) {
            $this->createConnection();
        }

        return $this->connection;
    }

    /**
     * @return $this
     */
    protected function createConnection()
    {
        $this->connection = $connection = new Client(
            $this->config('connection.host'),
            $this->config('connection.port'),
            $this->config('connection.options', []),
            $this->config('connection.TLS', false)
        );

        return $this;
    }

    /**
     * @param      $key
     * @param null $default
     *
     * @return string|int|float|array|null
     */
    protected function config($key, $default = null)
    {
        return config(sprintf('%s.%s', $this->cfg_key, $key), $default);
    }

    /**
     * @param $username
     *
     * @return string
     */
    protected function getUniqueId($username)
    {
        return sha1(sprintf("%d.%s", $this->getAuthSourceId(), $username), true);
    }
}
