<?php

declare(strict_types=1);

/**
 * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

namespace OCA\Mail\Service\MailFilter;

use OCA\Mail\Exception\ImapFlagEncodingException;
use OCA\Mail\IMAP\ImapFlag;
use OCA\Mail\Sieve\SieveUtils;

class FilterBuilder {
	private const SEPARATOR = '### Nextcloud Mail: Filters ### DON\'T EDIT ###';
	private const DATA_MARKER = '# FILTER: ';
	/** @deprecated use SieveUtils::NEWLINE */
	private const SIEVE_NEWLINE = "\r\n";

	public function __construct(
		private ImapFlag $imapFlag,
	) {
	}

	public function buildSieveScript(array $filters, string $untouchedScript): string {
		$commands = [];
		$extensions = [];

		foreach ($filters as $filter) {
			if ($filter['enable'] === false) {
				continue;
			}

			$commands[] = '# ' . $filter['name'];

			$tests = [];
			foreach ($filter['tests'] as $test) {
				if ($test['field'] === 'subject') {
					$tests[] = sprintf(
						'header :%s "Subject" %s',
						$test['operator'],
						SieveUtils::stringList($test['values']),
					);
				}
				if ($test['field'] === 'to') {
					$tests[] = sprintf(
						'address :%s :all "To" %s',
						$test['operator'],
						SieveUtils::stringList($test['values']),
					);
				}
				if ($test['field'] === 'from') {
					$tests[] = sprintf(
						'address :%s :all "From" %s',
						$test['operator'],
						SieveUtils::stringList($test['values']),
					);
				}
			}

			if (count($tests) === 0) {
				// skip filter without tests
				$commands[] = '# No valid tests found';
				continue;
			}

			$actions = [];
			foreach ($filter['actions'] as $action) {
				if ($action['type'] === 'fileinto') {
					$extensions[] = 'fileinto';
					$actions[] = sprintf(
						'fileinto "%s";',
						SieveUtils::escapeString($action['mailbox'])
					);
				}
				if ($action['type'] === 'addflag' || $action['type'] === 'addsystemflag') {
					$extensions[] = 'imap4flags';
					if ($this->isSystemFlag($action['flag'])) {
						$flag = SieveUtils::escapeString($action['flag']);
					} else {
						$flag = SieveUtils::escapeString($this->sanitizeFlag($action['flag']));
					}
					$actions[] = sprintf('addflag "%s";', $flag);
				}
				if ($action['type'] === 'keep') {
					$actions[] = 'keep;';
				}
				if ($action['type'] === 'stop') {
					$actions[] = 'stop;';
				}
			}

			if (count($tests) > 1) {
				$ifTest = sprintf('%s (%s)', $filter['operator'], implode(', ', $tests));
			} else {
				$ifTest = $tests[0];
			}

			$actions = array_map(
				static fn ($action) => "\t" . $action,
				$actions
			);

			$ifBlock = sprintf(
				"if %s {\r\n%s\r\n}",
				$ifTest,
				implode(self::SIEVE_NEWLINE, $actions)
			);

			$commands[] = $ifBlock;
		}

		$lines = [];

		$extensions = array_unique($extensions);
		if (count($extensions) > 0) {
			$lines[] = self::SEPARATOR;
			$lines[] = 'require ' . SieveUtils::stringList($extensions) . ';';
			$lines[] = self::SEPARATOR;
		}

		/*
		 * Using implode("\r\n", $lines) may introduce an extra newline if the original script already ends with one.
		 * There may be a cleaner solution, but I couldn't find one that works seamlessly with Filter and Autoresponder.
		 * Feel free to give it a try!
		 */
		if (str_ends_with($untouchedScript, self::SIEVE_NEWLINE . self::SIEVE_NEWLINE)) {
			$untouchedScript = substr($untouchedScript, 0, -2);
		}
		$lines[] = $untouchedScript;

		if (count($filters) > 0) {
			$lines[] = self::SEPARATOR;
			$lines[] = self::DATA_MARKER . json_encode($this->sanitizeDefinition($filters), JSON_THROW_ON_ERROR);
			array_push($lines, ...$commands);
			$lines[] = self::SEPARATOR;
		}

		return implode(self::SIEVE_NEWLINE, $lines);
	}

	private function sanitizeFlag(string $flag): string {
		try {
			return $this->imapFlag->create($flag);
		} catch (ImapFlagEncodingException) {
			return 'placeholder_for_invalid_label';
		}
	}

	private function sanitizeDefinition(array $filters): array {
		return array_map(static function ($filter) {
			unset($filter['accountId'], $filter['id']);
			$filter['tests'] = array_map(static function ($test) {
				unset($test['id']);
				return $test;
			}, $filter['tests']);
			$filter['actions'] = array_map(static function ($action) {
				unset($action['id']);
				return $action;
			}, $filter['actions']);
			$filter['priority'] = (int)$filter['priority'];
			return $filter;
		}, $filters);
	}

	private function isSystemFlag(string $flag): bool {
		$flags = [
			\Horde_Imap_Client::FLAG_ANSWERED,
			\Horde_Imap_Client::FLAG_DELETED,
			\Horde_Imap_Client::FLAG_DRAFT,
			\Horde_Imap_Client::FLAG_FLAGGED,
			\Horde_Imap_Client::FLAG_RECENT,
			\Horde_Imap_Client::FLAG_SEEN,
		];

		// Check is done in lowercase to keep the notation from the RFC (e.g. \Seen)
		return in_array(strtolower($flag), $flags, true);
	}
}
