<?php

namespace onespace\tools\activeApi\helpers;

use InvalidArgumentException;
use onespace\tools\enum\general\HTTPMethod;
use Yii;
use yii\helpers\ArrayHelper;

/**
 * Trait for performing Create, Update and Delete active record actions across an API.
 */
trait CreateUpdateDeleteHelperTrait {
    use ApiClientHelperTrait;
    use BaseApiHelperTrait;

    /**
     * The default endpoint for performing a 'create'
     *
     * @var string  $createDefaultEndpoint  Default: 'create'
     *
     * @access  public
     */

    protected string $createDefaultEndpoint = 'create';

    /**
     * The default endpoint for performing a 'update'
     *
     * @var string  $updateDefaultEndpoint  Default: 'update'
     *
     * @access  public
     */

    protected string $updateDefaultEndpoint = 'update';

    /**
     * The default endpoint for performing a 'delete'
     *
     * @var string  $deleteDefaultEndpoint  Default: 'delete'
     *
     * @access  public
     */

    protected string $deleteDefaultEndpoint = 'delete';

    /**
     * A task to perform before a model is saved (updated or inserted)
     *
     * @access  public
     */
    abstract public function beforeSave(bool $insert): bool;

    /**
     * A task to perform after the model has been saved (updated or inserted)
     *
     * @access  public
     */
    abstract public function afterSave(): void;

    /**
     * Saves and syncs a model back to a model. This detects if this is a 'create' or 'update'
     * and performs the appropriate api call.
     *
     * @param   bool        $runValidation  Whether or not to run validation before save. Default: true
     * @param   array|null  $attributes     Which attributes to validate. Null validates all active attributes. Default: null
     *
     * @return  bool
     *
     * @access  public
     */
    public function save(bool $runValidation = true, ?array $attributes = null): bool {
        if ($this->isNew) {
            return $this->insert($runValidation, $attributes);
        } else {
            return $this->update($runValidation, $attributes);
        }
    }

    /**
     * Performs an insert, to add a new record on the API.
     *
     * @param   bool        $runValidation  Whether or not to run validation before save. Default: true
     * @param   array|null  $attributes     Which attributes to validate. Null validates all active attributes. Default: null
     *
     * @return  bool
     *
     * @access  public
     */
    public function insert(bool $runValidation, ?array $attributes = null): bool {
        if ($runValidation && !$this->validate($attributes)) {
            $this->_errors[] = 'Model not inserted due to validation error.';
            $this->_errors[] = json_encode($this->getErrors());
            Yii::error('Model not inserted due to validation error.', __METHOD__);
            Yii::error($this->getErrors(), __METHOD__);
            return false;
        }

        $this->prepareEndpoint($this->createDefaultEndpoint);
        $this->prepareMethod(HTTPMethod::POST);

        if ($this->beforeSave(true)) {
            $response = $this->getClient()->create(ArrayHelper::toArray($this), $this->_endpoint, $this->_method);

            $this->checkResponse($response);
            $this->afterSave();

            $data = $response->data;
            $this->setLastResponseData($data);
            return $data['success'];
        } else {
            $this->_errors[] = 'Before save returns false';
            Yii::error($this->getErrors(), __METHOD__);
            return false;
        }
    }

    /**
     * Performs an update, to update a record on the API.
     *
     * @param   bool        $runValidation  Whether or not to run validation before save. Default: true
     * @param   array|null  $attributes     Which attributes to validate. Null validates all active attributes. Default: null
     *
     * @return  bool
     *
     * @access  public
     */
    public function update(bool $runValidation, ?array $attributes = null): bool {
        if ($runValidation && !$this->validate($attributes)) {
            $this->_errors[] = 'Model not inserted due to validation error.';
            $this->_errors[] = json_encode($this->getErrors());
            Yii::error('Model not inserted due to validation error.', __METHOD__);
            Yii::error($this->getErrors(), __METHOD__);
            return false;
        }

        $this->prepareEndpoint($this->updateDefaultEndpoint);
        $this->prepareMethod(HTTPMethod::POST);

        // Set where
        $primaryKey = $this->primaryKey();
        $where = [$primaryKey => $this->$primaryKey];

        if ($this->beforeSave(false)) {
            $response = $this->getClient()->update(ArrayHelper::toArray($this), $where, $this->_endpoint, $this->_method);

            $this->checkResponse($response);
            $this->afterSave();

            $data = $response->data;
            $this->setLastResponseData($data);
            return $data['success'];
        } else {
            $this->_errors[] = 'Before save returns false';
            Yii::error($this->getErrors(), __METHOD__);
            return false;
        }
    }

    /**
     * Performs a delete, to delete a record on the API.
     *
     * @return  bool
     *
     * @access  public
     */
    public function delete(): bool {
        $this->prepareEndpoint($this->deleteDefaultEndpoint);
        $this->prepareMethod(HTTPMethod::POST);

        $primaryKey = $this->primaryKey();

        $response = $this->getClient()->delete([$primaryKey => $this->$primaryKey], $this->_endpoint);

        $this->checkResponse($response);

        $data = $response->data;
        $this->setLastResponseData($data);
        return $data['success'];
    }

    /**
     * Overwrites the standard validate method on `yii\base\Model`. This allows the skipping of certain methods
     * which cannot be validated directly from the database - for example 'unique'.
     *
     * @param   ?array  $attributeNames The attributes to validate. If null, it will validate all active attributes. Default: null
     * @param   bool    $clearErrors    Whether or not to clear errors before validation. Default: true
     *
     * @return  bool
     *
     * @access  public
     */
    public function validate($attributeNames = null, $clearErrors = true) {
        if ($clearErrors) {
            $this->clearErrors();
        }

        if (!$this->beforeValidate()) {
            return false;
        }

        $scenarios = $this->scenarios();
        $scenario = $this->getScenario();
        if (!isset($scenarios[$scenario])) {
            throw new InvalidArgumentException("Unknown scenario: $scenario");
        }

        if ($attributeNames === null) {
            $attributeNames = $this->activeAttributes();
        }

        $attributeNames = (array)$attributeNames;

        /**
         * These cannot be validated on this side of the API.
         */
        $skip = [
            \yii\validators\UniqueValidator::class,
        ];

        foreach ($this->getActiveValidators() as $validator) {
            if (in_array($validator::class, $skip)) {
                continue;
            }
            $validator->validateAttributes($this, $attributeNames);
        }
        $this->afterValidate();

        return !$this->hasErrors();
    }

    /**
     * Allows the changing of the default 'create' endpoint.
     *
     * @param   string  $endpoint   The new endpoint value
     *
     * @access  public
     */
    public function setCreateDefaultEndpoint(string $endpoint): void {
        $this->createDefaultEndpoint = $endpoint;
    }

    /**
     * Allows the changing of the default 'update' endpoint.
     *
     * @param   string  $endpoint   The new endpoint value
     *
     * @access  public
     */
    public function setUpdateDefaultEndpoint(string $endpoint): void {
        $this->updateDefaultEndpoint = $endpoint;
    }

    /**
     * Allows the changing of the default 'delete' endpoint.
     *
     * @param   string  $endpoint   The new endpoint value
     *
     * @access  public
     */
    public function setDeleteDefaultEndpoint(string $endpoint): void {
        $this->deleteDefaultEndpoint = $endpoint;
    }
}
