<?php
namespace Activitypub\Collection;

use WP_Error;
use WP_Query;
use Activitypub\Http;
use Activitypub\Webfinger;
use Activitypub\Model\Follower;

use function Activitypub\is_tombstone;
use function Activitypub\get_remote_metadata_by_actor;

/**
 * ActivityPub Followers Collection
 *
 * @author Matt Wiebe
 * @author Matthias Pfefferle
 */
class Followers {
	const POST_TYPE = 'ap_follower';
	const CACHE_KEY_INBOXES = 'follower_inboxes_%s';

	/**
	 * Add new Follower
	 *
	 * @param int    $user_id The ID of the WordPress User
	 * @param string $actor   The Actor URL
	 *
	 * @return array|WP_Error The Follower (WP_Post array) or an WP_Error
	 */
	public static function add_follower( $user_id, $actor ) {
		$meta = get_remote_metadata_by_actor( $actor );

		if ( is_tombstone( $meta ) ) {
			return $meta;
		}

		if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
			return new WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'activitypub' ), array( 'status' => 400 ) );
		}

		$follower = new Follower();
		$follower->from_array( $meta );

		$id = $follower->upsert();

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$post_meta = get_post_meta( $id, 'activitypub_user_id' );

		// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
		if ( is_array( $post_meta ) && ! in_array( $user_id, $post_meta ) ) {
			add_post_meta( $id, 'activitypub_user_id', $user_id );
			wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
		}

		return $follower;
	}

	/**
	 * Remove a Follower
	 *
	 * @param int    $user_id The ID of the WordPress User
	 * @param string $actor   The Actor URL
	 *
	 * @return bool|WP_Error True on success, false or WP_Error on failure.
	 */
	public static function remove_follower( $user_id, $actor ) {
		wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );

		$follower = self::get_follower( $user_id, $actor );

		if ( ! $follower ) {
			return false;
		}

		return delete_post_meta( $follower->get__id(), 'activitypub_user_id', $user_id );
	}

	/**
	 * Get a Follower.
	 *
	 * @param int    $user_id The ID of the WordPress User
	 * @param string $actor   The Actor URL
	 *
	 * @return \Activitypub\Model\Follower|null The Follower object or null
	 */
	public static function get_follower( $user_id, $actor ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$post_id = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = 'activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s",
				array(
					esc_sql( self::POST_TYPE ),
					esc_sql( $user_id ),
					esc_sql( $actor ),
				)
			)
		);

		if ( $post_id ) {
			$post = get_post( $post_id );
			return Follower::init_from_cpt( $post );
		}

		return null;
	}

	/**
	 * Get a Follower by Actor indepenent from the User.
	 *
	 * @param string $actor The Actor URL.
	 *
	 * @return \Activitypub\Model\Follower|null The Follower object or null
	 */
	public static function get_follower_by_actor( $actor ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$post_id = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT ID FROM $wpdb->posts WHERE guid=%s",
				esc_sql( $actor )
			)
		);

		if ( $post_id ) {
			$post = get_post( $post_id );
			return Follower::init_from_cpt( $post );
		}

		return null;
	}

	/**
	 * Get the Followers of a given user
	 *
	 * @param int    $user_id The ID of the WordPress User.
	 * @param int    $number  Maximum number of results to return.
	 * @param int    $page    Page number.
	 * @param array  $args    The WP_Query arguments.
	 * @return array List of `Follower` objects.
	 */
	public static function get_followers( $user_id, $number = -1, $page = null, $args = array() ) {
		$data = self::get_followers_with_count( $user_id, $number, $page, $args );
		return $data['followers'];
	}

	/**
	 * Get the Followers of a given user, along with a total count for pagination purposes.
	 *
	 * @param int    $user_id The ID of the WordPress User.
	 * @param int    $number  Maximum number of results to return.
	 * @param int    $page    Page number.
	 * @param array  $args    The WP_Query arguments.
	 *
	 * @return array
	 *               followers List of `Follower` objects.
	 *               total     Total number of followers.
	 */
	public static function get_followers_with_count( $user_id, $number = -1, $page = null, $args = array() ) {
		$defaults = array(
			'post_type'      => self::POST_TYPE,
			'posts_per_page' => $number,
			'paged'          => $page,
			'orderby'        => 'ID',
			'order'          => 'DESC',
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
			'meta_query'     => array(
				array(
					'key'   => 'activitypub_user_id',
					'value' => $user_id,
				),
			),
		);

		$args = wp_parse_args( $args, $defaults );
		$query = new WP_Query( $args );
		$total = $query->found_posts;
		$followers = array_map(
			function( $post ) {
				return Follower::init_from_cpt( $post );
			},
			$query->get_posts()
		);
		return compact( 'followers', 'total' );
	}

	/**
	 * Get all Followers
	 *
	 * @param array $args The WP_Query arguments.
	 *
	 * @return array The Term list of Followers.
	 */
	public static function get_all_followers() {
		$args = array(
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
			'meta_query' => array(
				'relation' => 'AND',
				array(
					'key'     => 'activitypub_inbox',
					'compare' => 'EXISTS',
				),
				array(
					'key'     => 'activitypub_actor_json',
					'compare' => 'EXISTS',
				),
			),
		);
		return self::get_followers( null, null, null, $args );
	}

	/**
	 * Count the total number of followers
	 *
	 * @param int $user_id The ID of the WordPress User
	 *
	 * @return int The number of Followers
	 */
	public static function count_followers( $user_id ) {
		$query = new WP_Query(
			array(
				'post_type'  => self::POST_TYPE,
				'fields'     => 'ids',
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
				'meta_query' => array(
					'relation' => 'AND',
					array(
						'key'   => 'activitypub_user_id',
						'value' => $user_id,
					),
					array(
						'key'     => 'activitypub_inbox',
						'compare' => 'EXISTS',
					),
					array(
						'key'     => 'activitypub_actor_json',
						'compare' => 'EXISTS',
					),
				),
			)
		);

		return $query->found_posts;
	}

	/**
	 * Returns all Inboxes fo a Users Followers
	 *
	 * @param int $user_id The ID of the WordPress User
	 *
	 * @return array The list of Inboxes
	 */
	public static function get_inboxes( $user_id ) {
		$cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id );
		$inboxes = wp_cache_get( $cache_key, 'activitypub' );

		if ( $inboxes ) {
			return $inboxes;
		}

		// get all Followers of a ID of the WordPress User
		$posts = new WP_Query(
			array(
				'post_type'  => self::POST_TYPE,
				'fields'     => 'ids',
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
				'meta_query' => array(
					'relation' => 'AND',
					array(
						'key'     => 'activitypub_inbox',
						'compare' => 'EXISTS',
					),
					array(
						'key'   => 'activitypub_user_id',
						'value' => $user_id,
					),
					array(
						'key'     => 'activitypub_inbox',
						'value'   => '',
						'compare' => '!=',
					),
				),
			)
		);

		$posts = $posts->get_posts();

		if ( ! $posts ) {
			return array();
		}

		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$results = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT DISTINCT meta_value FROM {$wpdb->postmeta}
				WHERE post_id IN (" . implode( ', ', array_fill( 0, count( $posts ), '%d' ) ) . ")
				AND meta_key = 'activitypub_inbox'
				AND meta_value IS NOT NULL",
				$posts
			)
		);

		$inboxes = array_filter( $results );
		wp_cache_set( $cache_key, $inboxes, 'activitypub' );

		return $inboxes;
	}

	/**
	 * Get all Followers that have not been updated for a given time
	 *
	 * @param enum $output     The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT.
	 * @param int  $number     Limits the result.
	 * @param int  $older_than The time in seconds.
	 *
	 * @return mixed The Term list of Followers, the format depends on $output.
	 */
	public static function get_outdated_followers( $number = 50, $older_than = 86400 ) {
		$args = array(
			'post_type'      => self::POST_TYPE,
			'posts_per_page' => $number,
			'orderby'        => 'modified',
			'order'          => 'ASC',
			'post_status'    => 'any', // 'any' includes 'trash
			'date_query'     => array(
				array(
					'column' => 'post_modified_gmt',
					'before' => gmdate( 'Y-m-d', \time() - $older_than ),
				),
			),
		);

		$posts = new WP_Query( $args );
		$items = array();

		foreach ( $posts->get_posts() as $follower ) {
			$items[] = Follower::init_from_cpt( $follower ); // phpcs:ignore
		}

		return $items;
	}

	/**
	 * Get all Followers that had errors
	 *
	 * @param enum    $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT
	 * @param integer $number The number of Followers to return.
	 *
	 * @return mixed The Term list of Followers, the format depends on $output.
	 */
	public static function get_faulty_followers( $number = 20 ) {
		$args = array(
			'post_type'      => self::POST_TYPE,
			'posts_per_page' => $number,
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
			'meta_query'     => array(
				'relation' => 'OR',
				array(
					'key'     => 'activitypub_errors',
					'compare' => 'EXISTS',
				),
				array(
					'key'     => 'activitypub_inbox',
					'compare' => 'NOT EXISTS',
				),
				array(
					'key'     => 'activitypub_actor_json',
					'compare' => 'NOT EXISTS',
				),
				array(
					'key'     => 'activitypub_inbox',
					'value'   => '',
					'compare' => '=',
				),
				array(
					'key'     => 'activitypub_actor_json',
					'value'   => '',
					'compare' => '=',
				),
			),
		);

		$posts = new WP_Query( $args );
		$items = array();

		foreach ( $posts->get_posts() as $follower ) {
			$items[] = Follower::init_from_cpt( $follower ); // phpcs:ignore
		}

		return $items;
	}

	/**
	 * This function is used to store errors that occur when
	 * sending an ActivityPub message to a Follower.
	 *
	 * The error will be stored in the
	 * post meta.
	 *
	 * @param int   $post_id The ID of the WordPress Custom-Post-Type.
	 * @param mixed $error   The error message. Can be a string or a WP_Error.
	 *
	 * @return int|false The meta ID on success, false on failure.
	 */
	public static function add_error( $post_id, $error ) {
		if ( is_string( $error ) ) {
			$error_message = $error;
		} elseif ( is_wp_error( $error ) ) {
			$error_message = $error->get_error_message();
		} else {
			$error_message = __(
				'Unknown Error or misconfigured Error-Message',
				'activitypub'
			);
		}

		return add_post_meta(
			$post_id,
			'activitypub_errors',
			$error_message
		);
	}
}