<?php
namespace Fifa\ConnectId\Sdk\ApiClient\Organisation;

use Exception;
use Fifa\ConnectId\Sdk\ActorResolver\ActorResolverInterface;
use Fifa\ConnectId\Sdk\Api\Model\FindOrganisationsRequestType;
use Fifa\ConnectId\Sdk\Api\Model\FindOrganisationsResponseType;
use Fifa\ConnectId\Sdk\Api\Model\GetOrganisationDuplicatesResponseType;
use Fifa\ConnectId\Sdk\Api\Model\MemberAssociationResponseType;
use Fifa\ConnectId\Sdk\Api\Model\MergeOrganisationsRequestType;
use Fifa\ConnectId\Sdk\Api\Model\NationalAssociationsOnboardedRequestType;
use Fifa\ConnectId\Sdk\Api\Model\NationalAssociationsOnboardedResponseType;
use Fifa\ConnectId\Sdk\Api\Model\RegisterResponseType;
use Fifa\ConnectId\Sdk\Api\Model\UnmergeOrganisationsRequestType;
use Fifa\ConnectId\Sdk\Api\Resource;
use Fifa\ConnectId\Sdk\Api\Model\OrganisationLocalType;
use Fifa\ConnectId\Sdk\ApiClient\HttpResponseLogging;
use Fifa\ConnectId\Sdk\ApiModelValidation\ContextTypeValidator;
use Fifa\ConnectId\Sdk\ApiModelValidation\FifaIdValidator;
use Fifa\ConnectId\Sdk\Exception\FifaConflictException;
use Fifa\ConnectId\Sdk\Exception\FifaConnectIdException;
use Fifa\ConnectId\Sdk\Exception\FifaDuplicatedOrganisationFoundException;
use Fifa\ConnectId\Sdk\Exception\FifaEntityNotFoundException;
use Fifa\ConnectId\Sdk\Exception\FifaOrganisationMergedException;
use Fifa\ConnectId\Sdk\Exception\InvalidClientDataException;
use Fifa\ConnectId\Sdk\ExceptionHandler\ExceptionHandler;
use Fifa\ConnectId\Sdk\ExceptionHandler\Handler\ConflictResponseHandler;
use Fifa\ConnectId\Sdk\ExceptionHandler\Handler\FifaDuplicatedOrganisationFoundHandler;
use Fifa\ConnectId\Sdk\ExceptionHandler\Handler\MergeOperations\FifaOrganisationMergedHandler;
use Fifa\ConnectId\Sdk\Registration\ValidationException;
use Fifa\ConnectId\Sdk\Util\ActorEncoder;
use Fifa\ConnectId\Sdk\Util\ForcePrefixFormatter;
use Fifa\ConnectServiceBus\Sdk\Exception\AuthenticationException;
use Fifa\ConnectServiceBus\Sdk\Exception\UnauthorizedException;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;

/**
 * Class OrganisationApi
 * @package Fifa\ConnectId\Sdk\ApiClient\Organisation
 */
class OrganisationApi implements OrganisationApiInterface
{
    use HttpResponseLogging;

    /**
     * @var Resource\OrganisationApi
     */
    private $organisationApi;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var ActorResolverInterface
     */
    private $actorResolver;

    /**
     * Constructor
     *
     * @param Resource\OrganisationApi $organisationApi
     * @param LoggerInterface $logger
     * @param ActorResolverInterface $actorResolver
     */
    public function __construct(Resource\OrganisationApi $organisationApi, LoggerInterface $logger, ActorResolverInterface $actorResolver)
    {
        $this->organisationApi = $organisationApi;
        $this->logger = $logger;
        $this->actorResolver = $actorResolver;
    }

    /**
     * Registers a new organisation and assigns FIFA Unique Identifier.
     * Uses organisation data as defined in FIFA Data Standard.
     *
     * @param OrganisationLocalType $organisation - object containing information about the organisation.
     * @param bool $force - registers organisation even if possible duplicates are detected
     * @return string - FIFA Unique Identifier
     *
     * @throws InvalidClientDataException
     * @throws FifaConnectIdException
     * @throws FifaDuplicatedOrganisationFoundException
     * @throws UnauthorizedException
     * @throws AuthenticationException
     * @throws ValidationException
     */
    public function register(OrganisationLocalType $organisation, bool $force = false): string
    {
        ContextTypeValidator::validate($organisation, "OrganisationLocalType");
        $this->logger->info('Registering an organisation', array('organisation' => $organisation->getInternationalName()));
        try {
            /** @var RegisterResponseType $registerResponseType */
            list($registerResponseType, $statusCode, $httpHeaders) =
                $this->organisationApi->organisationRegisterByForceWithHttpInfo($organisation, $force ? 'true' : 'false', ActorEncoder::encode($this->actorResolver));

            $this->logResponse($this->logger, $registerResponseType, $statusCode, $httpHeaders, 'Registered organisation');
            return $registerResponseType->getUniqueFifaId();
        }
        catch (Exception $e) {
            $exception = ExceptionHandler::create($e, new FifaDuplicatedOrganisationFoundHandler());
            $this->logger->error('Error during ' . ForcePrefixFormatter::forceToString($force, 'registering') . ' an organisation',
                array('exception' => $exception->__toString()));
            throw $exception;
        }
    }

    /**
     * Gets organisation data associated with the specified FIFA Unique Identifier.
     *
     * @param string $organisationFifaId - FIFA Unique Identifier
     * @return OrganisationLocalType
     *
     * @throws InvalidClientDataException
     * @throws FifaEntityNotFoundException
     * @throws FifaConnectIdException
     * @throws FifaOrganisationMergedException
     * @throws UnauthorizedException
     * @throws AuthenticationException
     * @throws ValidationException
     */
    public function getOrganisation(string $organisationFifaId): OrganisationLocalType
    {
        FifaIdValidator::validate($organisationFifaId, "organisationFifaId");
        $this->logger->info('Getting organisation data', array('uniqueFifaId' => $organisationFifaId));
        try {
            /** @var OrganisationLocalType $organisationType */
            list($organisationType, $statusCode, $httpHeaders) = $this->organisationApi->organisationGetByOrganisationfifaidWithHttpInfo($organisationFifaId);
            $this->logResponse($this->logger, $organisationType, $statusCode, $httpHeaders, 'Organisation data received');

            return $organisationType;
        }
        catch (Exception $e) {
            $exception = ExceptionHandler::create($e, new FifaOrganisationMergedHandler());
            $this->logger->error('Getting organisation data error', array('exception' => $exception->__toString()));
            throw $exception;
        }
    }

    /**
     * Updates the organisation.
     *
     * @param OrganisationLocalType $organisation - Object containing information about the organisation to update
     * @param bool $force - updates organisation even if possible duplicates are detected
     * @return OrganisationLocalType
     *
     * @throws InvalidClientDataException
     * @throws FifaEntityNotFoundException
     * @throws FifaConnectIdException
     * @throws FifaDuplicatedOrganisationFoundException
     * @throws UnauthorizedException
     * @throws AuthenticationException
     * @throws ValidationException
     */
    public function update(OrganisationLocalType $organisation, bool $force = false): OrganisationLocalType
    {
        ContextTypeValidator::validate($organisation, "OrganisationLocalType");
        $this->logger->info('Updating an organisation', array('organisation' => $organisation->getInternationalName()));
        try {
            /** @var OrganisationLocalType $organisationType */
            list($organisationType, $statusCode, $httpHeaders) =
                $this->organisationApi->organisationUpdateByForceWithHttpInfo($organisation, $force ? 'true' : 'false', ActorEncoder::encode($this->actorResolver));
            $this->logResponse($this->logger, $organisationType, $statusCode, $httpHeaders, 'Updated organisation data received');

            return $organisationType;
        }
        catch (Exception $e) {
            $exception = ExceptionHandler::create($e, new FifaDuplicatedOrganisationFoundHandler());
            $this->logger->error('Failed to ' . ForcePrefixFormatter::forceToString($force, 'update') . ' the organisation ' . $organisation->getOrganisationFifaId(),
                array('exception' => $exception->__toString()));
            throw $exception;
        }
    }

    /**
     * Merges two organisations. Returns the primary organisation.
     *
     * @param $primaryOrganisationFifaId - Primary organisation FIFA Unique Identifier
     * @param $secondaryOrganisationFifaId - Secondary organisation FIFA Unique Identifier
     * @return OrganisationLocalType
     *
     * @throws InvalidClientDataException
     * @throws FifaEntityNotFoundException
     * @throws FifaConflictException
     * @throws FifaConnectIdException
     * @throws FifaDuplicatedOrganisationFoundException
     * @throws UnauthorizedException
     * @throws AuthenticationException
     * @throws ValidationException
     */
    public function mergeOrganisations($primaryOrganisationFifaId, $secondaryOrganisationFifaId): OrganisationLocalType
    {
        FifaIdValidator::validate($primaryOrganisationFifaId, "primaryOrganisationFifaId");
        FifaIdValidator::validate($secondaryOrganisationFifaId, "secondaryOrganisationFifaId");

        $this->logger->info(sprintf('Merging organisations: `%s` (Primary) and `%s` (Secondary)', $primaryOrganisationFifaId, $secondaryOrganisationFifaId));
        try {

            $request = new MergeOrganisationsRequestType();
            $request->setPrimaryOrganisationFifaId($primaryOrganisationFifaId)->setSecondaryOrganisationFifaId($secondaryOrganisationFifaId);

            /** @var OrganisationLocalType $organisationType */
            list($organisationType, $statusCode, $httpHeaders) = $this->organisationApi->organisationMergeWithHttpInfo($request, ActorEncoder::encode($this->actorResolver));
            $this->logResponse($this->logger, $organisationType, $statusCode, $httpHeaders, 'Merged organisation data received');

            return $organisationType;
        }
        catch (Exception $e) {
            $exception = ExceptionHandler::create($e, new ConflictResponseHandler());
            $this->logger->error(
                sprintf('Failed to merge organisations `%s` (Primary) and `%s` (Secondary)', $primaryOrganisationFifaId, $secondaryOrganisationFifaId),
                array('exception' => $exception->__toString())
            );
            throw $exception;
        }
    }

    /**
     * Reverts merge operation. Returns the secondary organisation when unmerge operations is completed.
     *
     * @param $primaryOrganisationFifaId - Primary organisation FIFA Unique Identifier
     * @param $secondaryOrganisationFifaId - Secondary organisation FIFA Unique Identifier
     * @return OrganisationLocalType
     *
     * @throws InvalidClientDataException
     * @throws FifaEntityNotFoundException
     * @throws FifaConflictException
     * @throws FifaConnectIdException
     * @throws UnauthorizedException
     * @throws AuthenticationException
     * @throws ValidationException
     */
    public function unmergeOrganisations($primaryOrganisationFifaId, $secondaryOrganisationFifaId): OrganisationLocalType
    {
        FifaIdValidator::validate($primaryOrganisationFifaId, "primaryOrganisationFifaId");
        FifaIdValidator::validate($secondaryOrganisationFifaId, "secondaryOrganisationFifaId");

        $this->logger->info(sprintf('Unmerging organisations: `%s` (Primary) and `%s` (Secondary)', $primaryOrganisationFifaId, $secondaryOrganisationFifaId));
        try {

            $request = new UnmergeOrganisationsRequestType();
            $request->setPrimaryOrganisationFifaId($primaryOrganisationFifaId)->setSecondaryOrganisationFifaId($secondaryOrganisationFifaId);

            /** @var OrganisationLocalType $organisationType */
            list($organisationType, $statusCode, $httpHeaders) = $this->organisationApi->organisationUnmergeWithHttpInfo($request, ActorEncoder::encode($this->actorResolver));
            $this->logResponse($this->logger, $organisationType, $statusCode, $httpHeaders, 'Unmerged organisation data received');

            return $organisationType;
        }
        catch (Exception $e) {
            $exception = ExceptionHandler::create($e, new ConflictResponseHandler());
            $this->logger->error(
                sprintf('Failed to unmerge organisations `%s` (Primary) and `%s` (Secondary)', $primaryOrganisationFifaId, $secondaryOrganisationFifaId),
                array('exception' => $exception->__toString())
            );
            throw $exception;
        }
    }

    /**
     * Searches for organisations.
     *
     * @param FindOrganisationsRequestType $request - Object containing search criteria
     * @return FindOrganisationsResponseType
     *
     * @throws FifaConnectIdException
     * @throws UnauthorizedException
     * @throws AuthenticationException
     * @throws ValidationException
     */
    public function findOrganisations(FindOrganisationsRequestType $request): FindOrganisationsResponseType
    {
        ContextTypeValidator::validate($request, "FindOrganisationsRequestType");
        try {
            $this->logger->info(sprintf('Searching for organisations: %s', $request->getInternationalName()));
            list($response, $statusCode, $httpHeaders) =
                $this->organisationApi->organisationFindWithHttpInfo($request, ActorEncoder::encode($this->actorResolver));
            $this->logResponse($this->logger, $response, $statusCode, $httpHeaders, 'Received find organisations response');

            return $response;
        }
        catch (Exception $e) {
            $exception = ExceptionHandler::create($e);
            $this->logger->error('Failed to find organisations', array(
                'request' => $request->__toString(),
                'exception' => $exception->__toString()
            ));
            throw $exception;
        }
    }

    /**
     * Verify in Connect ID if organisations are onboarded.
     *
     * @throws AuthenticationException
     * @throws UnauthorizedException
     * @throws InvalidClientDataException
     * @throws FifaConnectIdException
     * @throws FifaEntityNotFoundException
     * @throws ValidationException
     *
     * @param NationalAssociationsOnboardedRequestType $request
     * @return NationalAssociationsOnboardedResponseType - List of objects containing organisations FIFA ID and boolean which indicates if an organisation is onboarded.
     */
    public function getNationalAssociationsOnboardedStatus(NationalAssociationsOnboardedRequestType $request): NationalAssociationsOnboardedResponseType
    {
        if (empty($request->getNationalAssociationsFifaIds())) {
            throw new InvalidArgumentException('NationalAssociationsFifaIds list cannot be empty.');
        }

        ContextTypeValidator::validate($request, "NationalAssociationsOnboardedRequestType");

        try {
            $this->logger->info(sprintf('Verifying organisations [%s] onboarded status', join(', ', $request->getNationalAssociationsFifaIds())));
            $response = $this->organisationApi->organisationGetMemberAssociationsOnboardedStatus($request, ActorEncoder::encode($this->actorResolver));
            $this->logger->debug('Received NationalAssociationsOnboardedStatus response from the ID Directory service', ['response' => $response->__toString()]);

            return $response;
        }
        catch (Exception $e) {
            $exception = ExceptionHandler::create($e);
            $this->logger->error(
                sprintf('Failed to verify if organisations [%s] are onboarded', join(', ', $request->getNationalAssociationsFifaIds())),
                array('exception' => $exception->__toString())
            );
            throw $exception;
        }
    }

    /**
     * Verify in Connect ID all National Associations onboarded status.
     *
     * @throws AuthenticationException
     * @throws UnauthorizedException
     * @throws FifaConnectIdException
     *
     * @return NationalAssociationsOnboardedResponseType - List of objects containing organisations FIFA ID and boolean which indicates if an organisation is onboarded.
     */
    public function getAllNationalAssociationsOnboardedStatus(): NationalAssociationsOnboardedResponseType
    {
        try {
            $this->logger->info('Verifying all National Associations onboarded status');
            $response = $this->organisationApi->organisationGetMemberAssociationsOnboardedStatus(
                new NationalAssociationsOnboardedRequestType(),
                ActorEncoder::encode($this->actorResolver)
            );
            $this->logger->debug('Received AllNationalAssociationsOnboardedStatus response from the ID Directory service', ['response' => $response->__toString()]);

            return $response;
        }
        catch (Exception $e) {
            $exception = ExceptionHandler::create($e);
            $this->logger->error(
                'Failed to verify National Associations onboarded status',
                array('exception' => $exception->__toString())
            );
            throw $exception;
        }
    }

    /**
     * Gets a list of organisations this organisation may be duplicated with.
     * Uses organisation data as defined in FIFA Data Standard.
     *
     * @param OrganisationLocalType $organisation - Object containing information about the organisation
     * @return array
     *
     * @throws InvalidClientDataException
     * @throws FifaConnectIdException
     * @throws UnauthorizedException
     * @throws AuthenticationException
     * @throws ValidationException
     */
    public function getDuplicates(OrganisationLocalType $organisation): array
    {
        ContextTypeValidator::validate($organisation, "OrganisationLocalType");
        try {
            $this->logger->info(sprintf('Getting duplicates for an organisation: %s', $organisation->getOrganisationFifaId()));
            $response = $this->organisationApi->organisationGetDuplicatesWithHttpInfo($organisation, ActorEncoder::encode($this->actorResolver));
            /** @var GetOrganisationDuplicatesResponseType $response[0] */
            $this->logger->debug('Received organisation duplicates response from the ID Directory service', ['response' => $response[0]->__toString()]);

            return $response;
        }
        catch (Exception $e) {
            $exception = ExceptionHandler::create($e);
            $this->logger->error(
                sprintf('Failed to get organisation duplicates: %s', $organisation->getOrganisationFifaId()),
                array('exception' => $exception->__toString())
            );
            throw $exception;
        }
    }

    /**
     * Returns ID Directory API client
     * @return Resource\OrganisationApi
     */
    public function getIdDirectoryApiClient(): Resource\OrganisationApi
    {
        return $this->organisationApi;
    }

    /**
     * Gets FIFA Id and short name of the Member Association the searched organisation belongs to.
     *
     * @param string $organisationFifaId - FIFA Unique Identifier
     * @return MemberAssociationResponseType
     * @throws FifaEntityNotFoundException
     * @throws FifaConnectIdException
     * @throws AuthenticationException
     * @throws UnauthorizedException
     * @throws InvalidClientDataException
     * @throws ValidationException
     */
    public function getMemberAssociationForFifaId(string $organisationFifaId): MemberAssociationResponseType
    {
        FifaIdValidator::validate($organisationFifaId, "organisationFifaId");
        $this->logger->info(sprintf('Getting Member Association for an organisation: %s', $organisationFifaId));
        try {
            /** @var MemberAssociationResponseType $memberAssociation */
            list($memberAssociation, $statusCode, $httpHeaders) = $this->organisationApi->organisationGetMemberAssociationForFifaIdByOrganisationfifaidWithHttpInfo($organisationFifaId);

            $this->logResponse($this->logger, $memberAssociation, $statusCode, $httpHeaders, 'Member Association data received');

            return $memberAssociation;
        }
        catch (Exception $e) {
            $exception = ExceptionHandler::create($e);
            $this->logger->error(
                sprintf('Failed to get Member Association for an organisation: %s', $organisationFifaId),
                array('exception' => $exception->__toString())
            );
            throw $exception;
        }
    }
}