<?php

namespace Velis\Model\RelationLoader\Relations;

use InvalidArgumentException;
use ReflectionException;
use Velis\App;
use Velis\Db\Db;
use Velis\Db\Postgres;
use Velis\Model\DataObject;
use Velis\Model\RelationLoader\Exceptions\CannotLoadPivotRelatedModelsException;

/**
 * Base class for the HasManyThrough and HasOneThrough relations.
 *
 * @author Szymon Janaczek <szymon.janaczek@velistech.com>
 *
 * @template TRelatedModel of DataObject
 * @template TTargetModel of DataObject
 * @extends Relation<TRelatedModel, TTargetModel>
 */
abstract class HasManyOrOneThrough extends Relation
{
    private string $junctionTable;
    private string $junctionForeignKey;
    private string $junctionLocalKey;
    private ?string $tableName = null;

    /**
     * @param string $relatedModel
     * @param string $junctionTable
     * @param string $junctionForeignKey
     * @param string $foreignKey
     * @param string $junctionLocalKey
     * @param string $localKey
     * @param TTargetModel|null $targetModel
     */
    public function __construct(
        string $relatedModel,
        string $junctionTable,
        string $junctionForeignKey,
        string $foreignKey,
        string $junctionLocalKey,
        string $localKey,
        $targetModel = null,
        ?string $tableName = null
    ) {
        parent::__construct($relatedModel, $foreignKey, $localKey, $targetModel);

        $this->junctionTable = $junctionTable;
        $this->junctionForeignKey = $junctionForeignKey;
        $this->junctionLocalKey = $junctionLocalKey;
        $this->tableName = $tableName;
    }

    private function getDb(): Db
    {
        return App::$di['db'];
    }

    private function getPlaceholder(array $foreignKeys): string
    {
        return implode(',', array_map(fn ($i) => ":id$i", range(0, count($foreignKeys) - 1)));
    }

    private function getPreparedBindings(array $foreignKeys): array
    {
        $bindings = [];
        $i = 0;
        foreach ($foreignKeys as $key) {
            $bindings["id$i"] = $key;
            $i++;
        }

        return $bindings;
    }

    /**
     * @throws CannotLoadPivotRelatedModelsException
     */
    private function getRelatedTableName(): string
    {
        try {
            $tableName = $this->tableName ?? $this->getDb()->getClassMetadata($this->relatedModel)->getTableName();
        } catch (InvalidArgumentException | ReflectionException $e) {
            throw new CannotLoadPivotRelatedModelsException($this->relatedModel, $e->getCode(), $e);
        }

        return $tableName;
    }

    private function getFields(string $tableName): string
    {
        if ($this->fields) {
            return join(',', array_map(fn (string $column) => $tableName . '.' . $column, $this->fields));
        }

        return $tableName . '.*';
    }

    private function sortOrder(string $tableName): string
    {
        if ($this->sortOrder) {
            return $this->sortOrder;
        }

        return $this->relatedModel::$_listDefaultOrder ?? $tableName . '.' . $this->localKey;
    }

    /**
     * Prepare a where clause with the custom search params.
     *
     * @param string $tableName
     * @return array<string, array<string, mixed>>
     */
    private function getWhere(string $tableName): array
    {
        if (!is_array($this->filters) || count($this->filters) <= 0) {
            return ['', []];
        }

        $bindings = [];
        foreach ($this->filters as $column => $value) {
            $bindings["$tableName.$column"] = $value;
        }

        $postgres = new Postgres();
        $clause = $postgres->conditions($bindings);

        return [$clause, $bindings];
    }

    /**
     * Transforms RAW arrays into proper model classes.
     *
     * @param array<int, mixed> $data
     * @param string[] $pivots names of the pivot fields
     * @return array<int, mixed> same format as ->keyBy() method data[foreignKey][n] = relatedModel
     */
    private function hydrateModels(array $data, array $pivots): array
    {
        $relatedModel = $this->relatedModel;
        foreach ($data as &$group) {
            foreach ($group as &$item) {
                unset($item[$pivots[0]]);
                unset($item[$pivots[1]]);

                $item = new $relatedModel($item);
            }
        }

        return $data;
    }

    /**
     * @param int[]|string[] $foreignKeys
     * @return array<int, array<TRelatedModel>>|array<string, TRelatedModel>
     * @throws CannotLoadPivotRelatedModelsException
     */
    public function getModels(array $foreignKeys): array
    {
        if (count($foreignKeys) <= 0) {
            return [];
        }

        $placeholder = $this->getPlaceholder($foreignKeys);
        $bindings = $this->getPreparedBindings($foreignKeys);
        $tableName = $this->getRelatedTableName();
        $fields = $this->getFields($tableName);
        $sortOrder = $this->sortOrder($tableName);
        [$customWhere, $customBindings] = $this->getWhere($tableName);

        $data = $this->getDb()->getAll(
            "SELECT
                $fields,
                $this->junctionTable.$this->junctionLocalKey as pivot_$this->junctionLocalKey,
                $this->junctionTable.$this->junctionForeignKey as pivot_$this->junctionForeignKey
            FROM
                $this->junctionTable
            LEFT JOIN $tableName
                ON $tableName.$this->localKey = $this->junctionTable.$this->junctionLocalKey
            WHERE
                $this->junctionTable.$this->junctionForeignKey IN ($placeholder)
                $customWhere
            ORDER BY $sortOrder",
            array_merge($bindings, $customBindings)
        );

        $data = $this->keyBy($data, 'pivot_' . $this->junctionForeignKey);

        return $this->hydrateModels($data, ["pivot_$this->junctionForeignKey", "pivot_$this->junctionLocalKey"]);
    }
}
