<?php
declare( strict_types=1 );

namespace FiboFilters\Settings;

use FiboFilters\Config\Env;
use FiboFilters\Util\Cache;
use FiboFilters\Util\DbUtil;
use wpdb;

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

/**
 * Storage handler for plugin data that shouldn't be stored in the wp_options table because of length.
 *
 * Creates, updates, deletes and retrieves records from the fibofilters_storage table
 * with optional object‑cache integration.
 */
class Storage {

	public const STORAGE_DB_NAME = 'fibofilters_storage';
	public const DB_VERSION      = 1;

	/**
	 * Helper for charset / collation utils.
	 *
	 * @var \FiboFilters\Util\DbUtil
	 */
	private DbUtil $db_util;

	/**
	 * Cache utility instance.
	 *
	 * @var Cache
	 */
	protected Cache $cache;

	/**
	 * Class constructor.
	 *
	 * @param DbUtil $db_util Database utilities helper.
	 * @param Cache  $cache    Cache utility.
	 */
	public function __construct( DbUtil $db_util, Cache $cache ) {
		$this->db_util = $db_util;
		$this->cache   = $cache;
	}

	/**
	 * List of keys allowed to be saved / retrieved.
	 *
	 * @return array<string> Whitelisted keys.
	 */
	private function get_whitelist_keys(): array {
		return [
			'filters',
			'indexer_last_build_logs',
			'indexer_last_build_logs_tmp',
			'seo_rules',
		];
	}

	/**
	 * Checks whether a key is on the allow‑list.
	 *
	 * @param string $key Key to check.
	 *
	 * @return bool True when key is whitelisted.
	 */
	public function is_key_whitelisted( string $key ): bool {
		return in_array( $key, $this->get_whitelist_keys(), true );
	}

	/**
	 * Returns cache group name.
	 *
	 * @return string Cache group.
	 */
	public function get_cache_group(): string {
		return 'storage';
	}

	/**
	 * Returns the fully‑qualified table name (with `$wpdb->prefix`).
	 *
	 * @return string Fully‑qualified table name.
	 * @global wpdb $wpdb WordPress database abstraction object.
	 */
	public function get_table_name(): string {
		global $wpdb;

		return $wpdb->prefix . self::STORAGE_DB_NAME;
	}

	/**
	 * Adds table references to the global `$wpdb` object.
	 *
	 * @return void
	 * @global wpdb $wpdb WordPress database abstraction object.
	 */
	public function register_tables(): void {
		global $wpdb;

		$wpdb->fibofilters_storage = $this->get_table_name();
		$wpdb->tables[]            = self::STORAGE_DB_NAME;
	}

	/**
	 * Ensures the custom table exists and is up‑to‑date.
	 *
	 * If the stored DB version differs from the expected version, the table
	 * will be (re)created via {@see self::create()}.
	 *
	 * @return void
	 */
	public function maybe_create(): void {
		if ( get_option( Env::$plugin_prefix . 'storage_db_version' ) !== self::DB_VERSION ) {
			update_option( Env::$plugin_prefix . 'storage_db_version', self::DB_VERSION );
			$this->create();
		}
	}

	/**
	 *  DB structure for the storage table.
	 *
	 * @return string SQL.
	 */
	public function table_struct(): string {
		$table_name = $this->get_table_name();
		$collate    = $this->db_util->get_collate( $table_name );

		return "CREATE TABLE $table_name (
                name            VARCHAR(255)   NOT NULL,
                content         LONGTEXT       NOT NULL,
                lang            VARCHAR(7)     NOT NULL DEFAULT '',
                PRIMARY KEY (`name`, `lang`)
            ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC $collate;";
	}

	/**
	 * Creates the database table.
	 * Includes WordPress's upgrade functions (dbDelta) for schema changes.
	 *
	 * @return void
	 */
	public function create(): void {
		if ( ! function_exists( 'dbDelta' ) ) {
			require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		}
		dbDelta( $this->table_struct() );
	}

	/**
	 * Drops the Storage database.
	 *
	 * @return void
	 */
	public function drop(): void {
		global $wpdb;

		$wpdb->query(
			$wpdb->prepare(
				'DROP TABLE IF EXISTS %i', // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder
				$this->get_table_name()
			)
		);
	}

	/**
	 * Saves a record in the storage table.
	 *
	 * Inserts a row identified by the given key.
	 * Returns `true` when the operation was successful, otherwise `false`.
	 *
	 * @param string $key     Unique content identifier.
	 * @param string $content Content (JSON‐encoded or plain string).
	 * @param string $lang    Language.
	 *
	 * @return bool           Whether the row was written successfully.
	 */
	public function save( string $key, string $content, string $lang = '' ): bool {
		global $wpdb;

		// Break early if the key is not on the allow-list.
		if ( ! $this->is_key_whitelisted( $key ) ) {
			return false;
		}

		$table = $this->get_table_name();

		$data_to_insert = [
			'name'    => $key,
			'content' => $content,
			'lang'    => $lang,
		];

		$format = [
			'%s', // name.
			'%s', // content.
			'%s', // lang.
		];

		$inserted_rows = $wpdb->insert( $table, $data_to_insert, $format );

		return is_int( $inserted_rows ) && $inserted_rows > 0;
	}

	/**
	 * Updates an existing Storage entry by deleting the old record and saving the new one.
	 *
	 * @param string $key Unique content identifier.
	 * @param string $content Content (JSON‐encoded or plain string).
	 * @param string $lang Language.
	 *
	 * @return bool         True on success, false on failure.
	 */
	public function update( string $key, string $content, string $lang = '' ): bool {

		// Break early if the key is not on the allow-list.
		if ( ! $this->is_key_whitelisted( $key ) ) {
			return false;
		}

		$old_value = $this->get( $key, $lang, true );

		$this->delete( $key, $lang );

		$saved = $this->save( $key, $content, $lang );

		if ( $saved ) {
			do_action( "fibofilters/storage/update/{$key}", $old_value, $content, $lang );
		}

		return $saved;
	}

	/**
	 * Deletes a Storage row by name.
	 *
	 * @param string $key Entry key to delete.
	 * @param string $lang Language.
	 *
	 * @return bool   True when at least one row is removed, false on failure or when
	 *                no matching row exists.
	 */
	public function delete( string $key, string $lang = '' ): bool {
		global $wpdb;

		// Break early if the key is not on the allow-list.
		if ( ! $this->is_key_whitelisted( $key ) ) {
			return false;
		}

		$table = $this->get_table_name();

		$result = $wpdb->delete(
			$table,
			[
				'name' => $key,
				'lang' => $lang,

			],
			[ '%s', '%s' ]
		);

		return is_int( $result ) && $result > 0;
	}

	/**
	 * Retrieves a cached or database-stored string.
	 *
	 * @param string $key Name of the content.
	 * @param string $lang Language.
	 * @param bool   $skip_cache Whether to skip the cache lookup.
	 *
	 * @return string The stored translation content, or an empty string on failure.
	 */
	public function get( string $key, string $lang = '', bool $skip_cache = false ): string {
		global $wpdb;

		// Break early if the key is not on the allow-list.
		if ( ! $this->is_key_whitelisted( $key ) ) {
			return '';
		}

		$table = $this->get_table_name();

		$cache_key = $key . ( $lang ? '_' . $lang : '' );

		$cached = $skip_cache ? false : $this->cache->get( $cache_key, $this->get_cache_group() );

		if ( $cached !== false ) {
			return $cached;
		}

		$where = $wpdb->prepare( ' AND name = %s', $key );

		if ( $lang !== '' ) {
			$where .= $wpdb->prepare( ' AND lang = %s', $lang );
		}

		$sql = "
		SELECT content
		FROM {$table}
		WHERE 1 = 1
			{$where}
	";

		$result = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		$content = '';
		if ( is_string( $result ) && $result !== '' ) {
			$content = $result;
			$this->cache->set( $cache_key, $content, $this->get_cache_group() );
		}

		return $content;
	}
}
