<?php

namespace apexl\Vault\operators;

use apexl\Vault\exceptions\ExecutionException;
use apexl\Vault\interfaces\operators;
use PDO as PdoConnection;
use PDOStatement;
use stdClass;
use Throwable;

abstract class pdo implements operators
{
    public array $statementError;
    protected string $database;
    protected string $host;
    protected string $user;
    protected string $pass;
    protected string $schema;
    protected string $sql = '';
    /* @var string[] */
    protected array $queryFields = [];
    protected string $table;
    protected stdClass $connectionDetails;
    protected array $options;
    protected PDOStatement $statement;
    protected int $loggedInUserId;
    protected ?PdoConnection $connection;

    public function __construct(stdClass $connectionDetails)
    {

        $this->setHost($connectionDetails->dbhost ?? '')
            ->setDatabase($connectionDetails->dbname ?? '')
            ->setUser($connectionDetails->dbuser ?? '')
            ->setPass($connectionDetails->dbpass ?? '')
            ->setSchema($connectionDetails->driver ?? '')
            ->setOptions((array) ($connectionDetails->options ?? []));
    }

    public function setOptions(array $options = []): pdo
    {
        $this->options = $options;

        return $this;
    }

    public function setSchema(string $schema = ''): pdo
    {
        $this->schema = $schema;

        return $this;
    }

    public function setHost(string $host): pdo
    {
        $this->host = $host;

        return $this;
    }

    public function __destruct()
    {
        $this->connection = null;
    }

    public function getDatabase(): string
    {
        return $this->database;
    }

    /**
     * Function to set the name of the database we're connecting to
     */
    public function setDatabase(string $name = ''): pdo
    {
        $this->database = $name;

        return $this;
    }

    public function getUser(): string
    {
        return $this->user;
    }

    /**
     * Function to set the username to connect with
     */
    public function setUser(string $user = ''): pdo
    {
        $this->user = $user;

        return $this;
    }

    public function getPass(): string
    {
        return $this->pass;
    }

    public function setPass(string $pass = ''): pdo
    {
        $this->pass = $pass;

        return $this;
    }

    public function getTable(): ?string
    {
        return $this->table ?? null;
    }

    public function testConnection(): bool
    {
        try {
            $this->makeConnection();

            return true;
        } catch (Throwable) {
            return false;
        }
    }

    abstract protected function makeConnection(): PdoConnection;

    /**
     * @throws ExecutionException
     */
    public function executeRaw(string $sql): array
    {
        $this->sql = $sql;
        $this->queryFields = [];
        $this->execute();

        logger('query')->debug($this->sql, [
            'params' => $this->queryFields,
            'error' => $this->statementError,
        ]);

        if ($this->statementError[0] == '00000') {
            return [true, null];
        }

        return [false, implode(', ', $this->statementError)];
    }

    /**
     * @throws ExecutionException
     */
    public function execute(): pdo
    {
        //Allow us to connect as late as possible.
        if (empty($this->connection)) {
            $this->initConnection();
        }
        try {

            $prepare = $this->connection->prepare($this->sql);

            $this->statement = $prepare;
            $timeStart = microtime(true);

            $this->statement->execute($this->queryFields);
            logger('query')->debug($this->sql, [
                'params' => $this->queryFields,
                'time' => microtime(true) - $timeStart,
            ]);

        } catch (Throwable $throwable) {
            logger('query')->error($throwable->getMessage(), [
                'sql' => $this->sql,
                'params' => $this->queryFields,
            ]);

            throw new ExecutionException(
                sprintf('%s: %s', $throwable->getCode(), $throwable->getMessage()),
                is_int($throwable->getCode()) ? $throwable->getCode() : 500,
                $throwable
            );
        }

        return $this;
    }

    // This should be used only with migrations,  otherwise for safety execute should be used

    protected function initConnection(): pdo
    {
        $this->connection = $this->makeConnection();

        return $this;
    }

    public function fetchColumn(int $ix = 0): mixed
    {
        return $this->statement->fetchColumn($ix);
    }

    public function fetchAllAssoc(): array|false
    {
        return $this->statement->fetchAll(PdoConnection::FETCH_ASSOC);
    }

    public function fetchAll(): array|false
    {
        return $this->statement->fetchAll(PdoConnection::FETCH_OBJ);
    }

    public function fetchAssoc(): array|false
    {
        return $this->statement->fetch(PdoConnection::FETCH_ASSOC);
    }

    public function fetch(): stdClass|false
    {
        return $this->statement->fetch(PdoConnection::FETCH_OBJ);
    }

    public function setLoggedInUserId(int $userId): bool
    {
        $this->loggedInUserId = $userId;

        return true;
    }

    public function isCount(): bool
    {
        return stripos($this->sql, "count(") !== false;
    }

    public function getQueryFields(): array
    {
        return $this->queryFields;
    }

    public function getConnection(): PdoConnection
    {
        if (!isset($this->connection)) {
            $this->initConnection();
        }

        return $this->connection;
    }
}
