<?php

declare(strict_types=1);

namespace Instinctor\Core;

if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * DB — Database abstraction layer.
 *
 * Static facade that delegates to a driver selected by INSTINCTOR_ENV.
 * On WordPress (default): WordPressDriver wraps $wpdb, fetched fresh per call.
 * On standalone:          PDODriver (not yet implemented — stubs throw clearly).
 *
 * Usage mirrors $wpdb but as static methods:
 *   DB::get_results("SELECT ...")      → $wpdb->get_results(...)
 *   DB::prepare("SELECT %d", $id)     → $wpdb->prepare(...)
 *   DB::insert('table', [...])        → $wpdb->insert(...)
 *   DB::prefix()                      → $wpdb->prefix
 *   DB::insert_id()                   → $wpdb->insert_id
 *   DB::table('posts')                → $wpdb->posts  (WP core tables)
 *
 * Migration path: replace $wpdb->method(...) with DB::method(...)
 * one file at a time. Both are identical in behaviour on WordPress.
 */
final class DB
{
    private static ?DB\DriverInterface $driver = null;

    // -------------------------------------------------------------------------
    // Driver bootstrap
    // -------------------------------------------------------------------------

    private static function driver(): DB\DriverInterface
    {
        if (self::$driver === null) {
            $env = defined('INSTINCTOR_ENV') ? INSTINCTOR_ENV : 'wordpress';

            self::$driver = $env === 'standalone'
                ? new DB\PDODriver()
                : new DB\WordPressDriver();
        }

        return self::$driver;
    }

    /**
     * Reset the driver — used in tests or to force re-initialisation.
     * Not called anywhere in production code.
     */
    public static function reset(): void
    {
        self::$driver = null;
    }

    // -------------------------------------------------------------------------
    // Query methods
    // -------------------------------------------------------------------------

    /**
     * Prepare a SQL query safely (parameterised placeholder substitution).
     * Mirrors: $wpdb->prepare($query, ...$args)
     *
     * @param  string $query  SQL with %d / %s / %f placeholders
     * @param  mixed  ...$args Values to substitute
     * @return string         Prepared SQL string
     */
    public static function prepare(string $query, mixed ...$args): string
    {
        // WordPress-compatible: callers may pass the args as a single array
        // (e.g. DB::prepare($sql, [$a, $b])). Without this, the array is cast
        // to int 1 for the first placeholder and the rest are dropped — which
        // silently turned "LIMIT %d OFFSET %d" with [50,0] into "LIMIT 1".
        if (count($args) === 1 && is_array($args[0])) {
            $args = $args[0];
        }
        return self::driver()->prepare($query, ...$args);
    }

    /**
     * Run a query and return all rows as an array of objects (default)
     * or associative arrays when $output = ARRAY_A.
     * Mirrors: $wpdb->get_results($query, $output)
     *
     * @return array<object|array>
     */
    public static function get_results(string $query, string $output = 'OBJECT'): array
    {
        return self::driver()->get_results($query, $output);
    }

    /**
     * Run a query and return the first row only.
     * Mirrors: $wpdb->get_row($query, $output, $y)
     *
     * @return object|array|null
     */
    public static function get_row(string $query, string $output = 'OBJECT', int $y = 0): mixed
    {
        return self::driver()->get_row($query, $output, $y);
    }

    /**
     * Run a query and return a single scalar value from the first row/column.
     * Mirrors: $wpdb->get_var($query, $x, $y)
     *
     * @return string|null
     */
    public static function get_var(string $query, int $x = 0, int $y = 0): ?string
    {
        return self::driver()->get_var($query, $x, $y);
    }

    /**
     * Run a query and return a single column as a flat array.
     * Mirrors: $wpdb->get_col($query, $x)
     *
     * @return array<string>
     */
    public static function get_col(string $query, int $x = 0): array
    {
        return self::driver()->get_col($query, $x);
    }

    /**
     * Execute a raw SQL query (INSERT, UPDATE, DELETE, CREATE, etc.)
     * Returns number of rows affected, or false on error.
     * Mirrors: $wpdb->query($query)
     *
     * @return int|false
     */
    public static function query(string $query): int|false
    {
        return self::driver()->query($query);
    }

    // -------------------------------------------------------------------------
    // Write helpers
    // -------------------------------------------------------------------------

    /**
     * Insert a row. Returns number of rows inserted (1) or false on error.
     * Mirrors: $wpdb->insert($table, $data, $format)
     *
     * @param  string        $table   Table name (without prefix — use DB::prefix())
     * @param  array         $data    Column => value pairs
     * @param  array|null    $format  printf-style format strings per value (%s, %d, %f)
     * @return int|false
     */
    public static function insert(string $table, array $data, ?array $format = null): int|false
    {
        return self::driver()->insert($table, $data, $format);
    }

    /**
     * Update rows matching $where. Returns rows updated or false on error.
     * Mirrors: $wpdb->update($table, $data, $where, $format, $where_format)
     *
     * @return int|false
     */
    public static function update(
        string $table,
        array $data,
        array $where,
        ?array $format = null,
        ?array $where_format = null
    ): int|false {
        return self::driver()->update($table, $data, $where, $format, $where_format);
    }

    /**
     * Delete rows matching $where. Returns rows deleted or false on error.
     * Mirrors: $wpdb->delete($table, $where, $where_format)
     *
     * @return int|false
     */
    public static function delete(string $table, array $where, ?array $where_format = null): int|false
    {
        return self::driver()->delete($table, $where, $where_format);
    }

    /**
     * Insert or update (INSERT ... ON DUPLICATE KEY UPDATE).
     * Mirrors: $wpdb->replace($table, $data, $format)
     *
     * @return int|false
     */
    public static function replace(string $table, array $data, ?array $format = null): int|false
    {
        return self::driver()->replace($table, $data, $format);
    }

    // -------------------------------------------------------------------------
    // Properties (exposed as methods for driver portability)
    // -------------------------------------------------------------------------

    /**
     * Table prefix. Mirrors: $wpdb->prefix
     */
    public static function prefix(): string
    {
        return self::driver()->prefix();
    }

    /**
     * Auto-increment ID from the last INSERT. Mirrors: $wpdb->insert_id
     */
    public static function insert_id(): int
    {
        return self::driver()->insert_id();
    }

    /**
     * Error message from the last failed query. Mirrors: $wpdb->last_error
     */
    public static function last_error(): string
    {
        return self::driver()->last_error();
    }

    /**
     * Escape a string for use in a LIKE clause.
     * Mirrors: $wpdb->esc_like($text)
     */
    public static function esc_like(string $text): string
    {
        return self::driver()->esc_like($text);
    }

    /**
     * Return the charset collate string for CREATE TABLE statements.
     * Mirrors: $wpdb->get_charset_collate()
     */
    public static function get_charset_collate(): string
    {
        return self::driver()->get_charset_collate();
    }

    /**
     * WordPress core table names (posts, postmeta, users, terms, etc.)
     * Mirrors: $wpdb->posts, $wpdb->postmeta, etc.
     *
     * On standalone these are meaningless — call sites that use WP core tables
     * will be refactored away during the standalone migration.
     *
     * @param  string $name  e.g. 'posts', 'postmeta', 'users', 'terms',
     *                            'term_taxonomy', 'term_relationships'
     * @return string        Fully-qualified table name
     */
    public static function table(string $name): string
    {
        return self::driver()->table($name);
    }
}
