<?php

namespace onespace\core\behaviours;

use onespace\core\components\Anonymiser;
use InvalidArgumentException;
use yii\base\Behavior;
use yii\db\ActiveRecord;
use yii\base\Event;
use yii\base\ModelEvent;
use yii\helpers\ArrayHelper;

/**
 * @psalm-api
 * 
 * This is used as a way to anonymise data on a model by generating random replacement values
 * 
 * Actual randomisation is handled by the `\onespace\core\components\Anonymiser` component - see there for more
 * details on how things actually work
 * 
 * @property ActiveRecord $owner automatically set by attaching this behaviour to an ActiveRecord
 * @property int $seed Static seed value
 * @property string $seedField name of a field to reference for a seed value
 * @property array $fields fields to anonymise, in key => value format. See `[[Anonymiser]]` class for details
 * @property string $locale locale to use in generation. Defaults to `en_ZA`
 * @property bool|callable(ActiveRecord $owner):bool $active whether or not the behaviour is active. 
 * 
 * @template-extends Behavior<ActiveRecord>
 */
class AnonymiseFieldsBehaviour extends Behavior {
    /** 
     * @var ActiveRecord
     * @psalm-suppress NonInvariantDocblockPropertyType - Owner will always be assigned on attach() method.
     */
    public $owner;

    /** @var int */
    public $seed;

    /** @var string|null */
    public $seedField = null;

    /** @var array */
    public $fields = [];

    /** @var string */
    public $locale = 'en_ZA';

    /** @var bool|callable(ActiveRecord $owner): bool $active */
    public $active = true;

    private bool $_hasAnonymised = false;
    private bool $_active = false;

    /**
     * @param ActiveRecord $owner
     * @return void
     */
    #[\Override]
    public function attach($owner) {
        parent::attach($owner);
        if (!$owner instanceof ActiveRecord) {
            throw new InvalidArgumentException('AnonymiseFieldsBehaviour only works with ActiveRecords!');
        }

        // run the active check
        if (is_callable($this->active)) {
            $this->_active = call_user_func($this->active, $owner);
        } else {
            $this->_active = ($this->active == true);
        }
    }

    #[\Override]
    public function events(): array {
        return [
            ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave',
            ActiveRecord::EVENT_AFTER_FIND => 'afterFind',
        ];
    }

    /**
     * Anonymise the data when it's grabbed from the db
     * @param Event $_event 
     * @return void 
     */
    public function afterFind(Event $_event) {
        if ($this->_active) {
            // grab the model's data as an array
            $data = $this->owner->attributes;

            // actually anonymise the data
            $anonymiser = Anonymiser::getInstance($this->locale);
            $anonymiser->anonymiseRow($this->seedField ?: intval($this->seed), $this->fields, $data);

            // keep track that this model has been anonymised
            $this->_hasAnonymised = true;

            // put the data back into the model
            /** @var string $attribute */
            foreach ($this->fields as $attribute => $_realData) {
                $this->owner->$attribute = ArrayHelper::getValue($data, $attribute);
            }
        }
    }

    /**
     * If we've anonymised data, the user shouldn't be allowed to save it
     * @param ModelEvent $event 
     * @return void 
     */
    public function beforeSave(ModelEvent $event) {
        if ($this->_hasAnonymised) {
            // prevent the event from continuing
            $event->isValid = false;
            // add an error to explain to the user why the data isn't being saved
            /** @var string $attribute */
            foreach ($this->fields as $attribute => $_data) {
                $this->owner->addError($attribute, 'This field has been anonymised - the data cannot be saved!');
            }
        }
    }
}
