<?php

declare(strict_types=1);

namespace Gls\GlsPoland\AdePlus\Authorization;

use Gls\GlsPoland\AdePlus\Authorization\Fault\SessionExpiredFault;
use Gls\GlsPoland\AdePlus\Authorization\Fault\SessionNotFoundFault;
use Gls\GlsPoland\AdePlus\UserCredentialsInterface;
use Gls\GlsPoland\Soap\Client\Caller\CallerInterface;
use Gls\GlsPoland\Soap\Client\Exception\SoapFault;
use Gls\GlsPoland\Soap\Client\Type\RequestInterface;
use Gls\GlsPoland\Soap\Client\Type\ResultInterface;

final class AuthorizingCaller implements CallerInterface
{
    private const SESSION_IDLE_TIMEOUT_SECONDS = 1740; // 29 min for a 30 min server-side timeout

    private $caller;
    private $userCredentials;
    private $session;
    private $sessionLastActivityAt;
    private $integratorId;

    public function __construct(CallerInterface $caller, UserCredentialsInterface $userCredentials, ?string $integratorId = null)
    {
        $this->caller = $caller;
        $this->userCredentials = $userCredentials;
        $this->integratorId = null !== $integratorId && '' !== trim($integratorId)
            ? trim($integratorId)
            : null;
    }

    public function call(string $method, RequestInterface $request): ResultInterface
    {
        if (!$request instanceof AuthorizedRequest || null !== $request->getSession()) {
            return $this->caller->call($method, $request);
        }

        $authorized = $this->authorize($request);

        try {
            $result = $this->caller->call($method, $authorized);
            $this->sessionLastActivityAt = time();

            return $result;
        } catch (SoapFault $exception) {
            if (!in_array($exception->getFaultCode(), [SessionNotFoundFault::CODE, SessionExpiredFault::CODE], true)) {
                throw $exception;
            }

            $this->session = null;
            $this->sessionLastActivityAt = null;
            $authorized = $this->authorize($request);

            $result = $this->caller->call($method, $authorized);
            $this->sessionLastActivityAt = time();

            return $result;
        }
    }

    public function __destruct()
    {
        if (null === $this->session) {
            return;
        }

        try {
            $this->caller->call('adeLogout', new Logout($this->session));
        } catch (\Exception $e) {
            // ignore silently
        }
    }

    /**
     * @template T of AuthorizedRequest
     *
     * @param T $request
     *
     * @return T
     */
    private function authorize(AuthorizedRequest $request): AuthorizedRequest
    {
        if ($this->requiresLogin()) {
            $this->login();
        }

        return $request->withSession($this->session);
    }

    private function login(): void
    {
        if (null !== $this->integratorId) {
            $result = $this->caller->call('adeLoginIntegrator', new LoginIntegrator(
                $this->userCredentials->getUsername(),
                $this->userCredentials->getPassword(),
                $this->integratorId
            ));
        } else {
            $result = $this->caller->call('adeLogin', new Login(
                $this->userCredentials->getUsername(),
                $this->userCredentials->getPassword()
            ));
        }

        assert($result instanceof Session);

        $this->session = $result->getValue();
        $this->sessionLastActivityAt = time();
    }

    private function requiresLogin(): bool
    {
        if (null === $this->session) {
            return true;
        }

        if (null === $this->sessionLastActivityAt) {
            return true;
        }

        return (time() - $this->sessionLastActivityAt) >= self::SESSION_IDLE_TIMEOUT_SECONDS;
    }
}
