<?php

declare(strict_types=1);

namespace App\Actions\Repositories;

use App\Contracts\Repositories\RepositoryInterface;
use Closure;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

/**
 * @template TModel of \App\Models\BaseModel
 */
abstract class ReconcileRepositoriesAction
{
    /**
     * Perform set reconciliation between source and destination repositories.
     *
     * @param  RepositoryInterface<TModel>  $source
     * @param  RepositoryInterface<TModel>  $destination
     *
     * @throws Exception
     */
    public function reconcileRepositories(RepositoryInterface $source, RepositoryInterface $destination): ReconcileResults
    {
        try {
            DB::beginTransaction();

            $sourceModels = $source->get();

            $destinationModels = $destination->get($this->columnsForCreateDelete());

            $created = $this->createModelsFromSource($destination, $sourceModels, $destinationModels);

            $deleted = $this->deleteModelsFromDestination($destination, $sourceModels, $destinationModels);

            $destinationModels = $destination->get($this->columnsForUpdate());

            $updated = $this->updateDestinationModels($destination, $sourceModels, $destinationModels);

            DB::commit();

            return $this->getResults($created, $deleted, $updated);
        } catch (Exception $e) {
            Log::error($e->getMessage());

            DB::rollBack();

            throw $e;
        }
    }

    /**
     * The columns used for create and delete set operations.
     *
     * @return string[]
     */
    abstract protected function columnsForCreateDelete(): array;

    /**
     * Callback for create and delete set operation item comparison.
     */
    abstract protected function diffCallbackForCreateDelete(): Closure;

    /**
     * Create models that exist in source but not in destination.
     *
     * @param  RepositoryInterface<TModel>  $destination
     */
    protected function createModelsFromSource(
        RepositoryInterface $destination,
        Collection $sourceModels,
        Collection $destinationModels
    ): Collection {
        $createModels = $sourceModels->diffUsing($destinationModels, $this->diffCallbackForCreateDelete());

        return $createModels->each(fn (Model $createModel): bool => $destination->save($createModel));
    }

    /**
     * Delete models that exist in destination but not in source.
     *
     * @param  RepositoryInterface<TModel>  $destination
     */
    protected function deleteModelsFromDestination(
        RepositoryInterface $destination,
        Collection $sourceModels,
        Collection $destinationModels
    ): Collection {
        $deleteModels = $destinationModels->diffUsing($sourceModels, $this->diffCallbackForCreateDelete());

        return $deleteModels->each(fn (Model $deleteModel): bool => $destination->delete($deleteModel));
    }

    /**
     * The columns used for update set operation.
     *
     * @return string[]
     */
    abstract protected function columnsForUpdate(): array;

    /**
     * Callback for update set operation item comparison.
     */
    abstract protected function diffCallbackForUpdate(): Closure;

    /**
     * Get source model that has been updated for destination model.
     */
    abstract protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model;

    /**
     * Update destination models that have changed in source.
     *
     * @param  RepositoryInterface<TModel>  $destination
     */
    protected function updateDestinationModels(
        RepositoryInterface $destination,
        Collection $sourceModels,
        Collection $destinationModels
    ): Collection {
        $updatedModels = $destinationModels->diffUsing($sourceModels, $this->diffCallbackForUpdate());

        return $updatedModels->each(function (Model $updatedModel) use ($sourceModels, $destination): void {
            $sourceModel = $this->resolveUpdatedModel($sourceModels, $updatedModel);
            if ($sourceModel instanceof Model) {
                $destination->update($updatedModel, $sourceModel->withoutAppends()->toArray());
            }
        });
    }

    /**
     * Get reconciliation results.
     */
    abstract protected function getResults(Collection $created, Collection $deleted, Collection $updated): ReconcileResults;
}
