<?php

namespace CliqThemes\ORM\Relations;

use CliqThemes\ORM\Model;

/**
 * Represents a many-to-many relationship between two models.
 */
class BelongsToMany extends Relation
{
    protected string $pivotTable;

    protected string $foreignPivotKey;

    protected string $relatedPivotKey;

    /**
     * @param string $pivotTable      Pivot table joining the two models.
     * @param string $foreignPivotKey Column on the pivot table referencing the parent.
     * @param string $relatedPivotKey Column on the pivot table referencing the related model.
     */
    public function __construct(Model $parent, Model $related, string $pivotTable, string $foreignPivotKey, string $relatedPivotKey)
    {
        parent::__construct($parent, $related);
        $this->pivotTable = $pivotTable;
        $this->foreignPivotKey = $foreignPivotKey;
        $this->relatedPivotKey = $relatedPivotKey;
    }

    /**
     * Retrieve the related models via the pivot table relationship.
     *
     * @return Model[]
     */
    public function getResults(): array
    {
        $connection = Model::connection();
        $pivot = $connection->table($this->pivotTable);
        $relatedTable = $this->related->getQualifiedTable();

        $key = $this->parent->getKey();
        $placeholder = $this->determinePlaceholder($key);

        $sql = "SELECT {$relatedTable}.* FROM {$relatedTable} "
            . "INNER JOIN {$pivot} ON {$pivot}.`{$this->relatedPivotKey}` = {$relatedTable}.`" . $this->related->getPrimaryKey() . "` "
            . "WHERE {$pivot}.`{$this->foreignPivotKey}` = {$placeholder}";

        $rows = $connection->select($sql, [$this->castBinding($key, $placeholder)]);

        return array_map(function (array $row) {
            $class = get_class($this->related);
            $model = new $class();
            $model->forceFill($row);
            $model->syncOriginal();

            return $model;
        }, $rows);
    }

    /**
     * Determine the placeholder format for the pivot binding value.
     */
    protected function determinePlaceholder(mixed $value): string
    {
        if (is_int($value) || (is_string($value) && ctype_digit($value))) {
            return '%d';
        }

        if (is_float($value) || (is_string($value) && is_numeric($value))) {
            return '%f';
        }

        return '%s';
    }

    /**
     * Cast the binding value to match the placeholder type.
     */
    protected function castBinding(mixed $value, string $placeholder): mixed
    {
        return match ($placeholder) {
            '%d' => (int) $value,
            '%f' => (float) $value,
            default => $value,
        };
    }
}

