<?php
namespace Incoviba\Common\Ideal;

use PDO;
use PDOException;
use ReflectionProperty;
use ReflectionException;
use Incoviba\Common\Define;
use Incoviba\Common\Implement;
use Incoviba\Common\Implement\Exception\EmptyResult;

abstract class Repository implements Define\Repository
{
    public function __construct(protected Define\Connection $connection) {}

    protected string $table;
    public function getTable(): string
    {
        return $this->table;
    }
    public function setTable(string $table): Repository
    {
        $this->table = $table;
        return $this;
    }

    public function load(array $data_row): Define\Model
    {
        $model = $this->create($data_row);
        $this->setIndex($model, $data_row[$this->getKey()]);
        return $model;
    }

    public function remove(Define\Model $model): void
    {
        $query = $this->connection->getQueryBuilder()
            ->delete()->from($this->getTable())
            ->where("{$this->getKey()} = ?");
        $this->connection->execute($query, [$this->getIndex($model)]);
    }

    /**
     * @throws EmptyResult
     */
    public function fetchById(int $id): Define\Model
    {
        $query = $this->connection->getQueryBuilder()
            ->select()
            ->from($this->getTable())
            ->where("{$this->getKey()} = ?");
        return $this->fetchOne($query, [$id]);
    }

    /**
     * @throws EmptyResult
     */
    public function fetchAll(null|string|array $ordering = null): array
    {
        $query = $this->connection->getQueryBuilder()
            ->select()
            ->from($this->getTable());
        if ($ordering !== null) {
            $query->order($ordering);
        }
        return $this->fetchMany($query);
    }

    protected string $key = 'id';
    public function setKey(string $key): Repository
    {
        $this->key = $key;
        return $this;
    }
    protected function getKey(): string
    {
        return $this->key;
    }

    protected function setIndex(Define\Model &$model, mixed $value): Repository
    {
        $model->{$this->getKey()} = $value;
        return $this;
    }
    protected function getIndex(Define\Model $model): mixed
    {
        return $model->id;
    }

    protected function parseData(Define\Model $model, ?array $data, Implement\Repository\MapperParser $data_map): Define\Model
    {
        if ($data === null) {
            return $model;
        }
        foreach ($data_map->getColumns() as $column) {
            if (!$data_map->hasMapper($column)) {
                $this->parsePlainColumn($model, $column, $data);
                continue;
            }

            $settings = $data_map->getMapper($column);
            if ($settings->parse($model, $column, $data)) {
                continue;
            }
            $property = $column;
            if ($settings->hasProperty()) {
                $property = $settings->property;
            }
            $this->setDefault($model, $property);
        }
        return $model;
    }
    protected function parsePlainColumn(Define\Model &$model, string $column, ?array $data): void
    {
        $property = $column;
        if (isset($data[$column])) {
            $model->{$property} = $data[$column];
            return;
        }
        $this->setDefault($model, $property);
    }

    /**
     * @param Define\Model $model
     * @param string $property
     * @return void
     */
    protected function setDefault(Define\Model &$model, string $property): void
    {
        try {
            $prop = new ReflectionProperty($model, $property);
        } catch (ReflectionException) {
            $model->{$property} = null;
            return;
        }
        $type = $prop->getType()->getName();
        $value = match ($type) {
            'int' => 0,
            'float' => 0.0,
            'string' => '',
            default => null,
        };
        $model->{$property} = $value;
    }

    /**
     * @param array $columns
     * @param array $values
     * @return int
     * @throws PDOException
     */
    protected function saveNew(array $columns, array $values): int
    {
        $query = $this->connection->getQueryBuilder()
            ->insert()
            ->into($this->getTable())
            ->columns($columns)
            ->values(array_fill(0, count($columns), '?'));
        $this->connection->execute($query, $values);
        return $this->connection->getPDO()->lastInsertId();
    }

    /**
     * @param Define\Model $model
     * @param array $columns
     * @param array $data
     * @return Define\Model
     * @throws EmptyResult
     */
    protected function update(Define\Model $model, array $columns, array $data): Define\Model
    {
        $changes = [];
        $values = [];
        foreach ($columns as $column) {
            if (in_array($column, array_keys($data))) {
                $changes []= $column;
                $values []= $data[$column];
            }
        }

        if (count($changes) === 0) {
            return $model;
        }
        $columns_string = implode(', ', array_map(function($property) {return "`{$property}` = ?";}, $changes));
        $query = $this->connection->getQueryBuilder()
            ->update($this->getTable())
            ->set($columns_string)
            ->where("{$this->getKey()} = ?");
        $values []= $this->getIndex($model);
        try {
            $this->connection->execute($query, $values);
        } catch (PDOException $exception) {
            throw new EmptyResult($query, $exception, $data);
        }
        return $this->fetchById($this->getIndex($model));
    }

    /**
     * @param string $query
     * @param array|null $data
     * @return Define\Model
     * @throws EmptyResult
     */
    protected function fetchOne(string $query, ?array $data = null): Define\Model
    {
        try {
            $result = $this->connection->execute($query, $data)->fetch(PDO::FETCH_ASSOC);
            if ($result === false) {
                throw new EmptyResult($query, null, $data);
            }
        } catch (PDOException $exception) {
            throw new EmptyResult($query, $exception, $data);
        }
        return $this->load($result);
    }

    /**
     * @param string $query
     * @param array|null $data
     * @return array
     * @throws EmptyResult
     */
    protected function fetchMany(string $query, ?array $data = null): array
    {
        try {
            $results = $this->connection->execute($query, $data)->fetchAll(PDO::FETCH_ASSOC);
        } catch (PDOException $exception) {
            throw new EmptyResult($query, $exception, $data);
        }
        return array_map([$this, 'load'], $results);
    }

    /**
     * @param string $query
     * @param array|null $data
     * @return array
     * @throws EmptyResult
     */
    protected function fetchAsArray(string $query, ?array $data = null): array
    {
        try {
            $results = $this->connection->execute($query, $data)->fetchAll(PDO::FETCH_ASSOC);
        } catch (PDOException $exception) {
            throw new EmptyResult($query, $exception, $data);
        }
        return $results;
    }

    public function filterData(array $data): array
    {
        return $data;
    }
}
