<?php

namespace PaymentPage\Model;

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

use Exception;

class Skeleton {

	public static $table;

	public static $fields;

	public static $identifier;

	public static $identifier_any = false;

	public static $timestamps = [ 'created_at' ];

	/**
	 * @param $data
	 *
	 * @return static
	 * @throws Exception
	 */
	public static function findOrCreate( $data ) {
		$model = new static( $data );

		$model->get();

		if ( null === $model->id ) {
			$model->insert();
		}

		return $model;
	}

	/**
	 * @param $data
	 *
	 * @return static
	 * @throws Exception
	 */
	public static function findOrFail( $data ) {
		$model = new static( $data );
		$model->get();

		if ( ! $model->exists() ) {
			/* translators: %s: database table name */
			throw new Exception( esc_html( sprintf( __( 'Failed to find %s.', 'payment-page' ), static::$table ) ) );
		}

		return $model;
	}

	/**
	 * @param array $where
	 *
	 * @return static[]
	 */
	public static function listWhere( array $where ): array {
		$query = 'SELECT * FROM ' . payment_page_wpdb()->prefix . static::$table;

		// (Audit S11) Allow-list for column names and comparison operators. Previously
		// $k (column name) and $compare (operator) were concatenated directly into SQL.
		// All current callers pass static keys so this was not exploitable today, but it
		// was a foot-gun for any future caller that passed user-controlled input.
		$allowed_columns   = self::_get_allowed_query_columns();
		$allowed_operators = array( '=', '!=', '<>', '<', '<=', '>', '>=', 'IN', 'NOT IN', 'LIKE' );

		if ( ! empty( $where ) ) {
			$query .= ' WHERE 1 = 1 ';

			foreach ( $where as $k => $v ) {
				if ( ! in_array( $k, $allowed_columns, true ) ) {
					// Silently skip unknown columns. Callers should pre-validate; this is
					// the last-line defense against second-order SQLi.
					continue;
				}

				if ( ! is_array( $v ) ) {
					$query .= ' AND `' . $k . '` = ' . payment_page_wpdb()->prepare( '%s', $v );

					continue;
				}

				foreach ( $v as $compare => $value ) {
					$compare_upper = strtoupper( (string) $compare );
					if ( ! in_array( $compare_upper, $allowed_operators, true ) ) {
						continue;
					}

					if ( is_array( $value ) && ! empty( $value ) ) {
						$value = payment_page_wpdb()->_escape( $value );

						$value = '("' . implode( '","', $value ) . '")';
					} else {
						$value = payment_page_wpdb()->prepare( '%s', $value );
					}

					$query .= ' AND `' . $k . '` ' . $compare_upper . ' ' . $value;
				}
			}
		}

		$entries = payment_page_wpdb()->get_results( $query, ARRAY_A );

		if ( empty( $entries ) ) {
			return [];
		}

		$response = [];

		foreach ( $entries as $entry ) {
			$response[] = new static( $entry );
		}

		return $response;
	}

	/**
	 * @param array $ids
	 * @param array $args
	 *
	 * @return static[]
	 */
	public static function listByIds( array $ids, array $args = [] ): array {
		$ids = array_map( 'intval', $ids );

		if ( empty( $ids ) ) {
			return [];
		}

		$query = 'SELECT * FROM ' . payment_page_wpdb()->prefix . static::$table . ' WHERE id IN (' . implode( ',', $ids ) . ')';

		// (Audit S11) order_by must be in the model's $fields allow-list; order must be
		// exactly 'ASC' or 'DESC'. Unknown column or invalid direction silently skipped.
		if ( isset( $args['order_by'] ) ) {
			$allowed_columns = self::_get_allowed_query_columns();
			$order_by        = (string) $args['order_by'];

			if ( in_array( $order_by, $allowed_columns, true ) ) {
				$direction = ( isset( $args['order'] ) && strtoupper( (string) $args['order'] ) === 'ASC' ) ? 'ASC' : 'DESC';
				$query    .= ' ORDER BY `' . $order_by . '` ' . $direction;
			}
		}

		$entries = payment_page_wpdb()->get_results( $query, ARRAY_A );

		if ( empty( $entries ) ) {
			return [];
		}

		$response = [];

		foreach ( $entries as $entry ) {
			$response[] = new static( $entry );
		}

		return $response;
	}

	/**
	 * Derive the allow-list of column names for safe query construction from the
	 * model's static $fields map. Always includes 'id' (every PP model has one).
	 * (Audit S11)
	 *
	 * @return string[]
	 */
	private static function _get_allowed_query_columns(): array {
		$fields = is_array( static::$fields ) ? array_keys( static::$fields ) : array();
		$fields[] = 'id';

		return array_values( array_unique( $fields ) );
	}

	protected $_db_data;

	public $id = null;

	public function __construct( ?array $data ) {
		if ( is_array( $data ) ) {
			$this->populate( $data );
		}
	}

	/**
	 * @param $data
	 *
	 * @return $this
	 */
	public function populate( $data ) {
		foreach ( $data as $k => $v ) {
			if ( ! property_exists( $this, $k ) ) {
				continue;
			}

			if ( is_null( $v ) ) {
				continue;
			}

			if ( isset( static::$fields[ $k ] ) ) {
				if ( static::$fields[ $k ] === 'int' ) {
					$v = intval( $v );
				} elseif ( static::$fields[ $k ] === 'json' ) {
					if ( is_string( $v ) ) {
						$v = json_decode( $v, true );
					}

					if ( ! is_array( $v ) ) {
						$v = [];
					}
				}
			} elseif ( $k === 'id' ) {
				$v = intval( $v );
			}

			$this->$k = $v;
		}

		return $this;
	}

	public function get() {
		$identifier = static::$identifier;

		if ( empty( $this->id ) && ( empty( $identifier ) || ( is_string( $identifier ) && empty( $this->$identifier ) ) ) ) {
			return;
		}

		$query = 'SELECT * FROM ' . payment_page_wpdb()->prefix . static::$table;

		if ( null !== $this->id ) {
			$query .= ' WHERE id = ' . $this->id;
		} else {
			if ( is_array( static::$identifier ) ) {
				if ( static::$identifier_any ) {
					$query .= ' WHERE 1 = 2';

					foreach ( static::$identifier as $current_identifier ) {
						if ( ! empty( $this->$current_identifier ) ) {
							$query .= ' OR ' . $current_identifier . ' = ' . payment_page_wpdb()->prepare( '%s', $this->$current_identifier );
						}
					}
				} else {
					$query .= ' WHERE 1 = 1';

					foreach ( static::$identifier as $current_identifier ) {
						$query .= ' AND ' . $current_identifier . ' = ' . payment_page_wpdb()->prepare( '%s', $this->$current_identifier );
					}
				}
			} else {
				$query .= ' WHERE ' . static::$identifier . ' = ' . payment_page_wpdb()->prepare( '%s', $this->$identifier );
			}
		}

		$response = payment_page_wpdb()->get_row( $query, ARRAY_A );

		if ( null === $response ) {
			return;
		}

		$this->_db_data = $response;

		$this->populate( $response );
	}

	public function exists(): bool {
		return ! empty( $this->_db_data );
	}

	/**
	 * @return $this
	 * @throws Exception
	 */
	public function insert() {
		$insert_data = [];

		foreach ( static::$fields as $field_key => $field_type ) {
			$current_field_value = $this->$field_key;

			if ( 'json' === $field_type && is_array( $current_field_value ) ) {
				$current_field_value = json_encode( $current_field_value );
			}

			$insert_data[ $field_key ] = $current_field_value;
		}

		if ( in_array( 'created_at', static::$timestamps ) ) {
			$insert_data['created_at'] = time();
		}

		if ( in_array( 'updated_at', static::$timestamps ) ) {
			$insert_data['updated_at'] = time();
		}

		if ( false !== payment_page_wpdb()->insert( payment_page_wpdb()->prefix . static::$table, $insert_data ) ) {
			$this->id = payment_page_wpdb()->insert_id;
		} else {
			/* translators: %1$s: database table name, %2$s: error message */
			throw new Exception( esc_html( sprintf( __( 'Failed to insert %1$s - %2$s.', 'payment-page' ), static::$table, payment_page_wpdb()->last_error ) ) );
		}

		return $this;
	}

	/**
	 * @return $this
	 * @throws Exception
	 */
	public function save() {
		if ( $this->id === null ) {
			return $this->insert();
		}

		$update_data = [];

		foreach ( static::$fields as $field_key => $field_type ) {
			$current_field_value = $this->$field_key;

			if ( 'json' === $field_type && is_array( $current_field_value ) ) {
				$current_field_value = json_encode( $current_field_value );
			}

			$update_data[ $field_key ] = $current_field_value;
		}

		foreach ( $update_data as $k => $v ) {
			if ( ! isset( $this->_db_data[ $k ] ) ) {
				continue;
			}

			if ( $this->_db_data[ $k ] == $v ) {
				unset( $update_data[ $k ] );

				continue;
			}

			if ( static::$fields[ $k ] === 'json' ) {
				$test_db_data   = is_array( $this->_db_data[ $k ] ) ? $this->_db_data[ $k ] : json_decode( $this->_db_data[ $k ], true );
				$test_save_data = is_array( $v ) ? $v : json_decode( $v, true );

				foreach ( $test_save_data as $save_data_key => $save_data_value ) {
					if ( isset( $test_db_data[ $save_data_key ] ) ) {
						if ( is_array( $test_db_data[ $save_data_key ] ) ) {
							$test_db_data[ $save_data_key ] = json_encode( $test_db_data[ $save_data_key ] );
						}

						if ( is_array( $save_data_value ) ) {
							$save_data_value = json_encode( $save_data_value );
						}

						if ( $test_db_data[ $save_data_key ] === $save_data_value ) {
							unset( $test_db_data[ $save_data_key ] );
							unset( $test_save_data[ $save_data_key ] );
						}
					}
				}

				if ( empty( $test_save_data ) && empty( $test_db_data ) ) {
					unset( $update_data[ $k ] );
				}
			}
		}

		if ( empty( $update_data ) ) {
			return $this;
		}

		if ( in_array( 'updated_at', static::$timestamps ) ) {
			$update_data['updated_at'] = time();
		}

		if ( false === payment_page_wpdb()->update( payment_page_wpdb()->prefix . static::$table, $update_data, [ 'id' => $this->id ] ) ) {
			/* translators: %1$s: database table name, %2$s: record ID */
			throw new Exception( esc_html( sprintf( __( 'Failed to update %1$s for id %2$s.', 'payment-page' ), static::$table, $this->id ) ) );
		}

		return $this;
	}

	public function array(): array {
		$response = [];

		foreach ( static::$fields as $field_key => $field_type ) {
			$response[ $field_key ] = ( $this->$field_key ?? null );
		}

		return $response;
	}

}
