<?php

namespace Velis\Model\RelationLoader\Relations;

use Velis\Model\BaseModel;

/**
 * A Base relation class, used as the foundation for new relations.
 * Containing commonly used methods.
 *
 * @author Szymon Janaczek <szymon.janaczek@velistech.com>
 *
 * @template TRelatedModel of BaseModel
 * @template TTargetModel of BaseModel
 */
abstract class Relation
{
    /**
     * Class string of the model we want to get through relation.
     *
     * @var class-string<TRelatedModel>
     */
    protected string $relatedModel;

    /**
     * Name of the foreign key.
     */
    protected string $foreignKey;

    /**
     * Name of the local key.
     */
    protected string $localKey;

    /**
     * Model to which we want to get related model.
     *
     * @var TTargetModel|null
     */
    private $targetModel;

    /**
     * Data for the WHERE clause.
     * In format:
     * $filters[field_name] = string|int|bool|array.
     * Set null for default filtering.
     *
     * @var array<string, mixed>|null
     */
    protected ?array $filters = null;

    /**
     * Value for the ORDER BY clause.
     * Provided as string, for instance: `surname DESC, name ASC`.
     * Set null for default sorting.
     */
    protected ?string $sortOrder = null;

    /**
     * Fields to contain in the SELECT clause.
     * Set null if you want to get all of them - the same result as `SELECT *`.
     * @var string[]|null
     */
    protected ?array $fields = null;

    /**
     * @param class-string<TRelatedModel> $relatedModel Class string of the model we want to get.
     * @param string $foreignKey Name of the foreign key.
     * @param string|null $localKey In most cases, foreign and local keys are named in the same way.
     *                              So if the local key's name is not provided, use the foreign key's name.
     * @param TTargetModel|null $targetModel Object of the target model - used with lazy loading.
     *                                       If lazy loading is not necessary, just set null value.
     */
    public function __construct(
        string $relatedModel,
        string $foreignKey,
        ?string $localKey = null,
        BaseModel $targetModel = null
    ) {
        $this->relatedModel = $relatedModel;
        $this->foreignKey = $foreignKey;
        $this->localKey = $localKey ?? $foreignKey;
        $this->targetModel = $targetModel;
    }

    // region Setters

    /**
     * Filter related data.
     *
     * @param array<string, mixed>|null $filters
     * @return self<TRelatedModel, TTargetModel>
     */
    public function where(?array $filters = null): self
    {
        $this->filters = $filters;

        return $this;
    }

    /**
     * Sort related data.
     *
     * @param string|null $sortOrder
     * @return self<TRelatedModel, TTargetModel>
     */
    public function orderBy(?string $sortOrder = null): self
    {
        $this->sortOrder = $sortOrder;

        return $this;
    }

    /**
     * Select needed fields for your related data.
     *
     * @param string[]|null $fields
     * @return self<TRelatedModel, TTargetModel>
     */
    public function select(?array $fields = null): self
    {
        $this->fields = $fields;

        return $this;
    }

    /**
     * Manually set the related model.
     *
     * @param TRelatedModel $model
     * @return void
     */
    public function setTargetModel($model): void
    {
        $this->targetModel = $model;
    }
    // endregion

    public function getForeignKey(): string
    {
        return $this->foreignKey;
    }

    /**
     * Restructure the input array to get it in the form needed by related-to-target-model-matching algorithm:
     * data[foreignKey][n] = relatedModel
     *
     * @param array $data
     * @param string $key
     * @return array
     */
    protected function keyBy(array $data, string $key): array
    {
        $newData = [];
        foreach ($data as $row) {
            $newData[$row[$key]][] = $row;
        }

        return $newData;
    }

    /**
     * Fetch the related models for the passed foreign keys.
     * Its implementation may differ depending on the relation type:
     * HasMany vs HasManyThrough
     *
     * @param array<int|string> $foreignKeys
     * @return array<int, array<TRelatedModel>>|array<string, TRelatedModel>
     */
    abstract protected function getModels(array $foreignKeys): array;

    /**
     * Find and load the related model into relationsContainer for the target model.
     *
     * @param array<TRelatedModel> $relatedModels
     * @param array<TTargetModel> $targetModels
     *
     * @return array<TTargetModel>
     */
    public function match(string $relationName, array $relatedModels, array $targetModels): array
    {
        foreach ($targetModels as $targetModel) {
            $targetModel->setRelation($relationName, $this->getRelatedModels($targetModel, $relatedModels));
        }

        return $targetModels;
    }

    /**
     * Get related models list.
     *
     * @return array<TRelatedModel>|TRelatedModel|null
     */
    public function get()
    {
        $models = $this->getModels([$this->targetModel[$this->foreignKey]]);

        return reset($models) ?: null;
    }

    /**
     * Get a single related model entity.
     *
     * @return TRelatedModel
     */
    public function getOne()
    {
        $data = $this->get();
        if (is_array($data)) {
            return reset($data);
        }

        return null;
    }
}
