<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector;

use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpClient\Psr18Client;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Zimbra\Admin\AdminApi;
use Zimbra\Common\Enum\AccountBy;
use Zimbra\Common\Soap\ClientFactory;
use Zimbra\Common\Struct\AccountSelector;
use Zimbra\Common\Struct\Header\AccountInfo;
use Zimbra\Mail\MailApi;

final class SoapClientBuilder
{
    private readonly string $username;

    private readonly string $password;

    private readonly string $url;

    private readonly string $adminUrl;

    private readonly bool $verifyHost;

    private readonly bool $verifyPeer;

    private readonly bool $adminVerifyHost;

    private readonly bool $adminVerifyPeer;

    /**
     * Keep the cache of the tokens.
     *
     * @var array<string, array{token: string, expirationTime: \DateTimeImmutable}>
     */
    private array $tokenCache = [];

    public function __construct(
        private readonly ParameterBagInterface $parameterBag,
        private readonly HttpClientInterface $client,
        private readonly ClockInterface $clock,
    ) {
        $dsn = $this->parameterBag->get('chill_calendar.remote_calendar_dsn');
        $url = parse_url($dsn);

        $this->username = urldecode($url['user']);
        $this->password = urldecode($url['pass']);
        if ('zimbra+http' === $url['scheme']) {
            $scheme = 'http';
            $port = $url['port'] ?? 80;
        } elseif ('zimbra+https' === $url['scheme']) {
            $scheme = 'https';
            $port = $url['port'] ?? 443;
        } else {
            throw new \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException('Unsupported remote calendar scheme: '.$url['scheme']);
        }

        // get attributes for adminUrl
        $query = [];
        parse_str($url['query'] ?? '', $query);
        $adminPort = $query['adminPort'] ?? '7071';
        $adminHost = $query['adminHost'] ?? $url['host'];
        $adminScheme = $query['adminScheme'] ?? $scheme;

        $this->verifyPeer = (bool) ($query['verifyPeer'] ?? true);
        $this->verifyHost = (bool) ($query['verifyHost'] ?? true);
        $this->adminVerifyHost = (bool) ($query['adminVerifyHost'] ?? $this->verifyPeer);
        $this->adminVerifyPeer = (bool) ($query['adminVerifyPeer'] ?? $this->verifyHost);

        $this->url = $scheme.'://'.$url['host'].':'.$port;
        $this->adminUrl = $adminScheme.'://'.$adminHost.':'.$adminPort;
    }

    private function buildApi(): MailApi
    {
        $baseClient = $this->client->withOptions([
            'base_uri' => $location = $this->url.'/service/soap',
            'verify_host' => $this->verifyHost,
            'verify_peer' => $this->verifyPeer,
        ]);
        $psr18Client = new Psr18Client($baseClient);
        $api = new MailApi();
        $client = ClientFactory::create($location, $psr18Client);
        $api->setClient($client);

        return $api;
    }

    private function buildAdminApi(): AdminApi
    {
        $baseClient = $this->client->withOptions([
            'base_uri' => $location = $this->adminUrl.'/service/admin/soap',
            'verify_host' => $this->adminVerifyHost,
            'verify_peer' => $this->adminVerifyPeer,
        ]);
        $psr18Client = new Psr18Client($baseClient);
        $api = new AdminApi();
        $client = ClientFactory::create($location, $psr18Client);
        $api->setClient($client);

        return $api;
    }

    public function getApiForAccount(string $accountName): MailApi
    {
        ['token' => $token, 'expirationTime' => $expirationTime] = $this->tokenCache[$accountName]
            ?? ['token' => null, 'expirationTime' => null];

        if (null === $token || null === $expirationTime || $expirationTime <= $this->clock->now()) {
            $adminApi = $this->buildAdminApi();
            $adminApi->auth($this->username, $this->password);

            $delegateResponse = $adminApi->delegateAuth(new AccountSelector(AccountBy::NAME, $accountName));
            $token = $delegateResponse->getAuthToken();
            $expiration = $delegateResponse->getLifetime();
            $expirationTime = $this->clock->now()->add(new \DateInterval('PT'.$expiration.'S'));
            $this->tokenCache[$accountName] = ['token' => $token, 'expirationTime' => $expirationTime];
        }

        $apiBy = $this->buildApi();
        $apiBy->setAuthToken($token);
        $apiBy->setTargetAccount(new AccountInfo(AccountBy::NAME, $accountName));

        return $apiBy;
    }
}
