<?php

namespace WC_Affiliate\Abstracts;

use WP_Error;

defined( 'ABSPATH' ) || exit;

/**
 * Abstract Model Class
 *
 * Base class for all models in the WC Affiliate plugin.
 * Provides common functionality and enforces structure across models.
 *
 * @since   1.0.0
 * @package WC_Affiliate\Abstracts
 */
abstract class Model {

	/**
	 * Primary key ID
	 *
	 * @var int|null
	 */
	protected ?int $id = 0;

	/**
	 * Model data
	 *
	 * @var array
	 */
	protected array $data = array();

	/**
	 * Database table name (without prefix)
	 *
	 * @var ?string
	 */
	protected static ?string $table_name;

	/**
	 * Primary key column name
	 *
	 * @var string
	 */
	protected static string $primary_key = 'id';

	/**
	 * WordPress database object
	 *
	 * @var \wpdb
	 */
	protected \wpdb $wpdb;

	/**
	 * Whether the model exists in the database
	 *
	 * @var bool
	 */
	protected bool $exists = false;

	/**
	 * Model constructor
	 *
	 * @param int|null $id The model ID.
	 */
	public function __construct( ?int $id = null ) {
		global $wpdb;
		$this->wpdb = $wpdb;

		if ( $id ) {
			$this->id = (int) $id;
			$this->load();
		}
	}

	/**
	 * Get the model ID
	 *
	 * @return int|null
	 */
	public function get_id(): ?int {
		return $this->id;
	}

	/**
	 * Set the model ID
	 *
	 * @param int $id The model ID.
	 *
	 * @return self
	 */
	public function set_id( int $id ): Model {
		$this->id = (int) $id;

		return $this;
	}

	/**
	 * Check if the model exists
	 *
	 * @return bool
	 */
	public function exists(): bool {
		return $this->exists;
	}

	/**
	 * Get all model data
	 *
	 * @return array
	 */
	public function get_data(): array {
		return $this->data;
	}

	/**
	 * Set model data
	 *
	 * @param array $data The data to set.
	 *
	 * @return self
	 */
	public function set_data( array $data ): Model {
		$this->data = $data;

		return $this;
	}

	/**
	 * Get a specific data property
	 *
	 * @param string $key     The data key.
	 * @param mixed  $default Default value if key doesn't exist.
	 *
	 * @return mixed
	 */
	public function get( string $key, $default = null ) {
		return $this->data[ $key ] ?? $default;
	}

	/**
	 * Set a specific data property
	 *
	 * @param string $key   The data key.
	 * @param mixed  $value The value to set.
	 *
	 * @return self
	 */
	public function set( string $key, $value ): Model {
		$this->data[ $key ] = $value;

		return $this;
	}

	/**
	 * Get the full table name with WordPress prefix
	 *
	 * @return string
	 */
	public function get_table_name(): string {
		return $this->wpdb->prefix . static::$table_name;
	}

	/**
	 * Load model data from database
	 *
	 * @return void True if data was loaded, false otherwise
	 */
	protected function load(): void {
		if ( ! $this->id || ! static::$table_name ) {
			return;
		}

		$table       = $this->get_table_name();
		$primary_key = static::$primary_key;

		$row = $this->wpdb->get_row(
			$this->wpdb->prepare(
				"SELECT * FROM {$table} WHERE {$primary_key} = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$this->id
			),
			ARRAY_A
		);

		if ( $row ) {
			$this->data   = $row;
			$this->exists = true;
		}
	}

	/**
	 * Save the model to database
	 *
	 * @return bool|int False on failure, true on update, or insert ID on insert
	 */
	public function save() {
		if ( ! static::$table_name ) {
			return false;
		}

		$table       = $this->get_table_name();
		$primary_key = static::$primary_key;

		// Prepare data for database.
		$data = $this->prepare_data_for_save();

		if ( $this->exists() && $this->id ) {
			// Update existing record.
			$result = $this->wpdb->update(
				$table,
				$data,
				array( $primary_key => $this->id ),
				$this->get_data_format( $data ),
				array( '%d' )
			);

			return false !== $result;
		} else {
			// Insert new record.
			$result = $this->wpdb->insert(
				$table,
				$data,
				$this->get_data_format( $data )
			);

			if ( $result ) {
				$this->id     = $this->wpdb->insert_id;
				$this->exists = true;

				return $this->id;
			}

			return false;
		}
	}

	/**
	 * Delete the model from database
	 *
	 * @return WP_Error|bool True on success, WP_Error on failure
	 */
	public function delete() {
		if ( ! $this->exists() || ! $this->id || ! static::$table_name ) {
			return new WP_Error( 'model_not_found', __( 'Model not found or does not exist.', 'wc-affiliate' ) );
		}

		$table       = $this->get_table_name();
		$primary_key = static::$primary_key;

		$result = $this->wpdb->delete(
			$table,
			array( $primary_key => $this->id ),
			array( '%d' )
		);

		if ( $result ) {
			$this->exists = false;
			$this->id     = null;
			$this->data   = array();

			return true;
		}

		return new WP_Error( 'delete_failed', __( 'Failed to delete model.', 'wc-affiliate' ) );
	}

	/**
	 * Prepare data for saving to database
	 * Override this method in child classes to customize data preparation
	 *
	 * @return array
	 */
	protected function prepare_data_for_save(): array {
		$data = $this->data;

		// Remove primary key from data if it exists.
		if ( isset( $data[ static::$primary_key ] ) ) {
			unset( $data[ static::$primary_key ] );
		}

		return $data;
	}

	/**
	 * Get data format for wpdb operations
	 * Override this method in child classes to customize data formats
	 *
	 * @param array $data The data array.
	 *
	 * @return array
	 */
	protected function get_data_format( array $data ): array {
		$format = array();

		foreach ( $data as $key => $value ) {
			if ( is_int( $value ) ) {
				$format[] = '%d';
			} elseif ( is_float( $value ) ) {
				$format[] = '%f';
			} else {
				$format[] = '%s';
			}
		}

		return $format;
	}

	/**
	 * Find a model by ID
	 *
	 * @param int $id The model ID.
	 *
	 * @return static|null
	 */
	public static function find( int $id ) {
		$instance = new static( $id );

		return $instance->exists() ? $instance : null;
	}

	/**
	 * Get multiple models by criteria
	 *
	 * @param array $args Query arguments.
	 *
	 * @return array
	 */
	public static function find_all( array $args = array() ): array {
		global $wpdb;

		if ( ! static::$table_name ) {
			return array();
		}

		$defaults = array(
			'limit'   => 20,
			'offset'  => 0,
			'orderby' => static::$primary_key,
			'order'   => 'DESC',
			'where'   => array(),
		);

		$args  = wp_parse_args( $args, $defaults );
		$table = $wpdb->prefix . static::$table_name;

		// Build WHERE clause.
		$where_clause = '1=1';
		$where_values = array();

		if ( ! empty( $args['where'] ) ) {
			foreach ( $args['where'] as $column => $value ) {
				$where_clause  .= " AND {$column} = %s";
				$where_values[] = $value;
			}
		}

		// Build ORDER BY clause.
		$order_clause = sprintf(
			'ORDER BY %s %s',
			sanitize_sql_orderby( $args['orderby'] ),
			strtoupper( $args['order'] ) === 'ASC' ? 'ASC' : 'DESC'
		);

		// Build LIMIT clause.
		$limit_clause = sprintf(
			'LIMIT %d OFFSET %d',
			(int) $args['limit'],
			(int) $args['offset']
		);

		$query = "SELECT * FROM {$table} WHERE {$where_clause} {$order_clause} {$limit_clause}";

		if ( ! empty( $where_values ) ) {
			$query = $wpdb->prepare( $query, $where_values );
		}

		$results = $wpdb->get_results( $query, ARRAY_A );
		$models  = array();

		foreach ( $results as $row ) {
			$model         = new static();
			$model->data   = $row;
			$model->id     = (int) $row[ static::$primary_key ];
			$model->exists = true;
			$models[]      = $model;
		}

		return $models;
	}

	/**
	 * Count models by criteria
	 *
	 * @param array $where Where conditions.
	 *
	 * @return int
	 */
	public static function count( array $where = array() ): int {
		global $wpdb;

		if ( ! static::$table_name ) {
			return 0;
		}

		$table        = $wpdb->prefix . static::$table_name;
		$where_clause = '1=1';
		$where_values = array();

		if ( ! empty( $where ) ) {
			foreach ( $where as $column => $value ) {
				$where_clause  .= " AND {$column} = %s";
				$where_values[] = $value;
			}
		}

		$query = "SELECT COUNT(*) FROM {$table} WHERE {$where_clause}";

		if ( ! empty( $where_values ) ) {
			$query = $wpdb->prepare( $query, $where_values );
		}

		return (int) $wpdb->get_var( $query );
	}

	/**
	 * Convert model to array
	 *
	 * @return array
	 */
	public function to_array(): array {
		$data                         = $this->data;
		$data[ static::$primary_key ] = $this->id;

		return $data;
	}

	/**
	 * Convert model to JSON
	 *
	 * @return string
	 */
	public function to_json(): string {
		return wp_json_encode( $this->to_array() );
	}
}
