<?php

namespace AuthStack\Driver\SOAP\BPS;


use AuthStack\Driver\Driver\AuthDriverInterface;
use AuthStack\Driver\Errors\ConnectionError;
use AuthStack\Driver\Errors\InsufficientCredentialsError;
use AuthStack\Driver\Errors\NoAuthSourceError;
use AuthStack\Driver\Errors\NoUserObjectError;
use AuthStack\Driver\SOAP\AbstractSOAP;
use AuthStack\Driver\SOAP\BPS\Lib\BpsSoapClient;
use AuthStack\Driver\SOAP\BPS\Lib\XMLBuilder;
use AuthStack\Driver\SOAP\BPS\Lib\XMLParser;
use AuthStack\Driver\Traits\TraitTemplateVariables;
use AuthStack\Driver\User\UserInterface;
use AuthStack\Driver\Model\User as UserModel;
use AuthStack\TwoFactor\Collection\KeyCollection;


class SOAP extends AbstractSOAP implements AuthDriverInterface
{
    use TraitTemplateVariables;

    /**
     * @var XMLBuilder
     */
    protected $xmlBuilder;
    /**
     * @var XMLParser
     */
    protected $xmlParser;

    /**
     * @inheritdoc
     * @return bool
     */
    public function connect()
    {
        try {
            if (!$this->client instanceof BpsSoapClient) {
                $this->client = new BpsSoapClient($this->config('connection.wsdl'),
                    $this->config('connection.options', null));
            }

            $this->xmlBuilder = new XMLBuilder(
                $this->config('connection.credentials.username'),
                $this->config('connection.credentials.password'),
                $this->config('connection.dataset')
            );

            $this->xmlParser = new XMLParser();
        } catch (\Exception $e) {
            throw new ConnectionError($e->getMessage());
        }

        return true;
    }

    /**
     * @inheritdoc
     * @return BpsSoapClient
     * @throws ConnectionError
     */
    public function getConnection()
    {
        if (!$this->client instanceof BpsSoapClient) {
            throw new ConnectionError('No active SOAP connection found.');
        }

        return $this->client;
    }

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

        $username = trim($credentials['username']);
        $password = trim($credentials['password']);

        try {
            $xmlResult = $this->getConnection()->authenticate($this->xmlBuilder->authenticate($username, $password));
        } catch (\SoapFault $e) {
            $this->last_error = $e->getMessage();
            throw new InsufficientCredentialsError($e->getMessage(), $e->getCode());
        }

        $result = $this->xmlParser->authenticate($xmlResult);

        if (!$result->isSuccess()) {
            $this->last_error = $result->fail;
            return false;
        }

        $this->fetchUser($result->bpsId);

        return true;
    }

    /**
     * @param $bpsId
     * @return User
     */
    protected function fetchUser($bpsId)
    {
        $xmlResult = $this->getConnection()->getUserDetails($this->xmlBuilder->getUserDetails($bpsId));
        $result    = $this->xmlParser->getUserDetails($xmlResult);
        $map       = $this->config('attributes');

        $userDetails = [];
        foreach ($map as $key => $record) {
            $from = $record['name'];
            $to   = $record['renamed'];
            $userDetails[ $to ] = $result->getDotAttribute($from, null);
        }

        $this->user = User::fromArray($result->bpsId, $result->locked, $userDetails, $this->cfg_key);

        // 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($bpsId);

        // 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->user;
    }

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

    /**
     * @inheritdoc
     * @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');
        }
    }

    /**
     * @return array
     */
    public static function install()
    {
        return [
            'title'       => 'BPS SOAP',
            'description' => 'Configurable SOAP driver for BPS',
            'class_path'  => static::class,
        ];
    }

    /**
     * @return UserInterface|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 $id
     * @return UserInterface|User
     */
    public function getUserByPrimaryKey($id)
    {
        $this->fetchUser($id);
        return $this->getUser();
    }

    /**
     * @param string $username
     *
     * @return UserInterface|User
     */
    public function getUserByUsername($username)
    {
        $this->fetchUser($username);
        return $this->getUser();
    }

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

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

}