<?php

	namespace Icing\Model\Behavior;

	use Cake\Datasource\EntityInterface;
	use Cake\Datasource\Exception\RecordNotFoundException;
	use Cake\Event\EventInterface;
	use ArrayObject;
	use Cake\ORM\Association;
    use Cake\ORM\Behavior;
    use Cake\ORM\Query;
    use Closure;
    use Throwable;

    /**
	 * Allows for automatic summing of related BelongsTo fields
	 * This behavior is placed on the child model
	 *
	 * Usage:
	 * $this->addBehavior('Icing.SumCache', [
	 *        "BELONGS_TO_ALIAS" => [
	 *            "FIELD TO SAVE SUM" => "FIELD TO SUM"
	 *        ]
	 * ])
	 *
	 * for example, if InvoiceItems belongsTo Invoices and you want the sum of InvoiceItems.invoice_item_total to be summed to Invoices.invoice_total:
	 *
	 * $this->belongsTo("Invoices");
	 * $this->addBehavior("Icing.SumCache, [
	 *        "Invoices" => [
	 *            "invoice_total" => "invoice_item_total",
	 *            "invoice_subtotal" => "invoice_item_subtotal",
	 *        ]
	 * ]")
	 *
	 * You can define multiple fields on each association
	 *
	 */
	class SumCacheBehavior extends Behavior
	{

		protected function _processAssociations(EventInterface $event, EntityInterface $entity): void
		{
			foreach ($this->_config as $assocAlias => $fields) {
				$assoc = $this->_table->getAssociation($assocAlias);
				$this->_processAssociation($event, $entity, $assoc, $fields);
			}
		}

		/**
		 * This is the *fast* version. Only the difference is calculated and added back to the parent entity
		 *
		 * @param \Cake\Event\EventInterface $event
		 * @param \Cake\Datasource\EntityInterface $entity
		 * @param \Cake\ORM\Association $assoc
		 * @param array $fields
		 *
		 * @return void
		 */
		protected function _processAssociation(EventInterface $event, EntityInterface $entity, Association $assoc, array $fields): void
        {
			$deltas = [];

			$assocId = $entity->get($assoc->getForeignKey());
			if (!$assocId) {
				return;
			}

			foreach ($fields as $field => $callable) {

				if ($event->getName() === 'Model.afterDelete') {
					$entity->set('is_deleted', true); //todo DZ make this core
				}

				$getDelta = $this->getDeltaMethod($entity);

				if (is_string($callable)) {
					$property = $callable;
					$callable = fn(EntityInterface $entity, callable $getDelta) => $getDelta($property);
				}

				$delta = $callable($entity, $getDelta);

				if ($delta !== 0) {
					$deltas[$field] = $delta;
				}
			}

			try {
				if (!empty($deltas)) {
					$assocEntity = $assoc->get($entity->get($assoc->getForeignKey()));
					foreach ($deltas as $field => $delta) {
						$assocEntity->set($field, $assocEntity->get($field) + $delta);
					}

					$options = $event->getData()['options'] ?? [];
					$options = array_remove_keys((array)$options, ['_primary', '_cleanOnSuccess', '_sourceTable', 'associated']);

					$assoc->saveOrFail($assocEntity, $options);
				}
			} catch (RecordNotFoundException) {
			}
		}

		/**
		 * Returns a method that will allow for proper calculation of the save delta. By default, new entities don't save the correct default value and entities are unaware that they are being deleted.
		 *
		 * @param \Cake\Datasource\EntityInterface $entity
		 *
		 * @return \Closure
		 */
		public function getDeltaMethod(EntityInterface $entity): Closure
        {
			if ($entity->isNew()) {
				return fn($property) => $entity->get($property);
			}
			if ($entity->get('is_deleted')) {
				return fn($property) => -$entity->getOriginal($property);
			}
			return fn($property) => $entity->get($property) - $entity->getOriginal($property);
		}

		public function getSaveDelta($entity, $property)
		{
			$original = $entity->getOriginal($property) ?? 0;
			if ($entity->isNew()) {
				$original = 0;
			}
			$modified = $entity->get($property) ?? 0;
			return $modified - $original;
		}

		public function getDeleteDelta($entity)
		{

		}

		/**
		 * This is the *slow* version. The sum is recalculated with a SUM query of all related rows.
		 *
		 * @param \Cake\Datasource\EntityInterface $entity
		 * @param \Cake\ORM\Association $assoc
		 * @param array $settings
		 *
		 * @return void
		 */
		protected function __processAssociation(EntityInterface $entity, Association $assoc, array $settings): void
		{
			foreach ($settings as $cacheField => $config) {
				if (is_string($config)) {
					$config = ['field' => $config];
				}

				$conditions = [$this->_table->aliasField($assoc->getForeignKey()) => $entity->get($assoc->getForeignKey())]
					+ ($config['conditions'] ?? []);
				$finder     = $config['finder'] ?? 'all';
				$field      = $assoc->aliasField($config['field']);

				$sum = $this->_getSum($assoc, $field, $conditions, $finder);

				$assoc->updateAll(
					[$cacheField => $sum],
					[$assoc->getPrimaryKey() => $entity->get($assoc->getForeignKey())]
				);
			}
		}

		protected function _getSum(Association $assoc, $field, $conditions, $finder): float
		{
			return $this->_table
				->find($finder)
				->groupBy($assoc->getForeignKey())
				->where($conditions)
				->select(function (Query $query) use ($field) {
					return ['sum' => $query->func()->sum($field)];
				})
				->all()
				->extract('sum')
				->first() ?? 0;
		}

		public function afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
        {
			if (isset($options['ignoreCounterCache']) && $options['ignoreCounterCache'] === true) {
				return;
			}

			$this->_processAssociations($event, $entity);
		}

		public function afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
        {
			if (isset($options['ignoreCounterCache']) && $options['ignoreCounterCache'] === true) {
				return;
			}

			$this->_processAssociations($event, $entity);
		}

		public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options) {
			//diebug($entity);
		}

	}
