<?php

namespace SilverStripe\UserForms\Model\Recipient;

use SilverStripe\Assets\FileFinder;
use SilverStripe\CMS\Controllers\CMSMain;
use SilverStripe\CMS\Controllers\CMSPageEditController;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Email\Email;
use SilverStripe\Core\Manifest\ModuleResource;
use SilverStripe\Core\Manifest\ModuleResourceLoader;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FieldGroup;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldButtonRow;
use SilverStripe\Forms\GridField\GridFieldConfig;
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
use SilverStripe\Forms\GridField\GridFieldToolbarHeader;
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\TextareaField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Member;
use SilverStripe\UserForms\Model\EditableFormField;
use SilverStripe\UserForms\Model\EditableFormField\EditableEmailField;
use SilverStripe\UserForms\Model\EditableFormField\EditableMultipleOptionField;
use SilverStripe\UserForms\Model\EditableFormField\EditableTextField;
use SilverStripe\UserForms\Model\UserDefinedForm;
use SilverStripe\UserForms\UserForm;
use SilverStripe\View\Requirements;
use Symbiote\GridFieldExtensions\GridFieldAddNewInlineButton;
use Symbiote\GridFieldExtensions\GridFieldEditableColumns;

/**
 * A Form can have multiply members / emails to email the submission
 * to and custom subjects
 *
 * @package userforms
 */
class EmailRecipient extends DataObject
{
    private static $db = [
        'EmailAddress' => 'Varchar(200)',
        'EmailSubject' => 'Varchar(200)',
        'EmailFrom' => 'Varchar(200)',
        'EmailReplyTo' => 'Varchar(200)',
        'EmailBody' => 'Text',
        'EmailBodyHtml' => 'HTMLText',
        'EmailTemplate' => 'Varchar',
        'SendPlain' => 'Boolean',
        'HideFormData' => 'Boolean',
        'CustomRulesCondition' => 'Enum("And,Or")'
    ];

    private static $has_one = [
        'Form' => DataObject::class,
        'SendEmailFromField' => EditableFormField::class,
        'SendEmailToField' => EditableFormField::class,
        'SendEmailSubjectField' => EditableFormField::class
    ];

    private static $has_many = [
        'CustomRules' => EmailRecipientCondition::class,
    ];

    private static $owns = [
        'CustomRules',
    ];

    private static $cascade_deletes = [
        'CustomRules',
    ];

    private static $summary_fields = [
        'EmailAddress',
        'EmailSubject',
        'EmailFrom'
    ];

    private static $table_name = 'UserDefinedForm_EmailRecipient';

    /**
     * Disable versioned GridField to ensure that it doesn't interfere with {@link UserFormRecipientItemRequest}
     *
     * {@inheritDoc}
     */
    private static $versioned_gridfield_extensions = false;

    /**
     * Setting this to true will allow you to select "risky" fields as
     * email recipient, such as free-text entry fields.
     *
     * It's advisable to leave this off.
     *
     * @config
     * @var bool
     */
    private static $allow_unbound_recipient_fields = false;

    public function requireDefaultRecords()
    {
        parent::requireDefaultRecords();

        // make sure to migrate the class across (prior to v5.x)
        DB::query("UPDATE \"UserDefinedForm_EmailRecipient\" SET \"FormClass\" = 'Page' WHERE \"FormClass\" IS NULL");
    }

    public function summaryFields()
    {
        $fields = parent::summaryFields();
        if (isset($fields['EmailAddress'])) {
            /** @skipUpgrade */
            $fields['EmailAddress'] = _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILADDRESS', 'Email');
        }
        if (isset($fields['EmailSubject'])) {
            $fields['EmailSubject'] = _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILSUBJECT', 'Subject');
        }
        if (isset($fields['EmailFrom'])) {
            $fields['EmailFrom'] = _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILFROM', 'From');
        }
        return $fields;
    }

    /**
     * Get instance of UserForm when editing in getCMSFields
     *
     * @return UserDefinedForm|UserForm|null
     */
    protected function getFormParent()
    {
        // If polymorphic relationship is actually defined, use it
        if ($this->FormID && $this->FormClass) {
            $formClass = $this->FormClass;
            return $formClass::get()->byID($this->FormID);
        }

        // Revert to checking for a form from the session
        // LeftAndMain::sessionNamespace is protected. @todo replace this with a non-deprecated equivalent.
        $sessionNamespace = $this->config()->get('session_namespace') ?: CMSMain::class;

        $formID = Controller::curr()->getRequest()->getSession()->get($sessionNamespace . '.currentPage');
        if ($formID) {
            return UserDefinedForm::get()->byID($formID);
        }
    }

    public function getTitle()
    {
        if ($this->EmailAddress) {
            return $this->EmailAddress;
        }
        if ($this->EmailSubject) {
            return $this->EmailSubject;
        }
        return parent::getTitle();
    }

    /**
     * Generate a gridfield config for editing filter rules
     *
     * @return GridFieldConfig
     */
    protected function getRulesConfig()
    {
        if (!$this->getFormParent()) {
            return null;
        }
        $formFields = $this->getFormParent()->Fields();

        $config = GridFieldConfig::create()
            ->addComponents(
                new GridFieldButtonRow('before'),
                new GridFieldToolbarHeader(),
                new GridFieldAddNewInlineButton(),
                new GridFieldDeleteAction(),
                $columns = new GridFieldEditableColumns()
            );

        $columns->setDisplayFields(array(
            'ConditionFieldID' => function ($record, $column, $grid) use ($formFields) {
                return DropdownField::create($column, false, $formFields->map('ID', 'Title'));
            },
            'ConditionOption' => function ($record, $column, $grid) {
                $options = EmailRecipientCondition::config()->condition_options;
                return DropdownField::create($column, false, $options);
            },
            'ConditionValue' => function ($record, $column, $grid) {
                return TextField::create($column);
            }
        ));

        return $config;
    }

    /**
     * @return FieldList
     */
    public function getCMSFields()
    {
        Requirements::javascript('silverstripe/userforms:client/dist/js/userforms-cms.js');

        // Build fieldlist
        $fields = FieldList::create(Tabset::create('Root')->addExtraClass('EmailRecipientForm'));

        if (!$this->getFormParent()) {
            $fields->addFieldToTab('Root.EmailDetails', $this->getUnsavedFormLiteralField());
        }

        // Configuration fields
        $fields->addFieldsToTab('Root.EmailDetails', [
            $this->getSubjectCMSFields(),
            $this->getEmailToCMSFields(),
            $this->getEmailFromCMSFields(),
            $this->getEmailReplyToCMSFields(),
        ]);

        $fields->fieldByName('Root.EmailDetails')->setTitle(_t(__CLASS__ . '.EMAILDETAILSTAB', 'Email Details'));

        // Only show the preview link if the recipient has been saved.
        if (!empty($this->EmailTemplate)) {
            $pageEditController = singleton(CMSPageEditController::class);
            $pageEditController
                ->getRequest()
                ->setSession(Controller::curr()->getRequest()->getSession());

            // If used in a regular page context, will have "/edit" on the end, if used in a trait context
            // it won't. Strip that off in case.
            $currentUrl = Controller::curr()->getRequest()->getURL();
            if (substr($currentUrl, -5) === '/edit') {
                $currentUrl = substr($currentUrl, 0, strlen($currentUrl) - 5);
            }
            $previewUrl = Controller::join_links($currentUrl, 'preview');

            $preview = sprintf(
                '<p><a href="%s" target="_blank" class="btn btn-outline-secondary">%s</a></p><em>%s</em>',
                $previewUrl,
                _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.PREVIEW_EMAIL', 'Preview email'),
                _t(
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.PREVIEW_EMAIL_DESCRIPTION',
                    'Note: Unsaved changes will not appear in the preview.'
                )
            );
        } else {
            $preview = sprintf(
                '<p class="alert alert-warning">%s</p>',
                _t(
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.PREVIEW_EMAIL_UNAVAILABLE',
                    'You can preview this email once you have saved the Recipient.'
                )
            );
        }

        // Email templates
        $fields->addFieldsToTab('Root.EmailContent', [
            CheckboxField::create(
                'HideFormData',
                _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.HIDEFORMDATA', 'Hide form data from email?')
            ),
            CheckboxField::create(
                'SendPlain',
                _t(
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDPLAIN',
                    'Send email as plain text? (HTML will be stripped)'
                )
            ),
            HTMLEditorField::create(
                'EmailBodyHtml',
                _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILBODYHTML', 'Body')
            )
                ->addExtraClass('toggle-html-only'),
            TextareaField::create(
                'EmailBody',
                _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILBODY', 'Body')
            )
                ->addExtraClass('toggle-plain-only'),
            LiteralField::create('EmailPreview', $preview)
        ]);

        $templates = $this->getEmailTemplateDropdownValues();

        if ($templates) {
            $fields->insertBefore(
                DropdownField::create(
                    'EmailTemplate',
                    _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILTEMPLATE', 'Email template'),
                    $templates
                )->addExtraClass('toggle-html-only'),
                'EmailBodyHtml'
            );
        }

        $fields->fieldByName('Root.EmailContent')->setTitle(_t(__CLASS__ . '.EMAILCONTENTTAB', 'Email Content'));

        // Custom rules for sending this field
        $grid = GridField::create(
            'CustomRules',
            _t('SilverStripe\\UserForms\\Model\\EditableFormField.CUSTOMRULES', 'Custom Rules'),
            $this->CustomRules(),
            $this->getRulesConfig()
        );
        $grid->setDescription(_t(
            'SilverStripe\\UserForms\\Model\\UserDefinedForm.RulesDescription',
            'Emails will only be sent to the recipient if the custom rules are met. If no rules are defined, '
            . 'this recipient will receive notifications for every submission.'
        ));

        $fields->addFieldsToTab('Root.CustomRules', [
            DropdownField::create(
                'CustomRulesCondition',
                _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIF', 'Send condition'),
                [
                    'Or' => _t(
                        'SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIFOR',
                        'Any conditions are true'
                    ),
                    'And' => _t(
                        'SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIFAND',
                        'All conditions are true'
                    )
                ]
            ),
            $grid
        ]);

        $fields->fieldByName('Root.CustomRules')->setTitle(_t(__CLASS__ . '.CUSTOMRULESTAB', 'Custom Rules'));

        $this->extend('updateCMSFields', $fields);
        return $fields;
    }

    /**
     * Return whether a user can create an object of this type
     *
     * @param Member $member
     * @param array $context Virtual parameter to allow context to be passed in to check
     * @return bool
     */
    public function canCreate($member = null, $context = [])
    {
        // Check parent page
        $parent = $this->getCanCreateContext(func_get_args());
        if ($parent) {
            return $parent->canEdit($member);
        }

        // Fall back to secure admin permissions
        return parent::canCreate($member);
    }

    /**
     * Helper method to check the parent for this object
     *
     * @param array $args List of arguments passed to canCreate
     * @return SiteTree Parent page instance
     */
    protected function getCanCreateContext($args)
    {
        // Inspect second parameter to canCreate for a 'Parent' context
        if (isset($args[1][Form::class])) {
            return $args[1][Form::class];
        }
        // Hack in currently edited page if context is missing
        if (Controller::has_curr() && Controller::curr() instanceof CMSMain) {
            return Controller::curr()->currentPage();
        }

        // No page being edited
        return null;
    }

    public function canView($member = null)
    {
        if ($form = $this->getFormParent()) {
            return $form->canView($member);
        }
        return parent::canView($member);
    }

    public function canEdit($member = null)
    {
        if ($form = $this->getFormParent()) {
            return $form->canEdit($member);
        }

        return parent::canEdit($member);
    }

    /**
     * @param Member
     *
     * @return boolean
     */
    public function canDelete($member = null)
    {
        return $this->canEdit($member);
    }

    /**
     * Determine if this recipient may receive notifications for this submission
     *
     * @param array $data
     * @param Form $form
     * @return bool
     */
    public function canSend($data, $form)
    {
        // Skip if no rules configured
        $customRules = $this->CustomRules();
        if (!$customRules->count()) {
            return true;
        }

        // Check all rules
        $isAnd = $this->CustomRulesCondition === 'And';
        foreach ($customRules as $customRule) {
            /** @var EmailRecipientCondition  $customRule */
            $matches = $customRule->matches($data);
            if ($isAnd && !$matches) {
                return false;
            }
            if (!$isAnd && $matches) {
                return true;
            }
        }

        // Once all rules are checked
        return $isAnd;
    }

    /**
     * Make sure the email template saved against the recipient exists on the file system.
     *
     * @param string
     *
     * @return boolean
     */
    public function emailTemplateExists($template = '')
    {
        $t = ($template ? $template : $this->EmailTemplate);

        return array_key_exists($t, (array) $this->getEmailTemplateDropdownValues());
    }

    /**
     * Get the email body for the current email format
     *
     * @return string
     */
    public function getEmailBodyContent()
    {
        if ($this->SendPlain) {
            return DBField::create_field('HTMLText', $this->EmailBody)->Plain();
        }
        return DBField::create_field('HTMLText', $this->EmailBodyHtml);
    }

    /**
     * Gets a list of email templates suitable for populating the email template dropdown.
     *
     * @return array
     */
    public function getEmailTemplateDropdownValues()
    {
        $templates = [];

        $finder = new FileFinder();
        $finder->setOption('name_regex', '/^.*\.ss$/');

        $parent = $this->getFormParent();

        if (!$parent) {
            return [];
        }

        $emailTemplateDirectory = $parent->config()->get('email_template_directory');
        $templateDirectory = ModuleResourceLoader::resourcePath($emailTemplateDirectory);

        if (!$templateDirectory) {
            return [];
        }

        $found = $finder->find(BASE_PATH . DIRECTORY_SEPARATOR . $templateDirectory);

        foreach ($found as $key => $value) {
            $template = pathinfo($value);
            $absoluteFilename = $template['dirname'] . DIRECTORY_SEPARATOR . $template['filename'];

            // Optionally remove vendor/ path prefixes
            $resource = ModuleResourceLoader::singleton()->resolveResource($emailTemplateDirectory);
            if ($resource instanceof ModuleResource && $resource->getModule()) {
                $prefixToStrip = $resource->getModule()->getPath();
            } else {
                $prefixToStrip = BASE_PATH;
            }
            $templatePath = substr($absoluteFilename, strlen($prefixToStrip) + 1);

            // Optionally remove "templates/" prefixes
            if (preg_match('/(?<=templates\/).*$/', $templatePath, $matches)) {
                $templatePath = $matches[0];
            }

            $templates[$templatePath] = $template['filename'];
        }

        return $templates;
    }

    /**
     * Validate that valid email addresses are being used
     *
     * @return ValidationResult
     */
    public function validate()
    {
        $result = parent::validate();
        $checkEmail = [
            'EmailAddress' => 'EMAILADDRESSINVALID',
            'EmailFrom' => 'EMAILFROMINVALID',
            'EmailReplyTo' => 'EMAILREPLYTOINVALID',
        ];
        foreach ($checkEmail as $check => $translation) {
            if ($this->$check) {
                //may be a comma separated list of emails
                $addresses = explode(',', $this->$check);
                foreach ($addresses as $address) {
                    $trimAddress = trim($address);
                    if ($trimAddress && !Email::is_valid_address($trimAddress)) {
                        $error = _t(
                            __CLASS__.".$translation",
                            "Invalid email address $trimAddress"
                        );
                        $result->addError($error . " ($trimAddress)");
                    }
                }
            }
        }

        // if there is no from address and no fallback, you'll have errors if this isn't defined
        if (!$this->EmailFrom && empty(Email::getSendAllEmailsFrom()) && empty(Email::config()->get('admin_email'))) {
            $result->addError(_t(__CLASS__.".EMAILFROMREQUIRED", '"Email From" address is required'));
        }
        return $result;
    }

    /**
     * @return FieldGroup|TextField
     */
    protected function getSubjectCMSFields()
    {
        $subjectTextField = TextField::create(
            'EmailSubject',
            _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.TYPESUBJECT', 'Type subject')
        )
            ->setAttribute('style', 'min-width: 400px;');

        if ($this->getFormParent() && $this->getValidSubjectFields()) {
            return FieldGroup::create(
                $subjectTextField,
                DropdownField::create(
                    'SendEmailSubjectFieldID',
                    _t(
                        'SilverStripe\\UserForms\\Model\\UserDefinedForm.SELECTAFIELDTOSETSUBJECT',
                        '.. or select a field to use as the subject'
                    ),
                    $this->getValidSubjectFields()->map('ID', 'Title')
                )->setEmptyString('')
            )
                ->setTitle(_t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILSUBJECT', 'Email subject'));
        } else {
            return $subjectTextField;
        }
    }

    /**
     * @return FieldGroup|TextField
     */
    protected function getEmailToCMSFields()
    {
        $emailToTextField = TextField::create(
            'EmailAddress',
            _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.TYPETO', 'Type to address')
        )
            ->setAttribute('style', 'min-width: 400px;');

        if ($this->getFormParent() && $this->getValidEmailToFields()) {
            return FieldGroup::create(
                $emailToTextField,
                DropdownField::create(
                    'SendEmailToFieldID',
                    _t(
                        'SilverStripe\\UserForms\\Model\\UserDefinedForm.ORSELECTAFIELDTOUSEASTO',
                        '.. or select a field to use as the to address'
                    ),
                    $this->getValidEmailToFields()->map('ID', 'Title')
                )->setEmptyString(' ')
            )
                ->setTitle(_t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDEMAILTO', 'Send email to'))
                ->setDescription(_t(
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDEMAILTO_DESCRIPTION',
                    'You may enter multiple email addresses as a comma separated list.'
                ));
        } else {
            return $emailToTextField;
        }
    }

    /**
     * @return TextField
     */
    protected function getEmailFromCMSFields()
    {
        return TextField::create(
            'EmailFrom',
            _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.FROMADDRESS', 'Send email from')
        )
            ->setDescription(_t(
                'SilverStripe\\UserForms\\Model\\UserDefinedForm.EmailFromContent',
                "The from address allows you to set who the email comes from. On most servers this " .
                "will need to be set to an email address on the same domain name as your site. " .
                "For example on yoursite.com the from address may need to be something@yoursite.com. " .
                "You can however, set any email address you wish as the reply to address."
            ));
    }

    /**
     * @return FieldGroup|TextField
     */
    protected function getEmailReplyToCMSFields()
    {
        $replyToTextField = TextField::create('EmailReplyTo', _t(
            'SilverStripe\\UserForms\\Model\\UserDefinedForm.TYPEREPLY',
            'Type reply address'
        ))
            ->setAttribute('style', 'min-width: 400px;');
        if ($this->getFormParent() && $this->getValidEmailFromFields()) {
            return FieldGroup::create(
                $replyToTextField,
                DropdownField::create(
                    'SendEmailFromFieldID',
                    _t(
                        'SilverStripe\\UserForms\\Model\\UserDefinedForm.ORSELECTAFIELDTOUSEASFROM',
                        '.. or select a field to use as reply to address'
                    ),
                    $this->getValidEmailFromFields()->map('ID', 'Title')
                )->setEmptyString(' ')
            )
                ->setTitle(_t(
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.REPLYADDRESS',
                    'Email for reply to'
                ))
                ->setDescription(_t(
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.REPLYADDRESS_DESCRIPTION',
                    'The email address which the recipient is able to \'reply\' to.'
                ));
        } else {
            return $replyToTextField;
        }
    }

    /**
     * @return DataList|null
     */
    protected function getMultiOptionFields()
    {
        if (!$form = $this->getFormParent()) {
            return null;
        }
        return EditableMultipleOptionField::get()->filter('ParentID', $form->ID);
    }

    /**
     * @return ArrayList|null
     */
    protected function getValidSubjectFields()
    {
        if (!$form = $this->getFormParent()) {
            return null;
        }
        // For the subject, only one-line entry boxes make sense
        $validSubjectFields = ArrayList::create(
            EditableTextField::get()
                ->filter('ParentID', $form->ID)
                ->exclude('Rows:GreaterThan', 1)
                ->toArray()
        );
        $validSubjectFields->merge($this->getMultiOptionFields());
        return $validSubjectFields;
    }

    /**
     * @return DataList|null
     */
    protected function getValidEmailFromFields()
    {
        if (!$form = $this->getFormParent()) {
            return null;
        }

        // if they have email fields then we could send from it
        return EditableEmailField::get()->filter('ParentID', $form->ID);
    }

    /**
     * @return ArrayList|DataList|null
     */
    protected function getValidEmailToFields()
    {
        if (!$this->getFormParent()) {
            return null;
        }

        // Check valid email-recipient fields
        if ($this->config()->get('allow_unbound_recipient_fields')) {
            // To address can only be email fields or multi option fields
            $validEmailToFields = ArrayList::create($this->getValidEmailFromFields()->toArray());
            $validEmailToFields->merge($this->getMultiOptionFields());
            return $validEmailToFields;
        } else {
            // To address cannot be unbound, so restrict to pre-defined lists
            return $this->getMultiOptionFields();
        }
    }

    protected function getUnsavedFormLiteralField()
    {
        return LiteralField::create(
            'UnsavedFormMessage',
            sprintf(
                '<p class="alert alert-warning">%s</p>',
                _t(
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAIL_RECIPIENT_UNSAVED_FORM',
                    'You will be able to select from valid form fields after saving this record.'
                )
            )
        );
    }
}