<?php
namespace Entity;

/**
 * Entity class for Email queue management.
 * Used to store email details in a queue for later processing.
 */
class Email extends \App\Entity\GeneralAbstract
{
    /**
     * Indicate if the operations available for the current entity must be done inside a transaction or not.
     * @var Boolean
     */
    protected $saveInsideTransaction = true;
    
	/**
	 * Indicates if the email was not sent due to an SMTP configuration error.
	 * @var string
	 */
	public string $sendingOutput = "";

	/**
	 * @param $idEntity
	 * @return self
	 */
	static public function send(int $idEntity)
	{
		return new self('send', $idEntity);
	}

	/**
	 * @param $idEntity
	 * @return self
	 */
	static public function purge(int $idEntity)
	{
		return new self('purge', $idEntity);
	}

	/**
	 * Rewrites the update operation so it is never used.
	 * @param Int $idEntity
	 * @throws \Exception
	 */
	static public function update($idEntity)
	{
		throw new \Exception('You cannot use `update` operation with class `'.__CLASS__.'` but only `insert`.');
	}

    /**
     * Set the table name for this entity
     * @see \App\Entity.SelectAbstract::setTableName()
     */
    protected function setTableName(): void
    {
        global $DBTEmailsQueue;
        $this->tableName = $DBTEmailsQueue;
    }

    /**
     * Get attribute value
     * @param string $var
     * @return mixed
     */
    protected function getAttribute ($var) : mixed
    {
        switch ($var) {
			case 'embeddedImages': case 'attachments':  case 'bcc':  case 'cc': case 'to': {
                if (!is_array($this->fields[$var])) {
                    if ($this->fields[$var]) {
                        $this->fields[$var] = json_decode($this->fields[$var], true);
                    } else {
                        $this->fields[$var] = [];
                    }
                }
                return $this->fields[$var];
            }

            case 'addUser': {
                return \Entity\User::select($this->fields['idAddUser'], $this->selectForUpdate);
            }

            case 'updateUser': {
                return \Entity\User::select($this->fields['idUpdateUser'], $this->selectForUpdate);
            }
            

            // the rest of the fields (most of them are just copied)
            default: {
                return parent::getAttribute($var);
            }
        }
    }

    /**
     * Set attribute value
     * @param string $var
     * @param mixed $value
     * @throws \Exception
     */
    protected function setAttribute ($var, $value) : void
    {
        // check that operation is okay
        if ($this->operation != 'insert') {
            throw new \Exception('You can use __set magic method only with insert operation in `'.__CLASS__.'` class.');
        }

        // CHECK EACH POSSIBLE FIELD VALUE
        switch ($var) {
            case 'subject': case 'body': case 'fromName': case 'fromEmail': case 'priority': case 'sendDatetime': case 'category': case 'readReceipt': {
                $this->fields[$var] = $value;
                break;
            }

            case 'to': case 'cc': case 'bcc': case 'attachments': case 'embeddedImages': {
                // check if value is array
                if (!is_array($value)) {
                    throw new \Exception("The attribute `$var` can only be set as an array.");
                }

                $this->fields[$var] = $value;
                break;
            }

            // the field is not valid
            default: {
                throw new \Exception("The field '$var' is not valid for the `".__CLASS__."` entity.");
            }
        }
    }

    /**
     * Filter save data before saving
     * @param array $saveData
     * @return array
     */
    protected function save_filterSaveData(Array $saveData) : array
    {
        // set data for insert operation
        if ($this->operation == 'insert') {
			$saveData['status'] = 'pending';
			$saveData['sendDatetime'] = $saveData['sendDatetime']?:DATETIME;

			if (key_exists('attachments', $saveData)) {
				$CFG = \Engine\Loader_Cfg::getInstance()->getGlobal();
				$queuePath = $CFG['paths']['emailsQueue'];
				$attachments = [];
				foreach ($saveData['attachments'] as $index => $attachment) {

					$newAttach = [
						'name'	=> $attachment['name'],
					];

					// save file to disk
					if (key_exists('fileContent', $attachment)) {
						$fileUniqueName = TIMESTAMP."-".uniqid()."-".$attachment['name']; // we add a unique id and timestamp to avoid name collisions
						\Engine\File::create(APP_REAL_PATH.$queuePath.$fileUniqueName, $attachment['fileContent']);
						$newAttach['file'] = $queuePath.$fileUniqueName;
						$newAttach['persistent'] = false;
					}
					else {
						$newAttach['file'] = $attachment['fileNameWithPath'];
						$newAttach['persistent'] = true;
					}

					$attachments[] = $newAttach;
				}
				$saveData['attachments'] = $attachments;
			}

			// Encode arrays as JSON before saving
			foreach (['to', 'cc', 'bcc', 'attachments', 'embeddedImages'] as $field) {
				if (key_exists($field, $saveData)) {
					$saveData[$field] = json_encode($saveData[$field]);
				}
			}
		}

		if ($this->operation == 'send') {
			$saveData['status'] = 'sent';
			$saveData['idUpdateUser'] = CURRENT_ID_USER;
			$saveData['lastUpdateTime'] = DATETIME;
			$saveData['updatesCount'] = 'updatesCount+1';;

		}

        return $saveData;
    }

	/**
     * Process operations and DB writes
	 * @param array $saveData
	 * @return void
	 * @throws \DBException
	 */
    protected function save_process(array $saveData) : void
    {
		if ($this->operation == 'purge') {
			$this->save_process_purge($saveData);
			return;
		}

        parent::save_process($saveData);
    }

	/**
	 * Delete email from queue completely
	 * @param array $saveData
	 * @return void
	 */
	private function save_process_purge(array $saveData) : void
	{
		if ($this->save_executeSql_delete($this->tableName, 'id', $this->id)) {
			$this->messages->addSuccessCode('delete_ok');
		}
		else {
			$this->messages->addErrorText('Could not purge the email from the queue.');
		}
	}


	/**
	 * @param array $saveData
	 * @return void
	 */
	protected function save_postProcess(array $saveData) : void
	{
		if ($this->operation == 'send') {
			$this->save_postProcess_send($saveData);
			return;
		}
		if ($this->operation == 'delete') {
			$this->save_postProcess_delete($saveData);
		}

		parent::save_postProcess($saveData);
	}

    /**
     * Send email using TinaMailer based on saved attributes
	 * @param array $saveData
	 * @return void
	 * @throws \App\Entity\Exception
	 * @throws \DBException
	 */
    private function save_postProcess_send(array $saveData) : void
    {
        // lock and read current row
        $email = \Entity\Email::selectForUpdate($this->id);

		try {
			// Prepare TinaMailer
			$mail = \TinaMailer::create();
			$mail->Subject = $email->subject;
			$mail->From = $email->fromEmail;
			if ($email->fromName) {
				$mail->FromName = $email->fromName;
			}

			if ($email->readReceipt) {
				$mail->ConfirmReadingTo = $email->fromEmail;
			}

			// for the recipients (to, cc, bcc), we can set a unidimensional array with the value
			// being the email address and the key being the name (if needed, optional)
			foreach ($email->to as $name => $addr) {
				if ($addr) {
					if (!is_numeric($name)) {
						$mail->addAddress($addr, $name);
					}
					else {
						$mail->addAddress($addr);
					}
				}
			}
			foreach ($email->cc as $name => $addr) {
				if ($addr) {
					if (!is_numeric($name)) {
						$mail->addCC($addr, $name);
					}
					else {
						$mail->addCC($addr);
					}
				}
			}

			foreach ($email->bcc as $name => $addr) {
				if ($addr) {
					if (!is_numeric($name)) {
						$mail->addBCC($addr, $name);
					}
					else {
						$mail->addBCC($addr);
					}
				}
			}

			// Embedded images

			foreach ($email->embeddedImages as $img) {
				$mail->addEmbeddedImage(APP_REAL_PATH.$img['file'], $img['cid'], (key_exists('name', $img)?$img['name']:''));
			}
			// Attachments
			foreach ($email->attachments as $att) {
				$mail->addAttachment(APP_REAL_PATH.$att['file'], $att['name']);
			}

			$mail->msgHTML($email->body);
			$mail->AltBody = strip_tags($email->body);

            $mail->send();

			$this->sendingOutput = "success";

        } catch (\PHPMailer\PHPMailer\Exception $e) {
            $msg = $e->getMessage();

			$this->messages->removeSuccess(); //  remove success message for the update
			\Log::getInstance()->warning(sprintf('[EmailsQueue] Received error while sending email [`id`: #%d] from queue. Error: %s', $this->id, $msg));

			// Check if this is an SMTP configuration error
			$smtpErrorPatterns = [
				'SMTP Error: Could not authenticate',
				'SMTP Error: Could not connect to SMTP host',
				'SMTP connect() failed',
				'Invalid host',
				'Invalid hostentry',
				'Extension missing: openssl',
				'Server does not support SMTPUTF8'
			];

			$this->sendingOutput = "error";
			foreach ($smtpErrorPatterns as $pattern) {
				if (stripos($msg, $pattern) !== false) {
					$this->sendingOutput = "smtpAuthError";
					break;
				}
			}

			// truncate the message if it's too long
			if (mb_strlen($msg) > 500) {
				$msg = mb_substr($msg, 0, 500);
			}

			// For SMTP config errors, keep status as pending, but set the flag
			if ($this->sendingOutput == "smtpAuthError") {
				if ($this->save_executeSql_update($this->tableName, ['status' => 'pending', 'errorMessage' => $msg], 'id', $this->id)) {
					$this->messages->addWarningText($msg); // add it as warning, because we do not want to rollback the update
				} else {
					$this->messages->addErrorText("Could not change the status of the email to pending after an SMTP error.");
				}
			} else {
				if ($this->save_executeSql_update($this->tableName, ['status' => 'error', 'errorMessage' => $msg], 'id', $this->id)) {
					$this->messages->addWarningText($msg); // add it as warning, because we do not want to rollback the update
				}
				else {
					$this->messages->addErrorText("Could not change the status of the email to error.");
				}
			}
        }
    }
    
    /**
     * Override the parent method to handle deletion of attachment files from disk
	 * @param array $saveData
	 * @return void
	 */
    protected function save_postProcess_delete(array $saveData) : void
    {
        // Get the email record before it's marked as deleted to retrieve its attachments
        $email = \Entity\Email::selectForUpdate($this->id);

        // Delete attachment files from disk that were stored in the internal queue
        if ($email->attachments) {
            foreach ($email->attachments as $attachment) {
                if (!$attachment['persistent'] and \Engine\File::check(APP_REAL_PATH.$attachment['file'])) {
                    // This attachment was stored in the internal queue, so delete the file
					if (!\Engine\File::delete(APP_REAL_PATH.$attachment['file'])) {
						$this->messages->removeSuccess();
						$this->messages->addErrorCode('delete_fail');
					}
                }
            }
        }
    }
}