<?php

namespace Api {

  use Objects\ConnectionData;

  abstract class MailAPI extends Request {
    protected function getMailConfig(): ?ConnectionData {
      $req = new \Api\Settings\Get($this->user);
      $this->success = $req->execute(array("key" => "^mail_"));
      $this->lastError = $req->getLastError();

      if ($this->success) {
        $settings = $req->getResult()["settings"];

        if (!isset($settings["mail_enabled"]) || $settings["mail_enabled"] !== "1") {
          $this->createError("Mail is not configured yet.");
          return null;
        }

        $host = $settings["mail_host"] ?? "localhost";
        $port = intval($settings["mail_port"] ?? "25");
        $login = $settings["mail_username"] ?? "";
        $password = $settings["mail_password"] ?? "";
        $connectionData = new ConnectionData($host, $port, $login, $password);
        $connectionData->setProperty("from", $settings["mail_from"] ?? "");
        $connectionData->setProperty("last_sync", $settings["mail_last_sync"] ?? "");
        return $connectionData;
      }

      return null;
    }
  }
}

namespace Api\Mail {

  use Api\MailAPI;
  use Api\Parameter\Parameter;
  use Api\Parameter\StringType;
  use Driver\SQL\Column\Column;
  use Driver\SQL\Condition\Compare;
  use Driver\SQL\Condition\CondIn;
  use Driver\SQL\Strategy\UpdateStrategy;
  use External\PHPMailer\Exception;
  use External\PHPMailer\PHPMailer;
  use Objects\ConnectionData;
  use Objects\User;

  class Test extends MailAPI {

    public function __construct(User $user, bool $externalCall = false) {
      parent::__construct($user, $externalCall, array(
        "receiver" => new Parameter("receiver", Parameter::TYPE_EMAIL)
      ));
    }

    public function execute($values = array()): bool {
      if (!parent::execute($values)) {
        return false;
      }

      $receiver = $this->getParam("receiver");
      $req = new \Api\Mail\Send($this->user);
      $this->success = $req->execute(array(
        "to" => $receiver,
        "subject" => "Test E-Mail",
        "body" => "Hey! If you receive this e-mail, your mail configuration seems to be working."
      ));

      $this->lastError = $req->getLastError();
      return $this->success;
    }
  }

  class Send extends MailAPI {
    public function __construct($user, $externalCall = false) {
      parent::__construct($user, $externalCall, array(
        'to' => new Parameter('to', Parameter::TYPE_EMAIL, true, null),
        'subject' => new StringType('subject', -1),
        'body' => new StringType('body', -1),
        'replyTo' => new Parameter('replyTo', Parameter::TYPE_EMAIL, true, null),
        'replyName' => new StringType('replyName', 32, true, "")
      ));
      $this->isPublic = false;
    }

    public function execute($values = array()): bool {
      if (!parent::execute($values)) {
        return false;
      }

      $mailConfig = $this->getMailConfig();
      if (!$this->success) {
        return false;
      }

      $fromMail = $mailConfig->getProperty('from');
      $toMail = $this->getParam('to') ?? $fromMail;
      $subject = $this->getParam('subject');
      $replyTo = $this->getParam('replyTo');
      $replyName = $this->getParam('replyName');

      try {
        $mail = new PHPMailer;
        $mail->IsSMTP();
        $mail->setFrom($fromMail);
        $mail->addAddress($toMail);

        if ($replyTo) {
          $mail->addReplyTo($replyTo, $replyName);
        }

        $mail->Subject = $subject;
        $mail->SMTPDebug = 0;
        $mail->Host = $mailConfig->getHost();
        $mail->Port = $mailConfig->getPort();
        $mail->SMTPAuth = true;
        $mail->Username = $mailConfig->getLogin();
        $mail->Password = $mailConfig->getPassword();
        $mail->SMTPSecure = 'tls';
        $mail->IsHTML(true);
        $mail->CharSet = 'UTF-8';
        $mail->Body = $this->getParam('body');

        $this->success = @$mail->Send();
        if (!$this->success) {
          $this->lastError = "Error sending Mail: $mail->ErrorInfo";
          error_log("sendMail() failed: $mail->ErrorInfo");
        } else {
          $this->result["messageId"] = $mail->getLastMessageID();
        }
      } catch (Exception $e) {
        $this->success = false;
        $this->lastError = "Error sending Mail: $e";
      }

      return $this->success;
    }
  }

  // TODO: IMAP mail settings :(
  // TODO: attachments
  class Sync extends MailAPI {

    public function __construct(User $user, bool $externalCall = false) {
      parent::__construct($user, $externalCall, array());
      $this->loginRequired = true;
    }

    private function fetchMessageIds() {
      $sql = $this->user->getSQL();
      $res = $sql->select("uid", "messageId")
        ->from("ContactRequest")
        ->where(new Compare("messageId", NULL, "!="))
        ->execute();

      $this->success = ($res !== false);
      $this->lastError = $sql->getLastError();
      if (!$this->success) {
        return false;
      }

      $messageIds = [];
      foreach ($res as $row) {
        $messageIds[$row["messageId"]] = $row["uid"];
      }
      return $messageIds;
    }

    private function findContactRequest(array &$messageIds, array &$references): ?int {
      foreach ($references as &$ref) {
        if (isset($messageIds[$ref])) {
          return $messageIds[$ref];
        }
      }

      return null;
    }

    private function parseBody(string $body): string {
      // TODO clean this up
      return trim($body);
    }

    private function insertMessages($messages): bool {
      $sql = $this->user->getSQL();

      $query = $sql->insert("ContactMessage", ["request_id", "user_id", "message", "messageId", "created_at"])
        ->onDuplicateKeyStrategy(new UpdateStrategy(["messageId"], ["message" => new Column("message")]));

      $entityIds = [];
      foreach ($messages as $message) {
        $requestId = $message["requestId"];
        $query->addRow(
          $requestId,
          $sql->select("uid")->from("User")->where(new Compare("email", $message["from"]))->limit(1),
          $message["body"],
          $message["messageId"],
          (new \DateTime())->setTimeStamp($message["timestamp"]),
        );

        if (!in_array($requestId, $entityIds)) {
          $entityIds[] = $requestId;
        }
      }

      $this->success = $query->execute();
      $this->lastError = $sql->getLastError();

      // Update entity log
      if ($this->success && count($entityIds) > 0) {
        $sql->update("EntityLog")
          ->set("modified", $sql->now())
          ->where(new CondIn("entityId", $entityIds))
          ->execute();
      }

      return $this->success;
    }

    private function parseDate($date) {
      $formats = [null, "D M d Y H:i:s e+", "D, j M Y H:i:s e+"];
      foreach ($formats as $format) {
        try {
          $dateObj = ($format === null ? new \DateTime($date) : \DateTime::createFromFormat($format, $date));
          if ($dateObj) {
            return $dateObj;
          }
        } catch (\Exception $exception) {
        }
      }

      return $this->createError("Could not parse date: $date");
    }

    private function getReference(ConnectionData $mailConfig): string {
      $port = 993;
      $host = str_replace("smtp", "imap", $mailConfig->getHost());
      $flags = ["/ssl"];
      return '{' . $host . ':' . $port . implode("", $flags) . '}';
    }

    private function connect(ConnectionData $mailConfig) {

      $username = $mailConfig->getLogin();
      $password = $mailConfig->getPassword();
      $ref = $this->getReference($mailConfig);
      $mbox = @imap_open($ref, $username, $password, OP_READONLY);
      if (!$mbox) {
        return $this->createError("Can't connect to mail server via IMAP: " . imap_last_error());
      }

      return $mbox;
    }

    private function listFolders(ConnectionData $mailConfig, $mbox) {

      $boxes = @imap_list($mbox, $this->getReference($mailConfig), '*');
      if (!$boxes) {
        return $this->createError("Error listing imap folders: " . imap_last_error());
      }

      return $boxes;
    }

    private function getSenderAddress($header): string {
      if (property_exists($header, "reply_to") && count($header->reply_to) > 0) {
        $mailBox = $header->reply_to[0]->mailbox;
        $host = $header->reply_to[0]->host;
      } else if (property_exists($header, "from") && count($header->from) > 0) {
        $mailBox = $header->from[0]->mailbox;
        $host = $header->from[0]->host;
      } else {
        return "unknown_addr";
      }

      return "$mailBox@$host";
    }

    private function runSearch($mbox, string $searchCriteria, ?\DateTime $lastSyncDateTime, array $messageIds, array &$messages) {

      $result = @imap_search($mbox, $searchCriteria);
      if ($result === false) {
        $err = imap_last_error(); // might return false, if not messages were found, so we can just abort without throwing an error
        return empty($err) ? true : $this->createError("Could not run search: $err");
      }

      foreach ($result as $msgNo) {
        $header = imap_headerinfo($mbox, $msgNo);
        $date = $this->parseDate($header->date);
        if ($date === false) {
          return false;
        }

        if ($lastSyncDateTime === null || \datetimeDiff($lastSyncDateTime, $date) > 0) {

          $references = property_exists($header, "references") ?
            explode(" ", $header->references) : [];

          $requestId = $this->findContactRequest($messageIds, $references);
          if ($requestId) {
            $messageId = $header->message_id;
            $senderAddress = $this->getSenderAddress($header);

            $structure = imap_fetchstructure($mbox, $msgNo);
            $attachments = [];
            $hasAttachments = (property_exists($structure, "parts"));
            if ($hasAttachments) {
              foreach ($structure->parts as $part) {
                $disposition = (property_exists($part, "disposition") ? $part->disposition : null);
                if ($disposition === "attachment") {
                  $fileName = array_filter($part->dparameters, function ($param) {
                    return $param->attribute === "filename";
                  });
                  if (count($fileName) > 0) {
                    $attachments[] = $fileName[0]->value;
                  }
                }
              }
            }

            $body = imap_fetchbody($mbox, $msgNo, "1");
            $body = $this->parseBody($body);

            if (!isset($messageId[$messageId])) {
              $messages[$messageId] = [
                "messageId" => $messageId,
                "requestId" => $requestId,
                "timestamp" => $date->getTimestamp(),
                "from" => $senderAddress,
                "body" => $body,
                "attachments" => $attachments
              ];
            }
          }
        }
      }

      return true;
    }

    public function execute($values = array()): bool {
      if (!parent::execute($values)) {
        return false;
      }

      if (!function_exists("imap_open")) {
        return $this->createError("IMAP is not enabled. Enable it inside the php config. For more information visit: https://www.php.net/manual/en/imap.setup.php");
      }

      $messageIds = $this->fetchMessageIds();
      if ($messageIds === false) {
        return false;
      } else if (count($messageIds) === 0) {
        // nothing to sync here
        return true;
      }

      $mailConfig = $this->getMailConfig();
      if (!$this->success) {
        return false;
      }

      $mbox = $this->connect($mailConfig);
      if ($mbox === false) {
        return false;
      }

      $boxes = $this->listFolders($mailConfig, $mbox);
      if ($boxes === false) {
        return false;
      }

      $now = (new \DateTime())->getTimestamp();
      $lastSync = intval($mailConfig->getProperty("last_sync", "0"));
      if ($lastSync > 0) {
        $lastSyncDateTime = (new \DateTime())->setTimeStamp($lastSync);
        $dateStr = $lastSyncDateTime->format("d-M-Y");
        $searchCriteria = "SINCE \"$dateStr\"";
      } else {
        $lastSyncDateTime = null;
        $searchCriteria = "ALL";
      }

      $messages = [];
      foreach ($boxes as $box) {
        imap_reopen($mbox, $box);
        if (!$this->runSearch($mbox, $searchCriteria, $lastSyncDateTime, $messageIds, $messages)) {
          return false;
        }
      }

      @imap_close($mbox);
      if (!empty($messages) && !$this->insertMessages($messages)) {
        return false;
      }

      $req = new \Api\Settings\Set($this->user);
      $this->success = $req->execute(array("settings" => array("mail_last_sync" => "$now")));
      $this->lastError = $req->getLastError();
      return $this->success;
    }
  }
}