<?php declare(strict_types = 1);

namespace MailPoet\Services;

if (!defined('ABSPATH')) exit;


use MailPoet\Util\DmarcPolicyChecker;

class AuthorizedSenderDomainController {
  const DOMAIN_VERIFICATION_STATUS_VALID = 'valid';
  const DOMAIN_VERIFICATION_STATUS_INVALID = 'invalid';
  const DOMAIN_VERIFICATION_STATUS_PENDING = 'pending';

  const AUTHORIZED_SENDER_DOMAIN_ERROR_ALREADY_CREATED = 'Sender domain exist';
  const AUTHORIZED_SENDER_DOMAIN_ERROR_NOT_CREATED = 'Sender domain does not exist';
  const AUTHORIZED_SENDER_DOMAIN_ERROR_ALREADY_VERIFIED = 'Sender domain already verified';

  /** @var Bridge */
  private $bridge;

  /** @var DmarcPolicyChecker */
  private $dmarcPolicyChecker;

  /** @var null|array Cached response for with authorized domains */
  private $currentRecords = null;

  public function __construct(
    Bridge $bridge,
    DmarcPolicyChecker $dmarcPolicyChecker
  ) {
    $this->bridge = $bridge;
    $this->dmarcPolicyChecker = $dmarcPolicyChecker;
  }

  /**
   * Get record of Bridge::getAuthorizedSenderDomains
   */
  public function getDomainRecords(string $domain = ''): array {
    $records = $this->getAllRecords();
    if ($domain) {
      return $records[$domain] ?? [];
    }
    return $records;
  }

  /**
   * Get all Authorized Sender Domains
   *
   * Note: This includes both verified and unverified domains
   */
  public function getAllSenderDomains(): array {
    return $this->returnAllDomains($this->getAllRecords());
  }

  public function getAllSenderDomainsIgnoringCache(): array {
    $this->currentRecords = null;
    return $this->getAllSenderDomains();
  }

  /**
   * Get all Verified Sender Domains
   */
  public function getVerifiedSenderDomains(): array {
    return $this->returnVerifiedDomains($this->getAllRecords());
  }

  public function getVerifiedSenderDomainsIgnoringCache(): array {
    $this->currentRecords = null;
    return $this->getVerifiedSenderDomains();
  }

  /**
   * Create new Sender Domain
   *
   * Throws an InvalidArgumentException if domain already exist
   *
   * returns an Array of DNS response or array of error
   */
  public function createAuthorizedSenderDomain(string $domain): array {
    $allDomains = $this->getAllSenderDomains();

    $alreadyExist = in_array($domain, $allDomains);

    if ($alreadyExist) {
      // sender domain already created. skip making new request
      throw new \InvalidArgumentException(self::AUTHORIZED_SENDER_DOMAIN_ERROR_ALREADY_CREATED);
    }

    $finalData = $this->bridge->createAuthorizedSenderDomain($domain);

    if ($finalData && isset($finalData['error'])) {
      throw new \InvalidArgumentException($finalData['error']);
    }

    // Reset cached value since a new domain was added
    $this->currentRecords = null;

    return $finalData;
  }

  /**
   * Verify Sender Domain
   *
   * Throws an InvalidArgumentException if domain does not exist or domain is already verified
   *
   * * returns [ok: bool, dns: array] if domain verification is successful
   * * or [ok: bool, error:  string, dns: array] if domain verification failed
   * * or [error: string, status: bool] for other errors
   */
  public function verifyAuthorizedSenderDomain(string $domain): array {
    $records = $this->bridge->getAuthorizedSenderDomains();

    $allDomains = $this->returnAllDomains($records);
    $verifiedDomains = $this->returnVerifiedDomains($records);

    $alreadyExist = in_array($domain, $allDomains);

    if (!$alreadyExist) {
      // can't verify a domain that does not exist
      throw new \InvalidArgumentException(self::AUTHORIZED_SENDER_DOMAIN_ERROR_NOT_CREATED);
    }

    $alreadyVerified = in_array($domain, $verifiedDomains);

    if ($alreadyVerified) {
      // no need to reverify an already verified domain
      throw new \InvalidArgumentException(self::AUTHORIZED_SENDER_DOMAIN_ERROR_ALREADY_VERIFIED);
    }

    $finalData = $this->bridge->verifyAuthorizedSenderDomain($domain);

    if ($finalData && isset($finalData['error']) && !isset($finalData['dns'])) {
      // verify api response returns
      // ok: bool,
      // error:  string,
      // dns: array
      // due to this, we need to make sure this is an actual server (or other user) error and not a verification error
      throw new \InvalidArgumentException($finalData['error']);
    }

    return $finalData;
  }

  /**
   * Check Domain DMARC Policy
   *
   * returns `true` if domain has Restricted policy e.g. policy === reject or quarantine
   * otherwise returns `false`
   */
  public function isDomainDmarcRestricted(string $domain): bool {
    $result = $this->getDmarcPolicyForDomain($domain);
    return $result !== DmarcPolicyChecker::POLICY_NONE;
  }

  /**
   * Fetch Domain DMARC Policy
   *
   * returns reject or quarantine or none
   */
  public function getDmarcPolicyForDomain(string $domain): string {
    return $this->dmarcPolicyChecker->getDomainDmarcPolicy($domain);
  }

  /**
   * Little helper function to return All Domains. alias to `array_keys`
   *
   * The domain is the key returned from the Bridge::getAuthorizedSenderDomains
   */
  private function returnAllDomains(array $records): array {
    $domains = array_keys($records);
    return $domains;
  }

  /**
   * Little helper function to return All verified domains
   */
  private function returnVerifiedDomains(array $records): array {
    $verifiedDomains = [];

    foreach ($records as $key => $value) {
      if (count($value) < 3) continue;
      [$domainKey1, $domainKey2, $secretRecord] = $value;
      if (
        $domainKey1['status'] === self::DOMAIN_VERIFICATION_STATUS_VALID &&
        $domainKey2['status'] === self::DOMAIN_VERIFICATION_STATUS_VALID &&
        $secretRecord['status'] === self::DOMAIN_VERIFICATION_STATUS_VALID
      ) {
        $verifiedDomains[] = $key;
      }
    }

    return $verifiedDomains;
  }

  private function getAllRecords(): array {
    if ($this->currentRecords === null) {
      $this->currentRecords = $this->bridge->getAuthorizedSenderDomains();
    }
    return $this->currentRecords;
  }
}
