<?php
/**
 * @author Daniel J. Summers <daniel@bitbadger.solutions>
 * @license MIT
 */

declare(strict_types=1);

namespace BitBadger\PDODocument;

use BitBadger\InspiredByFSharp\Option;
use BitBadger\PDODocument\Mapper\Mapper;
use PDO;
use PDOException;
use PDOStatement;

/**
 * Functions to execute custom queries
 */
class Custom
{
    /**
     * Prepare a query for execution and run it
     *
     * @param string $query The query to be run
     * @param array<int|string, mixed> $parameters The parameters for the query
     * @return PDOStatement The result of executing the query
     * @throws DocumentException If the query execution is unsuccessful
     */
    public static function &runQuery(string $query, array $parameters): PDOStatement
    {
        $debug = defined('PDO_DOC_DEBUG_SQL');
        try {
            $stmt = Configuration::dbConn()->prepare($query);
        } catch (PDOException $ex) {
            $keyword = explode(' ', $query, 2)[0];
            throw new DocumentException(
                sprintf("Error executing %s statement: [%s] %s", $keyword, Configuration::dbConn()->errorCode(),
                    Configuration::dbConn()->errorInfo()[2]),
                previous: $ex);
        }
        foreach ($parameters as $key => $value) {
            if ($debug) echo "<pre>Binding $value to $key\n</pre>";
            $dataType = match (true) {
                is_bool($value) => PDO::PARAM_BOOL,
                is_int($value)  => PDO::PARAM_INT,
                is_null($value) => PDO::PARAM_NULL,
                default         => PDO::PARAM_STR,
            };
            $stmt->bindValue($key, $value, $dataType);
        }
        if ($debug) echo '<pre>SQL: ' . $stmt->queryString . '</pre>';
        try {
            if ($stmt->execute()) return $stmt;
        } catch (PDOException $ex) {
            $keyword = explode(' ', $query, 2)[0];
            throw new DocumentException(
                sprintf("Error executing %s statement: [%s] %s", $keyword, $stmt->errorCode(), $stmt->errorInfo()[2]),
                previous: $ex);
        }
        $keyword = explode(' ', $query, 2)[0];
        throw new DocumentException("Error executing $keyword statement: " . $stmt->errorCode());
    }

    /**
     * Execute a query that returns a list of results (lazy)
     *
     * @template TDoc The domain type of the document to retrieve
     * @param string $query The query to be executed
     * @param array<int|string, mixed> $parameters Parameters to use in executing the query
     * @param Mapper<TDoc> $mapper Mapper to deserialize the result
     * @return DocumentList<TDoc> The items matching the query
     * @throws DocumentException If any is encountered
     */
    public static function list(string $query, array $parameters, Mapper $mapper): DocumentList
    {
        return DocumentList::create($query, $parameters, $mapper);
    }

    /**
     * Execute a query that returns an array of results (eager)
     *
     * @template TDoc The domain type of the document to retrieve
     * @param string $query The query to be executed
     * @param array<int|string, mixed> $parameters Parameters to use in executing the query
     * @param Mapper<TDoc> $mapper Mapper to deserialize the result
     * @return TDoc[] The items matching the query
     * @throws DocumentException If any is encountered
     */
    public static function array(string $query, array $parameters, Mapper $mapper): array
    {
        return iterator_to_array(self::list($query, $parameters, $mapper)->items());
    }

    /**
     * Execute a query that returns one or no results (returns false if not found)
     *
     * @template TDoc The domain type of the document to retrieve
     * @param string $query The query to be executed (will have "LIMIT 1" appended)
     * @param array<int|string, mixed> $parameters Parameters to use in executing the query
     * @param Mapper<TDoc> $mapper Mapper to deserialize the result
     * @return Option<TDoc> A `Some` instance if the item is found, `None` otherwise
     * @throws DocumentException If any is encountered
     */
    public static function single(string $query, array $parameters, Mapper $mapper): Option
    {
        try {
            $stmt = &self::runQuery("$query LIMIT 1", $parameters);
            return ($first = $stmt->fetch(PDO::FETCH_ASSOC)) ? Option::Some($mapper->map($first)) : Option::None();
        } finally {
            $stmt = null;
        }
    }

    /**
     * Execute a query that does not return a value
     *
     * @param string $query The query to execute
     * @param array<int|string, mixed> $parameters Parameters to use in executing the query
     * @throws DocumentException If any is encountered
     */
    public static function nonQuery(string $query, array $parameters): void
    {
        try {
            $stmt = &self::runQuery($query, $parameters);
        } finally {
            $stmt = null;
        }
    }

    /**
     * Execute a query that returns a scalar value
     *
     * @template T The scalar type to return
     * @param string $query The query to retrieve the value
     * @param array<int|string, mixed> $parameters Parameters to use in executing the query
     * @param Mapper<T> $mapper The mapper to obtain the result
     * @return mixed|false|T The scalar value if found, false if not
     * @throws DocumentException If any is encountered
     */
    public static function scalar(string $query, array $parameters, Mapper $mapper): mixed
    {
        try {
            $stmt = &self::runQuery($query, $parameters);
            return ($first = $stmt->fetch(PDO::FETCH_NUM)) ? $mapper->map($first) : false;
        } finally {
            $stmt = null;
        }
    }
}