<?php
/**
 * The DB interface.
 *
 * @since      0.9.0
 * @package    RankMath
 * @subpackage RankMath\Links
 * @author     Rank Math <support@rankmath.com>
 */

namespace RankMath\Links;

use RankMath\Helpers\DB as DB_Helper;

defined( 'ABSPATH' ) || exit;

/**
 * Storage class.
 *
 * @copyright Copyright (C) 2008-2019, Yoast BV
 * The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
 */
class Storage {

	/**
	 * Get query builder.
	 *
	 * @return \RankMath\Admin\Database\Query_Builder
	 */
	public function table() {
		return \RankMath\Helpers\DB::query_builder( 'rank_math_internal_links' );
	}

	/**
	 * Removes all data for a given post.
	 *
	 * @param  int $post_id The post ID to remove the records for.
	 *
	 * @return int|false The number of rows updated, or false on error.
	 */
	public function cleanup( $post_id ) {
		return $this->table()->where( 'post_id', $post_id )->delete();
	}

	/**
	 * Get array of links from the database for given post.
	 *
	 * @param int $post_id The post to get the links for.
	 *
	 * @return Link[] The links array.
	 */
	public function get_links( $post_id ) {
		$links   = [];
		$results = $this->table()
			->select( [ 'url', 'post_id', 'target_post_id', 'type' ] )
			->where( 'post_id', $post_id )
			->get();

		foreach ( $results as $link ) {
			$links[] = new Link( $link->url, $link->target_post_id, $link->type );
		}

		return $links;
	}

	/**
	 * Save links for a post.
	 *
	 * @param integer $post_id The post ID to save.
	 * @param Link[]  $links   The links to save.
	 *
	 * @return void
	 */
	public function save_links( $post_id, array $links ) {
		// Reduces lock count from N to 1, reducing lock duration by ~95%.
		if ( empty( $links ) ) {
			return;
		}

		/**
		 * Filter: Allow extensions to override link saving behavior.
		 *
		 * Return non-null to bypass default save and handle it yourself.
		 *
		 * @param null|bool $override  Whether to override. Return true to bypass default save.
		 * @param int       $post_id   Post ID.
		 * @param array     $links     Array of Link objects to save.
		 * @param Storage   $storage   Storage instance for table access.
		 */
		$override = apply_filters( 'rank_math/links/save_links', null, $post_id, $links, $this );
		if ( null !== $override ) {
			return;
		}

		global $wpdb;
		$table = $wpdb->prefix . 'rank_math_internal_links';

		// Build VALUES for batch INSERT.
		$values = [];
		foreach ( $links as $link ) {
			$values[] = $wpdb->prepare(
				'(%s, %d, %d, %s)',
				$link->get_url(),
				$post_id,
				$link->get_target_post_id(),
				$link->get_type()
			);
		}

		// Execute single INSERT with multiple VALUES.
		$wpdb->query(
			"INSERT INTO {$table} (url, post_id, target_post_id, type) VALUES " . implode( ', ', $values )
		);
	}

	/**
	 * Update the link counts for a post and its referenced posts.
	 *
	 * @param int      $post_id Post to update.
	 * @param int|null $counts  Links count.
	 * @param Link[]   $links   Links to update incoming link count.
	 */
	public function update_link_counts( $post_id, $counts, array $links ) {
		$counts = wp_parse_args(
			$counts,
			[
				'internal_link_count' => 0,
				'external_link_count' => 0,
			]
		);

		$this->save_meta_data( $post_id, $counts );
		$this->update_incoming_links( $post_id, $links );
	}

	/**
	 * Update the incoming link count.
	 *
	 * @param int    $post_id Post which is processed.
	 * @param Link[] $links   Links we need to update the incoming link count of.
	 *
	 * @return void
	 */
	public function update_incoming_links( $post_id, $links ) {
		$post_ids = $this->get_internal_post_ids( $links );
		$post_ids = array_merge( [ $post_id ], $post_ids );
		$this->update_incoming_link_count( $post_ids );
	}

	/**
	 * Get post IDs from the link objects.
	 *
	 * @param Link[] $links Links we need to update the incoming link count of.
	 *
	 * @return int[] List of post IDs.
	 */
	protected function get_internal_post_ids( $links ) {
		$post_ids = [];
		foreach ( $links as $link ) {
			$post_ids[] = $link->get_target_post_id();
		}

		return array_filter( $post_ids );
	}

	/**
	 * Update the incoming link count.
	 *
	 * @param array $post_ids The posts to update the link count for.
	 */
	public function update_incoming_link_count( array $post_ids ) {
		$results = $this->table()
			->selectCount( 'id', 'incoming' )
			->select( 'target_post_id as post_id' )
			->whereIn( 'target_post_id', $post_ids )
			->groupBy( 'target_post_id' )->get();

		$post_ids_non_zero = [];
		foreach ( $results as $result ) {
			$this->save_meta_data( $result->post_id, [ 'incoming_link_count' => $result->incoming ] );
			$post_ids_non_zero[] = $result->post_id;
		}

		$post_ids_zero = array_diff( $post_ids, $post_ids_non_zero );
		foreach ( $post_ids_zero as $post_id ) {
			$this->save_meta_data( $post_id, [ 'incoming_link_count' => 0 ] );
		}
	}

	/**
	 * Save the link count to the database.
	 *
	 * Previous SELECT-then-INSERT/UPDATE pattern caused deadlocks when
	 * multiple posts linked to the same target post.
	 *
	 * @param int   $post_id   The ID to save the link count for.
	 * @param array $meta_data The total amount of links.
	 */
	public function save_meta_data( $post_id, array $meta_data ) {
		global $wpdb;

		$table = $wpdb->prefix . 'rank_math_internal_meta';

		// Build field assignments for ON DUPLICATE KEY UPDATE.
		$allowed_keys = [ 'incoming_link_count', 'internal_link_count', 'external_link_count' ];
		$update_parts = [];
		foreach ( $meta_data as $key => $value ) {
			if ( ! in_array( $key, $allowed_keys, true ) ) {
				continue;
			}
			$update_parts[] = $wpdb->prepare( "{$key} = %d", $value );
		}

		// Single operation eliminates SELECT-then-INSERT gap.
		return $wpdb->query(
			$wpdb->prepare(
				"INSERT INTO {$table}
				(object_id, incoming_link_count, internal_link_count, external_link_count)
				VALUES (%d, %d, %d, %d)
				ON DUPLICATE KEY UPDATE " . implode( ', ', $update_parts ),
				$post_id,
				$meta_data['incoming_link_count'] ?? 0,
				$meta_data['internal_link_count'] ?? 0,
				$meta_data['external_link_count'] ?? 0
			)
		);
	}
}
