<?php
/**
 * Secure Query Builder
 *
 * Provides a fluent interface for building secure SQL queries
 *
 * @package Care_API
 * @subpackage Utils
 * @version    1.0.0
 * @author     Descomplicar® <dev@descomplicar.pt>
 * @link       https://descomplicar.pt
 * @since      1.0.0
 */

namespace Care_API\Utils;

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

/**
 * Class Secure_Query_Builder
 *
 * Fluent interface for building secure SQL queries with automatic prepared statements
 *
 * @since 1.0.0
 */
class Secure_Query_Builder {

    /**
     * Query type
     *
     * @var string
     */
    private $query_type;

    /**
     * Table name
     *
     * @var string
     */
    private $table;

    /**
     * SELECT columns
     *
     * @var array
     */
    private $select = array();

    /**
     * WHERE conditions
     *
     * @var array
     */
    private $where = array();

    /**
     * JOIN clauses
     *
     * @var array
     */
    private $joins = array();

    /**
     * ORDER BY clauses
     *
     * @var array
     */
    private $order_by = array();

    /**
     * LIMIT clause
     *
     * @var int|null
     */
    private $limit;

    /**
     * OFFSET clause
     *
     * @var int|null
     */
    private $offset;

    /**
     * Query parameters
     *
     * @var array
     */
    private $parameters = array();

    /**
     * WordPress database instance
     *
     * @var wpdb
     */
    private $wpdb;

    /**
     * Allowed KiviCare tables
     *
     * @var array
     */
    private static $allowed_tables = array(
        'kc_clinics', 'kc_patients', 'kc_doctors', 'kc_appointments',
        'kc_patient_encounters', 'kc_prescription', 'kc_bills',
        'kc_services', 'kc_doctor_clinic_mappings', 'kc_patient_clinic_mappings',
        'kc_api_sessions'
    );

    /**
     * Allowed column patterns
     *
     * @var array
     */
    private static $allowed_columns = array(
        '/^[a-zA-Z0-9_]+$/',              // Basic column names
        '/^[a-zA-Z0-9_]+\.[a-zA-Z0-9_]+$/', // Table.column format
        '/^COUNT\([a-zA-Z0-9_.*]+\)$/',    // COUNT functions
        '/^MAX\([a-zA-Z0-9_.]+\)$/',       // MAX functions
        '/^MIN\([a-zA-Z0-9_.]+\)$/',       // MIN functions
        '/^SUM\([a-zA-Z0-9_.]+\)$/',       // SUM functions
        '/^AVG\([a-zA-Z0-9_.]+\)$/'        // AVG functions
    );

    /**
     * Constructor
     *
     * @since 1.0.0
     */
    public function __construct() {
        global $wpdb;
        $this->wpdb = $wpdb;
    }

    /**
     * Start a SELECT query
     *
     * @param array|string $columns Columns to select
     * @return self
     * @throws InvalidArgumentException If columns are invalid
     * @since 1.0.0
     */
    public function select( $columns = '*' ) {
        $this->query_type = 'SELECT';

        if ( $columns === '*' ) {
            $this->select = array( '*' );
        } elseif ( is_string( $columns ) ) {
            $this->select = array( $this->validate_column( $columns ) );
        } elseif ( is_array( $columns ) ) {
            $this->select = array_map( array( $this, 'validate_column' ), $columns );
        } else {
            throw new \InvalidArgumentException( 'Invalid columns parameter' );
        }

        return $this;
    }

    /**
     * Specify the table
     *
     * @param string $table Table name (without prefix)
     * @return self
     * @throws InvalidArgumentException If table is invalid
     * @since 1.0.0
     */
    public function from( $table ) {
        $this->table = $this->validate_table( $table );
        return $this;
    }

    /**
     * Add WHERE condition
     *
     * @param string $column   Column name
     * @param mixed  $value    Value or comparison operator
     * @param mixed  $value2   Second value for BETWEEN, IN, etc.
     * @param string $operator Operator (AND, OR)
     * @return self
     * @throws InvalidArgumentException If parameters are invalid
     * @since 1.0.0
     */
    public function where( $column, $value, $value2 = null, $operator = 'AND' ) {
        $column = $this->validate_column( $column );
        $operator = $this->validate_logical_operator( $operator );

        if ( is_array( $value ) ) {
            // IN clause
            $placeholders = implode( ', ', array_fill( 0, count( $value ), '%s' ) );
            $this->where[] = array(
                'clause' => "{$column} IN ({$placeholders})",
                'operator' => $operator,
                'params' => $value
            );
            $this->parameters = array_merge( $this->parameters, $value );
        } elseif ( $value2 !== null ) {
            // BETWEEN clause
            $this->where[] = array(
                'clause' => "{$column} BETWEEN %s AND %s",
                'operator' => $operator,
                'params' => array( $value, $value2 )
            );
            $this->parameters[] = $value;
            $this->parameters[] = $value2;
        } else {
            // Standard comparison
            $comparison = $this->determine_comparison_operator( $value );
            $this->where[] = array(
                'clause' => "{$column} {$comparison} %s",
                'operator' => $operator,
                'params' => array( $value )
            );
            $this->parameters[] = $value;
        }

        return $this;
    }

    /**
     * Add WHERE LIKE condition
     *
     * @param string $column   Column name
     * @param string $pattern  LIKE pattern
     * @param string $operator Logical operator (AND, OR)
     * @return self
     * @since 1.0.0
     */
    public function where_like( $column, $pattern, $operator = 'AND' ) {
        $column = $this->validate_column( $column );
        $operator = $this->validate_logical_operator( $operator );

        $this->where[] = array(
            'clause' => "{$column} LIKE %s",
            'operator' => $operator,
            'params' => array( $pattern )
        );
        $this->parameters[] = $pattern;

        return $this;
    }

    /**
     * Add JOIN clause
     *
     * @param string $table     Table to join
     * @param string $condition Join condition
     * @param string $type      Join type (INNER, LEFT, RIGHT)
     * @return self
     * @throws InvalidArgumentException If parameters are invalid
     * @since 1.0.0
     */
    public function join( $table, $condition, $type = 'INNER' ) {
        $table = $this->validate_table( $table );
        $type = $this->validate_join_type( $type );

        // Validate join condition format
        if ( ! preg_match( '/^[a-zA-Z0-9_.]+\s*=\s*[a-zA-Z0-9_.]+$/', $condition ) ) {
            throw new \InvalidArgumentException( 'Invalid join condition format' );
        }

        $this->joins[] = "{$type} JOIN {$table} ON {$condition}";

        return $this;
    }

    /**
     * Add ORDER BY clause
     *
     * @param string $column    Column name
     * @param string $direction Direction (ASC, DESC)
     * @return self
     * @throws InvalidArgumentException If parameters are invalid
     * @since 1.0.0
     */
    public function order_by( $column, $direction = 'ASC' ) {
        $column = $this->validate_column( $column );
        $direction = strtoupper( $direction );

        if ( ! in_array( $direction, array( 'ASC', 'DESC' ), true ) ) {
            throw new \InvalidArgumentException( 'Invalid ORDER BY direction' );
        }

        $this->order_by[] = "{$column} {$direction}";

        return $this;
    }

    /**
     * Set LIMIT clause
     *
     * @param int $limit Limit value
     * @return self
     * @throws InvalidArgumentException If limit is invalid
     * @since 1.0.0
     */
    public function limit( $limit ) {
        if ( ! is_int( $limit ) || $limit < 0 ) {
            throw new \InvalidArgumentException( 'Limit must be a non-negative integer' );
        }

        $this->limit = $limit;
        return $this;
    }

    /**
     * Set OFFSET clause
     *
     * @param int $offset Offset value
     * @return self
     * @throws InvalidArgumentException If offset is invalid
     * @since 1.0.0
     */
    public function offset( $offset ) {
        if ( ! is_int( $offset ) || $offset < 0 ) {
            throw new \InvalidArgumentException( 'Offset must be a non-negative integer' );
        }

        $this->offset = $offset;
        return $this;
    }

    /**
     * Build and execute the query
     *
     * @param string $output Output format (OBJECT, ARRAY_A, ARRAY_N)
     * @return mixed Query results
     * @throws InvalidArgumentException If query is invalid
     * @since 1.0.0
     */
    public function get( $output = OBJECT ) {
        $sql = $this->build_sql();

        if ( empty( $this->parameters ) ) {
            return $this->wpdb->get_results( $sql, $output );
        }

        $prepared_sql = $this->wpdb->prepare( $sql, $this->parameters );
        return $this->wpdb->get_results( $prepared_sql, $output );
    }

    /**
     * Get first row only
     *
     * @param string $output Output format
     * @return mixed Single row result
     * @since 1.0.0
     */
    public function first( $output = OBJECT ) {
        $this->limit( 1 );
        $results = $this->get( $output );

        return ! empty( $results ) ? $results[0] : null;
    }

    /**
     * Get single value
     *
     * @return mixed Single value result
     * @since 1.0.0
     */
    public function value() {
        $this->limit( 1 );
        $sql = $this->build_sql();

        if ( empty( $this->parameters ) ) {
            return $this->wpdb->get_var( $sql );
        }

        $prepared_sql = $this->wpdb->prepare( $sql, $this->parameters );
        return $this->wpdb->get_var( $prepared_sql );
    }

    /**
     * Get count of matching rows
     *
     * @return int Row count
     * @since 1.0.0
     */
    public function count() {
        $original_select = $this->select;
        $this->select = array( 'COUNT(*)' );

        $count = (int) $this->value();

        $this->select = $original_select;
        return $count;
    }

    /**
     * Build SQL query
     *
     * @return string Built SQL query
     * @throws InvalidArgumentException If query cannot be built
     * @since 1.0.0
     */
    private function build_sql() {
        if ( $this->query_type !== 'SELECT' ) {
            throw new \InvalidArgumentException( 'Only SELECT queries are currently supported' );
        }

        if ( empty( $this->table ) ) {
            throw new \InvalidArgumentException( 'Table must be specified' );
        }

        $sql = 'SELECT ' . implode( ', ', $this->select );
        $sql .= ' FROM ' . $this->table;

        // Add JOINs
        if ( ! empty( $this->joins ) ) {
            $sql .= ' ' . implode( ' ', $this->joins );
        }

        // Add WHERE conditions
        if ( ! empty( $this->where ) ) {
            $where_parts = array();

            foreach ( $this->where as $index => $condition ) {
                if ( $index === 0 ) {
                    $where_parts[] = $condition['clause'];
                } else {
                    $where_parts[] = $condition['operator'] . ' ' . $condition['clause'];
                }
            }

            $sql .= ' WHERE ' . implode( ' ', $where_parts );
        }

        // Add ORDER BY
        if ( ! empty( $this->order_by ) ) {
            $sql .= ' ORDER BY ' . implode( ', ', $this->order_by );
        }

        // Add LIMIT and OFFSET
        if ( $this->limit !== null ) {
            $sql .= ' LIMIT ' . $this->limit;

            if ( $this->offset !== null ) {
                $sql .= ' OFFSET ' . $this->offset;
            }
        }

        return $sql;
    }

    /**
     * Validate table name
     *
     * @param string $table Table name
     * @return string Full table name with prefix
     * @throws InvalidArgumentException If table is invalid
     * @since 1.0.0
     */
    private function validate_table( $table ) {
        if ( ! in_array( $table, self::$allowed_tables, true ) ) {
            throw new \InvalidArgumentException( "Table '{$table}' is not allowed" );
        }

        return $this->wpdb->prefix . $table;
    }

    /**
     * Validate column name
     *
     * @param string $column Column name
     * @return string Validated column name
     * @throws InvalidArgumentException If column is invalid
     * @since 1.0.0
     */
    private function validate_column( $column ) {
        foreach ( self::$allowed_columns as $pattern ) {
            if ( preg_match( $pattern, $column ) ) {
                return $column;
            }
        }

        throw new \InvalidArgumentException( "Invalid column name: {$column}" );
    }

    /**
     * Validate logical operator
     *
     * @param string $operator Logical operator
     * @return string Validated operator
     * @throws InvalidArgumentException If operator is invalid
     * @since 1.0.0
     */
    private function validate_logical_operator( $operator ) {
        $operator = strtoupper( $operator );

        if ( ! in_array( $operator, array( 'AND', 'OR' ), true ) ) {
            throw new \InvalidArgumentException( "Invalid logical operator: {$operator}" );
        }

        return $operator;
    }

    /**
     * Validate JOIN type
     *
     * @param string $type JOIN type
     * @return string Validated JOIN type
     * @throws InvalidArgumentException If JOIN type is invalid
     * @since 1.0.0
     */
    private function validate_join_type( $type ) {
        $type = strtoupper( $type );

        if ( ! in_array( $type, array( 'INNER', 'LEFT', 'RIGHT' ), true ) ) {
            throw new \InvalidArgumentException( "Invalid JOIN type: {$type}" );
        }

        return $type;
    }

    /**
     * Determine comparison operator based on value
     *
     * @param mixed $value Value to compare
     * @return string Comparison operator
     * @since 1.0.0
     */
    private function determine_comparison_operator( $value ) {
        if ( $value === null ) {
            return 'IS';
        }

        return '=';
    }

    /**
     * Reset builder state
     *
     * @return self
     * @since 1.0.0
     */
    public function reset() {
        $this->query_type = null;
        $this->table = null;
        $this->select = array();
        $this->where = array();
        $this->joins = array();
        $this->order_by = array();
        $this->limit = null;
        $this->offset = null;
        $this->parameters = array();

        return $this;
    }
}