<?php

declare(strict_types=1);

/**
 * ACC WPDB Adapter using PDO
 */
class ACC_WPDB_Adapter
{
    public PDO $pdo;
    public string $prefix;
    public string $base_prefix;

    public string $users;
    public string $usermeta;
    public string $posts;
    public string $postmeta;
    public string $options;

    public $insert_id = 0;
    public $last_error = '';
    public $last_query = '';
    public $num_rows = 0;
    public $rows_affected = 0;

    public function __construct(PDO $pdo, string $prefix = 'wp_')
    {
        $this->pdo         = $pdo;
        $this->prefix      = $prefix;
        $this->base_prefix = $prefix;

        $this->users    = $prefix . 'users';
        $this->usermeta = $prefix . 'usermeta';
        $this->posts    = $prefix . 'posts';
        $this->postmeta = $prefix . 'postmeta';
        $this->options  = $prefix . 'options';
    }

    /**
     * WordPress-like prepare
     *
     * پشتیبانی از:
     * %s string
     * %d integer
     * %f float
     * %% literal percent
     */
    public function prepare($query, ...$args)
    {
        if ($query === null) {
            return null;
        }

        if (count($args) === 1 && is_array($args[0])) {
            $args = $args[0];
        }

        $placeholderCount = preg_match_all('/(?<!%)%[sdf]/', $query, $matches);

        if ($placeholderCount === 0) {
            return $query;
        }

        $i = 0;

        $query = preg_replace_callback('/(?<!%)%[sdf]/', function ($m) use (&$args, &$i) {
            $placeholder = $m[0];

            if (!array_key_exists($i, $args)) {
                return $placeholder;
            }

            $value = $args[$i];
            $i++;

            switch ($placeholder) {
                case '%d':
                    return (string) intval($value);

                case '%f':
                    return (string) floatval($value);

                case '%s':
                default:
                    return $this->pdo->quote((string) $value);
            }
        }, $query);

        return str_replace('%%', '%', $query);
    }

    public function query(string $query)
    {
        $this->last_query = $query;
        $this->last_error = '';
        $this->num_rows = 0;
        $this->rows_affected = 0;

        try {
            $stmt = $this->pdo->query($query);

            if ($stmt instanceof PDOStatement) {
                $this->rows_affected = $stmt->rowCount();
                $this->num_rows = $stmt->rowCount();
            }

            return $this->rows_affected;

        } catch (Throwable $e) {
            $this->last_error = $e->getMessage();

            if (defined('ACC_DEBUG') && ACC_DEBUG) {
                throw $e;
            }

            return false;
        }
    }

    public function get_results(string $query, $output = OBJECT)
    {
        $this->last_query = $query;
        $this->last_error = '';

        try {
            $stmt = $this->pdo->query($query);

            if (!$stmt) {
                return [];
            }

            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
            $this->num_rows = count($rows);

            return $this->format_rows($rows, $output);

        } catch (Throwable $e) {
            $this->last_error = $e->getMessage();

            if (defined('ACC_DEBUG') && ACC_DEBUG) {
                throw $e;
            }

            return [];
        }
    }

    public function get_row(string $query, $output = OBJECT, int $y = 0)
    {
        $this->last_query = $query;
        $this->last_error = '';

        try {
            $stmt = $this->pdo->query($query);
            if (!$stmt) {
                return null;
            }

            if ($y > 0) {
                for ($i = 0; $i < $y; $i++) {
                    if ($stmt->fetch(PDO::FETCH_ASSOC) === false) {
                        return null;
                    }
                }
            }

            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            $this->num_rows = $row ? 1 : 0;

            if (!$row) {
                return null;
            }

            return $this->format_row($row, $output);

        } catch (Throwable $e) {
            $this->last_error = $e->getMessage();

            if (defined('ACC_DEBUG') && ACC_DEBUG) {
                throw $e;
            }

            return null;
        }
    }

    public function get_var(string $query, int $x = 0, int $y = 0)
    {
        $this->last_query = $query;
        $this->last_error = '';

        try {
            $stmt = $this->pdo->query($query);
            if (!$stmt) {
                return null;
            }

            if ($y > 0) {
                for ($i = 0; $i < $y; $i++) {
                    if ($stmt->fetch(PDO::FETCH_NUM) === false) {
                        return null;
                    }
                }
            }

            $row = $stmt->fetch(PDO::FETCH_NUM);
            $this->num_rows = $row ? 1 : 0;

            if (!$row || !array_key_exists($x, $row)) {
                return null;
            }

            return $row[$x];

        } catch (Throwable $e) {
            $this->last_error = $e->getMessage();

            if (defined('ACC_DEBUG') && ACC_DEBUG) {
                throw $e;
            }

            return null;
        }
    }

    public function get_col(string $query, int $x = 0): array
    {
        $rows = $this->get_results($query, ARRAY_N);

        $col = [];

        foreach ($rows as $row) {
            if (array_key_exists($x, $row)) {
                $col[] = $row[$x];
            }
        }

        return $col;
    }

    public function insert(string $table, array $data, $format = null)
    {
        $this->last_error = '';

        if (empty($data)) {
            return false;
        }

        $columns = array_keys($data);
        $placeholders = array_fill(0, count($columns), '?');

        $sql = "INSERT INTO {$table} (`" . implode('`,`', $columns) . "`) VALUES (" . implode(',', $placeholders) . ")";

        try {
            $stmt = $this->pdo->prepare($sql);
            $ok = $stmt->execute(array_values($data));

            $this->insert_id = (int) $this->pdo->lastInsertId();
            $this->rows_affected = $stmt->rowCount();

            return $ok ? 1 : false;

        } catch (Throwable $e) {
            $this->last_error = $e->getMessage();

            if (defined('ACC_DEBUG') && ACC_DEBUG) {
                throw $e;
            }

            return false;
        }
    }

    public function update(string $table, array $data, array $where, $format = null, $where_format = null)
    {
        $this->last_error = '';

        if (empty($data) || empty($where)) {
            return false;
        }

        $setParts = [];
        foreach (array_keys($data) as $column) {
            $setParts[] = "`{$column}` = ?";
        }

        $whereParts = [];
        foreach (array_keys($where) as $column) {
            $whereParts[] = "`{$column}` = ?";
        }

        $sql = "UPDATE {$table} SET " . implode(', ', $setParts) . " WHERE " . implode(' AND ', $whereParts);

        try {
            $stmt = $this->pdo->prepare($sql);
            $ok = $stmt->execute(array_merge(array_values($data), array_values($where)));

            $this->rows_affected = $stmt->rowCount();

            return $ok ? $this->rows_affected : false;

        } catch (Throwable $e) {
            $this->last_error = $e->getMessage();

            if (defined('ACC_DEBUG') && ACC_DEBUG) {
                throw $e;
            }

            return false;
        }
    }

    public function delete(string $table, array $where, $where_format = null)
    {
        $this->last_error = '';

        if (empty($where)) {
            return false;
        }

        $whereParts = [];
        foreach (array_keys($where) as $column) {
            $whereParts[] = "`{$column}` = ?";
        }

        $sql = "DELETE FROM {$table} WHERE " . implode(' AND ', $whereParts);

        try {
            $stmt = $this->pdo->prepare($sql);
            $ok = $stmt->execute(array_values($where));

            $this->rows_affected = $stmt->rowCount();

            return $ok ? $this->rows_affected : false;

        } catch (Throwable $e) {
            $this->last_error = $e->getMessage();

            if (defined('ACC_DEBUG') && ACC_DEBUG) {
                throw $e;
            }

            return false;
        }
    }

    public function replace(string $table, array $data, $format = null)
    {
        $this->last_error = '';

        if (empty($data)) {
            return false;
        }

        $columns = array_keys($data);
        $placeholders = array_fill(0, count($columns), '?');

        $sql = "REPLACE INTO {$table} (`" . implode('`,`', $columns) . "`) VALUES (" . implode(',', $placeholders) . ")";

        try {
            $stmt = $this->pdo->prepare($sql);
            $ok = $stmt->execute(array_values($data));

            $this->insert_id = (int) $this->pdo->lastInsertId();
            $this->rows_affected = $stmt->rowCount();

            return $ok ? $this->rows_affected : false;

        } catch (Throwable $e) {
            $this->last_error = $e->getMessage();

            if (defined('ACC_DEBUG') && ACC_DEBUG) {
                throw $e;
            }

            return false;
        }
    }

    public function esc_like(string $text): string
    {
        return addcslashes($text, '_%\\');
    }

    public function _escape(string $data): string
    {
        return substr($this->pdo->quote($data), 1, -1);
    }

    public function get_charset_collate(): string
    {
        return 'DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
    }

    private function format_rows(array $rows, $output): array
    {
        if ($output === ARRAY_A) {
            return $rows;
        }

        if ($output === ARRAY_N) {
            return array_map('array_values', $rows);
        }

        return array_map(fn($row) => (object) $row, $rows);
    }

    private function format_row(array $row, $output)
    {
        if ($output === ARRAY_A) {
            return $row;
        }

        if ($output === ARRAY_N) {
            return array_values($row);
        }

        return (object) $row;
    }
}
