<?php

namespace Apexl\Vault\Operators;

use Apexl\Vault\Connections\SqlConnection;
use Apexl\Vault\Exceptions\ExecutionException;
use Apexl\Vault\Interfaces\ConnectionInterface;
use Apexl\Vault\Interfaces\ResultInterface;
use Apexl\Vault\Results\PdoResult;
use PDO as PdoConnection;
use stdClass;
use Throwable;

abstract class Pdo
{
    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 int $loggedInUserId;
    protected ?PdoConnection $connection;
    private ?SqlConnection $sqlConnection = null;

    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 = [];
        $result = $this->execute();

        \Apexl\Io\Logger\logger('query')->debug($this->sql, [
            'params' => $this->queryFields,
            'error' => $result->error(),
        ]);

        if ($result->succeeded()) {
            return [true, null];
        }

        return [false, implode(', ', $result->error())];
    }

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

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

            $timeStart = microtime(true);

            $statement->execute($this->queryFields);
            $this->statementError = $statement->errorInfo();

            \Apexl\Io\Logger\logger('query')->debug($this->sql, [
                'params' => $this->queryFields,
                'time' => microtime(true) - $timeStart,
            ]);

        } catch (Throwable $throwable) {
            \Apexl\Io\Logger\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 new PdoResult($statement, $this->statementError);
    }

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

    protected function initConnection(): Pdo
    {
        $this->connection = $this->makeConnection();
        $this->sqlConnection = null;

        return $this;
    }

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

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

    /**
     * @internal Use connection()->handle() — this hands out the raw handle with none of
     *           the transaction ownership that goes with it.
     * @deprecated 5.0 Use connection()->handle().
     */
    public function getConnection(): PdoConnection
    {
        // A method is somewhere PHP will actually let us notice the call. Type
        // declarations are not: see interfaces/driver.php.
        trigger_error(
            'Apexl\\Vault: getConnection() is deprecated, use connection()->handle().',
            E_USER_DEPRECATED,
        );

        if (!isset($this->connection)) {
            $this->initConnection();
        }

        return $this->connection;
    }

    public function connection(): ConnectionInterface
    {
        // Memoised on the driver, so transaction depth is counted once per vault rather
        // than once per caller — a fresh wrapper each time would reintroduce exactly the
        // per-instance bookkeeping this replaces.
        if (!isset($this->connection)) {
            $this->initConnection();
        }

        return $this->sqlConnection ??= new SqlConnection($this->connection);
    }

    public function lastInsertId(): int
    {
        return $this->connection()->lastInsertId();
    }
}
