<?php

// SPDX-FileCopyrightText: 2023 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later

namespace Icinga\Module\Notifications\Web\Form;

use DateTime;
use Icinga\Exception\Http\HttpNotFoundException;
use Icinga\Module\Notifications\Model\AvailableChannelType;
use Icinga\Module\Notifications\Model\Channel;
use Icinga\Module\Notifications\Model\Contact;
use Icinga\Module\Notifications\Model\Rotation;
use Icinga\Module\Notifications\Model\RotationMember;
use Icinga\Module\Notifications\Model\RuleEscalationRecipient;
use Icinga\Web\Session;
use ipl\Html\Attributes;
use ipl\Html\Contract\FormSubmitElement;
use ipl\Html\FormDecoration\DescriptionDecorator;
use ipl\Html\HtmlDocument;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Sql\Connection;
use ipl\Stdlib\Filter;
use ipl\Validator\CallbackValidator;
use ipl\Validator\EmailAddressValidator;
use ipl\Validator\StringLengthValidator;
use ipl\Web\Common\CsrfCounterMeasure;
use ipl\Web\Compat\CompatForm;
use ipl\Web\Url;
use Ramsey\Uuid\Uuid;

class ContactForm extends CompatForm
{
    use CsrfCounterMeasure;

    /** @var string Emitted in case the contact should be deleted */
    public const ON_REMOVE = 'on_remove';

    private Connection $db;

    /** @var ?string Contact ID */
    private ?string $contactId = null;

    public function __construct(Connection $db)
    {
        $this->db = $db;
        $this->applyDefaultElementDecorators();

        $this->on(self::ON_SENT, function () {
            if ($this->hasBeenRemoved()) {
                $this->emit(self::ON_REMOVE, [$this]);
            }
        });
    }

    /**
     * Get whether the user pushed the remove button
     *
     * @return bool
     */
    private function hasBeenRemoved(): bool
    {
        $btn = $this->getPressedSubmitElement();
        $csrf = $this->getElement('CSRFToken');

        return $csrf !== null && $csrf->isValid() && $btn !== null && $btn->getName() === 'delete';
    }

    public function isValidEvent($event): bool
    {
        if ($event === self::ON_REMOVE) {
            return true;
        }

        return parent::isValidEvent($event);
    }

    protected function assemble(): void
    {
        $this->addAttributes(Attributes::create(['class' => 'contact-form']));
        $this->addCsrfCounterMeasure(Session::getSession()->getId());

        // Fieldset for contact full name and username
        $this->addElement('fieldset', 'contact', ['label' => $this->translate('Contact')]);
        $contact = $this->getElement('contact');

        $contact->addElement(
            'text',
            'full_name',
            [
                'label' => $this->translate('Contact Name'),
                'required' => true
            ]
        );

        // TODO: remove this once https://github.com/Icinga/ipl-html/issues/178 is fixed
        $contact->addElementLoader('ipl\\Web\\FormElement', 'Element');

        $contact->addElement(
            'suggestion',
            'username',
            [
                'label' => $this->translate('Icinga Web User'),
                'description' => $this->translate(
                    'Use this to associate actions in the UI, such as incident management, with this contact.'
                    . ' To successfully receive desktop notifications, this is also required.'
                ),
                'suggestionsUrl' => Url::fromPath(
                    'notifications/contact/suggest-icinga-web-user',
                    ['showCompact' => true, '_disableLayout' => 1]
                ),
                'validators' => [
                    new StringLengthValidator(['max' => 254]),
                    new CallbackValidator(function ($value, $validator) {
                        $contact = Contact::on($this->db)
                            ->filter(Filter::equal('username', $value));
                        if ($this->contactId) {
                            $contact->filter(Filter::unequal('id', $this->contactId));
                        }

                        if ($contact->first() !== null) {
                            $validator->addMessage($this->translate(
                                'A contact with the same username already exists.'
                            ));

                            return false;
                        }

                        return true;
                    })
                ]
            ]
        );
        $contact
            ->getElement('username')
            ->getDecorators()
            ->replaceDecorator('Description', DescriptionDecorator::class, ['class' => 'description']);

        $channelQuery = Channel::on($this->db)
            ->columns(['id', 'name', 'type']);

        $availableTypes = $this->db->fetchPairs(
            AvailableChannelType::on($this->db)->columns(['type', 'name'])->assembleSelect()
        );

        $channelNames = [];
        $channelTypes = [];
        foreach ($channelQuery as $channel) {
            $channelNames[$availableTypes[$channel->type]][$channel->id] = $channel->name;
            $channelTypes[$channel->id] = $channel->type;
        }

        $defaultChannel = $this->createElement(
            'select',
            'default_channel_id',
            [
                'label'             => $this->translate('Default Channel'),
                'description'       => $this->translate(
                    "Contact will be notified via the default channel, when no specific channel is configured"
                    . " in an event rule."
                ),
                'required'          => true,
                'class'             => 'autosubmit',
                'disabledOptions'   => [''],
                'options'           => [
                    '' => sprintf(' - %s - ', $this->translate('Please choose'))
                ] + $channelNames,
            ]
        );

        $defaultChannel
            ->getDecorators()
            ->replaceDecorator('Description', DescriptionDecorator::class, ['class' => 'description']);
        $this->decorate($defaultChannel);

        $contact->registerElement($defaultChannel);

        $this->addAddressElements($availableTypes, $channelTypes[$defaultChannel->getValue() ?? ''] ?? null);

        $this->addHtml(new HtmlElement('hr'));

        $this->addHtml($defaultChannel);

        $this->addElement(
            'submit',
            'submit',
            [
                'label' => $this->contactId === null ?
                    $this->translate('Create Contact') :
                    $this->translate('Save Changes')
            ]
        );
        if ($this->contactId !== null) {
            /** @var FormSubmitElement $deleteButton */
            $deleteButton = $this->createElement(
                'submit',
                'delete',
                [
                    'label'          => $this->translate('Delete Contact'),
                    'class'          => 'btn-remove',
                    'formnovalidate' => true
                ]
            );

            $this->registerElement($deleteButton);
            $this->getElement('submit')->prependWrapper((new HtmlDocument())->addHtml($deleteButton));
        }
    }

    /**
     * Load the contact with given id
     *
     * @param int $id
     *
     * @return $this
     *
     * @throws HttpNotFoundException
     */
    public function loadContact(int $id): static
    {
        $this->contactId = $id;

        $this->populate($this->fetchDbValues());

        return $this;
    }

    /**
     * Add the new contact
     */
    public function addContact(): void
    {
        $contactInfo = $this->getValues();
        $changedAt = (int) (new DateTime())->format("Uv");
        $this->db->beginTransaction();
        $this->db->insert(
            'contact',
            $contactInfo['contact'] + ['changed_at' => $changedAt, 'external_uuid' => Uuid::uuid4()->toString()]
        );
        $this->contactId = $this->db->lastInsertId();

        foreach (array_filter($contactInfo['contact_address']) as $type => $address) {
            $address = [
                'contact_id' => $this->contactId,
                'type'       => $type,
                'address'    => $address,
                'changed_at' => $changedAt
            ];

            $this->db->insert('contact_address', $address);
        }

        $this->db->commitTransaction();
    }

    /**
     * Edit the contact
     *
     * @return void
     */
    public function editContact(): void
    {
        $this->db->beginTransaction();

        $values = $this->getValues();
        $storedValues = $this->fetchDbValues();

        $changedAt = (int) (new DateTime())->format("Uv");
        if ($storedValues['contact'] !== $values['contact']) {
            $this->db->update(
                'contact',
                $values['contact'] + ['changed_at' => $changedAt],
                ['id = ?' => $this->contactId]
            );
        }

        $storedAddresses = $storedValues['contact_address_with_id'];
        foreach ($values['contact_address'] as $type => $address) {
            if ($address === null) {
                if (isset($storedAddresses[$type])) {
                    $this->db->update(
                        'contact_address',
                        ['changed_at' => $changedAt, 'deleted' => 'y'],
                        ['id = ?' => $storedAddresses[$type][0], 'deleted = ?' => 'n']
                    );
                }
            } elseif (! isset($storedAddresses[$type])) {
                $address = [
                    'contact_id' => $this->contactId,
                    'type'       => $type,
                    'address'    => $address,
                    'changed_at' => $changedAt
                ];

                $this->db->insert('contact_address', $address);
            } elseif ($storedAddresses[$type][1] !== $address) {
                $this->db->update(
                    'contact_address',
                    ['address' => $address, 'changed_at' => $changedAt],
                    [
                        'id = ?'         => $storedAddresses[$type][0],
                        'contact_id = ?' => $this->contactId
                    ]
                );
            }
        }

        $this->db->commitTransaction();
    }

    /**
     * Remove the contact
     */
    public function removeContact(): void
    {
        $this->db->beginTransaction();

        $markAsDeleted = ['changed_at' => (int) (new DateTime())->format("Uv"), 'deleted' => 'y'];
        $updateCondition = ['contact_id = ?' => $this->contactId, 'deleted = ?' => 'n'];

        $rotationAndMemberIds = $this->db->fetchPairs(
            RotationMember::on($this->db)
                ->columns(['id', 'rotation_id'])
                ->filter(Filter::equal('contact_id', $this->contactId))
                ->assembleSelect()
        );

        $rotationMemberIds = array_keys($rotationAndMemberIds);
        $rotationIds = array_values($rotationAndMemberIds);

        $this->db->update('rotation_member', $markAsDeleted + ['position' => null], $updateCondition);

        if (! empty($rotationMemberIds)) {
            $this->db->update(
                'timeperiod_entry',
                $markAsDeleted,
                ['rotation_member_id IN (?)' => $rotationMemberIds, 'deleted = ?' => 'n']
            );
        }

        if (! empty($rotationIds)) {
            $rotationIdsWithOtherMembers = $this->db->fetchCol(
                RotationMember::on($this->db)
                    ->columns('rotation_id')
                    ->filter(
                        Filter::all(
                            Filter::equal('rotation_id', $rotationIds),
                            Filter::unequal('contact_id', $this->contactId)
                        )
                    )->assembleSelect()
            );

            $toRemoveRotations = array_diff($rotationIds, $rotationIdsWithOtherMembers);

            if (! empty($toRemoveRotations)) {
                $rotations = Rotation::on($this->db)
                    ->columns(['id', 'schedule_id', 'priority', 'timeperiod.id'])
                    ->filter(Filter::equal('id', $toRemoveRotations));

                /** @var Rotation $rotation */
                foreach ($rotations as $rotation) {
                    $rotation->deleteRotation();
                }
            }
        }

        $escalationIds = $this->db->fetchCol(
            RuleEscalationRecipient::on($this->db)
                ->columns('rule_escalation_id')
                ->filter(Filter::equal('contact_id', $this->contactId))
                ->assembleSelect()
        );

        $this->db->update('rule_escalation_recipient', $markAsDeleted, $updateCondition);

        if (! empty($escalationIds)) {
            $escalationIdsWithOtherRecipients = $this->db->fetchCol(
                RuleEscalationRecipient::on($this->db)
                    ->columns('rule_escalation_id')
                    ->filter(Filter::all(
                        Filter::equal('rule_escalation_id', $escalationIds),
                        Filter::unequal('contact_id', $this->contactId)
                    ))->assembleSelect()
            );

            $toRemoveEscalations = array_diff($escalationIds, $escalationIdsWithOtherRecipients);

            if (! empty($toRemoveEscalations)) {
                $this->db->update(
                    'rule_escalation',
                    $markAsDeleted + ['position' => null],
                    ['id IN (?)' => $toRemoveEscalations]
                );
            }
        }

        $this->db->update('contactgroup_member', $markAsDeleted, $updateCondition);
        $this->db->update('contact_address', $markAsDeleted, $updateCondition);

        $this->db->update('contact', $markAsDeleted + ['username' => null], ['id = ?' => $this->contactId]);

        $this->db->commitTransaction();
    }

    /**
     * Get the contact name
     *
     * @return string
     */
    public function getContactName(): string
    {
        return $this->getElement('contact')->getValue('full_name');
    }

    /**
     * Fetch the values from the database
     *
     * @return array
     *
     * @throws HttpNotFoundException
     */
    private function fetchDbValues(): array
    {
        /** @var ?Contact $contact */
        $contact = Contact::on($this->db)
            ->filter(Filter::equal('id', $this->contactId))
            ->first();

        if ($contact === null) {
            throw new HttpNotFoundException(t('Contact not found'));
        }

        $values['contact'] = [
            'full_name'          => $contact->full_name,
            'username'           => $contact->username,
            'default_channel_id' => (string) $contact->default_channel_id
        ];

        $values['contact_address'] = [];
        $values['contact_address_with_id'] = []; //TODO: only used in editContact(), find better solution
        foreach ($contact->contact_address as $contactInfo) {
            $values['contact_address'][$contactInfo->type] = $contactInfo->address;
            $values['contact_address_with_id'][$contactInfo->type] = [$contactInfo->id, $contactInfo->address];
        }

        return $values;
    }

    /**
     * Add address elements for all existing channel plugins
     *
     * @param array<string, string> $availableChannelTypes The available channel types as `type` => `name` pair
     * @param ?string $defaultType The selected default channel type
     *
     * @return void
     */
    private function addAddressElements(array $availableChannelTypes, ?string $defaultType): void
    {
        if (empty($availableChannelTypes)) {
            return;
        }

        $address = $this->createElement('fieldset', 'contact_address', ['label' => $this->translate('Channels')]);
        $this->addElement($address);

        $address->addHtml(new HtmlElement(
            'p',
            new Attributes(['class' => 'description']),
            new Text($this->translate('Configure the channels available for this contact here.'))
        ));

        foreach ($availableChannelTypes as $type => $name) {
            $element = $this->createElement('text', $type, [
                'label'      => $name,
                'validators' => [new StringLengthValidator(['max' => 255])],
                'required'   => $type === $defaultType && $type !== 'webhook'
            ]);

            if ($type === 'email') {
                $element->addAttributes(['validators' => [new EmailAddressValidator()]]);
            }

            $address->addElement($element);
        }
    }
}
