<?php

namespace Give\DonationForms\DataTransferObjects;

use Give\DonationForms\Exceptions\DonationFormFieldErrorsException;
use Give\DonationForms\Exceptions\DonationFormForbidden;
use Give\DonationForms\Models\DonationForm;
use Give\Framework\FieldsAPI\Actions\CreateValidatorFromFormFields;
use Give\Framework\FieldsAPI\Exceptions\NameCollisionException;
use Give\Framework\FieldsAPI\Field;
use Give\Framework\FieldsAPI\SecurityChallenge;
use Give\Framework\Http\Response\Types\JsonResponse;
use Give\Framework\Support\Contracts\Arrayable;
use Give\Framework\Permissions\Facades\UserPermissions;
use WP_Error;

/**
 * @since 3.0.0
 */
class ValidationRouteData implements Arrayable
{
    /**
     * @var array
     */
    private $requestData;
    /**
     * @var int
     */
    public $formId;

    /**
     * Convert data from request into DTO
     *
     * @since 3.0.0
     */
    public static function fromRequest(array $requestData): self
    {
        $self = new self();
        $self->formId = (int)$requestData['formId'];
        $self->requestData = $requestData;

        return $self;
    }

    /**
     * This method loops over the form schema to
     * compares the request against the individual fields,
     * their types and validation rules.
     *
     * @since 4.1.0 updated to exclude security challenge fields during pre-validation
     * @since 3.22.0 added additional validation for form validity, added givewp_donation_form_fields_validated action
     * @since 3.0.0
     *
     * @throws DonationFormFieldErrorsException|NameCollisionException|DonationFormForbidden
     */
    public function validate(): JsonResponse
    {
        $request = $this->getRequestData();

        /** @var DonationForm $form */
        $form = DonationForm::find($this->formId);

        if (!$form || !$this->isValidForm($form)) {
            throw new DonationFormForbidden();
        }

        $formFields = array_filter($form->schema()->getFields(), function ($field) use ($request) {
            return array_key_exists($field->getName(), $request) && !$this->isSecurityChallengeField($field);
        });

        $validator = (new CreateValidatorFromFormFields())($formFields, $request);

        if ($validator->fails()) {
            $this->throwDonationFormFieldErrorsException($validator->errors());
        }

        $validatedValues = $validator->validated();

        /**
         * @since 4.16.0 added $isFinalSubmission (false here: per-step validation, not the final submission)
         * @since 3.22.0
         *
         * @param array $validatedValues   validated values in key value pairs
         * @param bool  $isFinalSubmission whether this is the final donation submission
         */
        do_action('givewp_donation_form_fields_validated', $validatedValues, false);

        return new JsonResponse(['valid' => true]);
    }

    /**
     * @since 3.0.0
     */
    public function getRequestData(): array
    {
        return $this->requestData;
    }

    /**
     * This loops over an array of errors in the specific FieldAPI format,
     * and converts them into a WP_Error object that is attached to the
     * exception and delivered back to the client via JSON.
     *
     * @since 3.0.0
     *
     * @param  array<string, string>  $errors
     *
     * @throws DonationFormFieldErrorsException
     */
    private function throwDonationFormFieldErrorsException(array $errors): void
    {
        $wpError = new WP_Error();

        foreach ($errors as $id => $error) {
            $wpError->add($id, $error);
        }

        throw new DonationFormFieldErrorsException($wpError);
    }

    /**
     * @since 4.14.0 update permission capability to use facade
     * @since 3.22.0
     */
    private function isValidForm(DonationForm $form): bool
    {
        if ($form->status->isTrash()) {
            return false;
        }

        if (!$form->status->isPublished() && !UserPermissions::donationForms()->canEdit()) {
            return false;
        }

        return true;
    }

    /**
     * @since 3.0.0
     */
    public function toArray(): array
    {
        return get_object_vars($this);
    }

    /**
     * @since 4.1.0
     */
    protected function isSecurityChallengeField(Field $field): bool
    {
        return is_subclass_of($field, SecurityChallenge::class);
    }
}
