<?php

/*
 * CardDAV client library for PHP ("PHP-CardDavClient").
 *
 * Copyright (c) 2020-2021 Michael Stilkerich <ms@mike2k.de>
 * Licensed under the MIT license. See COPYING file in the project root for details.
 */

declare(strict_types=1);

namespace MStilkerich\CardDavClient;

use MStilkerich\CardDavClient\XmlElements\ElementNames as XmlEN;

/**
 * Represents an account on a CardDAV Server.
 *
 * @psalm-type HttpOptions = array{
 *   username?: string,
 *   password?: string,
 *   bearertoken?: string,
 *   verify?: bool|string,
 *   preemptive_basic_auth?: bool,
 *   query?: array<string, string>,
 *   headers?: array<string, string | list<string>>,
 * }
 *
 * @psalm-type SerializedAccount = HttpOptions & array{discoveryUri: string, baseUrl?: ?string}
 *
 * @package Public\Entities
 */
class Account implements \JsonSerializable
{
    /**
     * Options for the HTTP communication, including authentication credentials.
     * @var HttpOptions
     */
    private $httpOptions;

    /**
     * URI originally used to discover the account.
     * Example: _example.com_
     * @var string
     */
    private $discoveryUri;

    /**
     * URL of the discovered CardDAV server, with empty path. May be null if not discovered yet.
     * Example: _https://carddav.example.com:443_
     * @var ?string
     */
    private $baseUrl;

    /**
     * Construct a new Account object.
     *
     * @param string $discoveryUri
     *  The URI to use for service discovery. This can be a partial URI, in the simplest case just a domain name. Note
     *  that if no protocol is given, https will be used. Unencrypted HTTP will only be done if explicitly given (e.g.
     *  _http://example.com_).
     * @psalm-param string|HttpOptions $httpOptions
     * @param string|array $httpOptions
     *  The options for HTTP communication, including authentication credentials.
     *  An array with any of the keys (if no password is needed, the array may be empty, e.g. GSSAPI/Kerberos):
     *    - username/password: The username and password for mechanisms requiring such credentials (e.g. Basic, Digest)
     *    - bearertoken: The token to use for Bearer authentication (OAUTH2)
     *    - verify: How to verify the server-side HTTPS certificate. True (the default) enables certificate
     *                  verification against the default CA bundle of the OS, false disables certificate verification
     *                  (note that this defeats the purpose of HTTPS and opens the door for man in the middle attacks).
     *                  Set to the path of a PEM file containing a custom CA bundle to perform verification against a
     *                  custom set of certification authorities.
     *    - preemptive_basic_auth: Set to true to always submit an Authorization header for HTTP Basic authentication
     *      (username and password options also required in this case) even if not challenged by the server. This may be
     *      required in rare use cases where the server allows unauthenticated access and will not challenge the client.
     *    - query: Query options to append to every URL queried for this account, as an associative array of query
     *             options and values.
     *    - headers: Headers to add to each request sent for this account, an associative array mapping header name to
     *               header value (string) or header values (list<string>).
     *
     *  Deprecated: username as string for authentication mechanisms requiring username / password.
     * @param string $password
     *  Deprecated: The password to use for authentication. This parameter is deprecated, include the password in the
     *  $httpOptions parameter. This parameter is ignored unless $httpOptions is used in its deprecated string form.
     * @param string $baseUrl
     *  The URL of the CardDAV server without the path part (e.g. _https://carddav.example.com:443_). This URL is used
     *  as base URL for the underlying {@see CardDavClient} that can be retrieved using {@see Account::getClient()}.
     *  When relative URIs are passed to the client, they will be relative to this base URL. If this account is used for
     *  discovery with the {@see Services\Discovery} service, this parameter can be omitted.
     * @api
     */
    public function __construct(string $discoveryUri, $httpOptions, string $password = "", ?string $baseUrl = null)
    {
        $this->discoveryUri = $discoveryUri;
        $this->baseUrl = $baseUrl;

        if (is_string($httpOptions)) {
            $this->httpOptions = [
                'username' => $httpOptions,
                'password' => $password
            ];
        } else {
            $this->httpOptions = $httpOptions;
        }
    }

    /**
     * Constructs an Account object from an array representation.
     *
     * This can be used to reconstruct/deserialize an Account from a stored (JSON) representation.
     *
     * @psalm-param SerializedAccount $props
     * @param array<string,?string|bool> $props An associative array containing the Account attributes.
     *  Keys with the meaning from {@see Account::__construct()}:
     *  `discoveryUri`, `baseUrl`, `username`, `password`, `bearertoken`, `verify`, `preemptive_basic_auth`
     * @see Account::jsonSerialize()
     * @api
     */
    public static function constructFromArray(array $props): Account
    {
        $requiredProps = [ 'discoveryUri' ];
        foreach ($requiredProps as $prop) {
            if (!isset($props[$prop])) {
                throw new \Exception("Array used to reconstruct account does not contain required property $prop");
            }
        }

        /** @psalm-var SerializedAccount $props vimeo/psalm#10853 */
        $discoveryUri = $props["discoveryUri"];
        $baseUrl = $props["baseUrl"] ?? null;
        unset($props["discoveryUri"], $props["baseUrl"]);

        return new Account($discoveryUri, $props, "", $baseUrl);
    }

    /**
     * Allows to serialize an Account object to JSON.
     *
     * @psalm-return SerializedAccount
     * @return array<string, ?string|bool> Associative array of attributes to serialize.
     * @see Account::constructFromArray()
     */
    public function jsonSerialize(): array
    {
        return [
            "discoveryUri" => $this->discoveryUri,
            "baseUrl" => $this->baseUrl
        ] + $this->httpOptions;
    }

    /**
     * Provides a CardDavClient object to interact with the server for this account.
     *
     * @param string $baseUrl
     *  A base URL to use by the client to resolve relative URIs. If not given, the base url of the Account is used.
     *  This is useful, for example, to override the base path with that of a collection.
     *
     * @return CardDavClient
     *  A CardDavClient object to interact with the server for this account.
     */
    public function getClient(?string $baseUrl = null): CardDavClient
    {
        $clientUri = $baseUrl ?? $this->getUrl();
        return new CardDavClient($clientUri, $this->httpOptions);
    }

    /**
     * Returns the discovery URI for this Account.
     * @api
     */
    public function getDiscoveryUri(): string
    {
        return $this->discoveryUri;
    }

    /**
     * Set the base URL of this account once the service URL has been discovered.
     */
    public function setUrl(string $url): void
    {
        $this->baseUrl = $url;
    }

    /**
     * Returns the base URL of the CardDAV service.
     * @api
     */
    public function getUrl(): string
    {
        if (is_null($this->baseUrl)) {
            throw new \Exception("The base URI of the account has not been discovered yet");
        }

        return $this->baseUrl;
    }

    /**
     * Provides a readable form of the core properties of the Account.
     *
     * This is meant for printing to a human, not for parsing, and therefore may change without considering this a
     * backwards incompatible change.
     */
    public function __toString(): string
    {
        $str = $this->discoveryUri;
        $str .= ", user: " . ($this->httpOptions['username'] ?? "");
        $str .= ", CardDAV URI: ";
        $str .= $this->baseUrl ?? "not discovered yet";
        return $str;
    }

    /**
     * Queries the given URI for the current-user-principal property.
     *
     * Property description by RFC5397: The DAV:current-user-principal property contains either a DAV:href or
     * DAV:unauthenticated XML element. The DAV:href element contains a URL to a principal resource corresponding to the
     * currently authenticated user. That URL MUST be one of the URLs in the DAV:principal-URL or DAV:alternate-URI-set
     * properties defined on the principal resource and MUST be an http(s) scheme URL. When authentication has not been
     * done or has failed, this property MUST contain the DAV:unauthenticated pseudo-principal.
     * In some cases, there may be multiple principal resources corresponding to the same authenticated principal. In
     * that case, the server is free to choose any one of the principal resource URIs for the value of the
     * DAV:current-user-principal property. However, servers SHOULD be consistent and use the same principal resource
     * URI for each authenticated principal.
     *
     * @param string $contextPathUri
     *  The given URI should typically be a context path per the terminology of RFC6764.
     *
     * @return ?string
     *  The principal URI (string), or NULL in case of error. The returned URI is suited to be used for queries with
     *  this client (i.e. either a full URI, or meaningful as relative URI to the base URI of this client).
     */
    public function findCurrentUserPrincipal(string $contextPathUri): ?string
    {
        $princUrl = null;

        try {
            $client = $this->getClient();
            $result = $client->findProperties($contextPathUri, [XmlEN::CURUSRPRINC]);

            if (isset($result[0]["props"][XmlEN::CURUSRPRINC])) {
                $princUrl = $result[0]["props"][XmlEN::CURUSRPRINC];
                $princUrl = CardDavClient::concatUrl($result[0]["uri"], $princUrl);
                Config::$logger->info("principal URL: $princUrl");
            }
        } catch (\Exception $e) {
            Config::$logger->info("Exception while querying current-user-principal: " . $e->getMessage());
        }

        return $princUrl;
    }

    /**
     * Queries the given URI for the CARDDAV:addressbook-home-set property.
     *
     * Property description by RFC6352: The CARDDAV:addressbook-home-set property is meant to allow users to easily find
     * the address book collections owned by the principal. Typically, users will group all the address book collections
     * that they own under a common collection. This property specifies the URL of collections that are either address
     * book collections or ordinary collections that have child or descendant address book collections owned by the
     * principal.
     *
     * @param string $principalUri
     *  The given URI should be (one of) the authenticated user's principal URI(s).
     *
     * @return null|list<string>
     *  The user's addressbook home URIs, or null in case of error. The returned URIs are suited to be used for queries
     *  with this client (i.e. either a full URI, or meaningful as relative URI to the base URI of this client).
     */
    public function findAddressbookHomes(string $principalUri): ?array
    {
        $addressbookHomeUris = [];

        try {
            $client = $this->getClient();
            $result = $client->findProperties($principalUri, [XmlEN::ABOOK_HOME]);

            if (isset($result[0]["props"][XmlEN::ABOOK_HOME])) {
                $hrefs = $result[0]["props"][XmlEN::ABOOK_HOME];

                foreach ($hrefs as $href) {
                    $addressbookHomeUri = CardDavClient::concatUrl($result[0]["uri"], $href);
                    $addressbookHomeUris[] = $addressbookHomeUri;
                    Config::$logger->info("addressbook home: $addressbookHomeUri");
                }
            }
        } catch (\Exception $e) {
            Config::$logger->info("Exception while querying addressbook-home-set: " . $e->getMessage());
            return null;
        }

        return $addressbookHomeUris;
    }
}

// vim: ts=4:sw=4:expandtab:fenc=utf8:ff=unix:tw=120
