<?php
/**
 * What was said about a claim.
 *
 * @package WarrantyCart
 */

namespace WarrantyCart\Core;

use WP_Error;

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

/**
 * Messages on a claim — to the customer, or between staff.
 *
 * A claim could be answered exactly once: the `resolution` note written when its
 * status moved. Anything else the shop needed to say — "can you photograph the
 * serial plate", "the part is three weeks out" — had to leave the plugin and go
 * by email, where it is then invisible to whoever picks the claim up next.
 *
 * One-way by design, for now. The customer reads what the shop writes and
 * replies by filing nothing; a reply channel needs a way to authenticate a guest
 * on the way back in, which is a larger piece of work than this closes.
 */
class ClaimMessages {

	/**
	 * A message the customer reads.
	 */
	public const TO_CUSTOMER = 'customer';

	/**
	 * A message only the shop reads.
	 */
	public const INTERNAL = 'internal';

	/**
	 * The table.
	 *
	 * @return string
	 */
	public static function table(): string {
		global $wpdb;

		return $wpdb->prefix . 'warranty_cart_claim_messages';
	}

	/**
	 * Who a message can be addressed to.
	 *
	 * @return array<string, string> Key to label.
	 */
	public static function visibilities(): array {
		return array(
			self::TO_CUSTOMER => __( 'Visible to the customer', 'warranty-cart' ),
			self::INTERNAL    => __( 'Internal note', 'warranty-cart' ),
		);
	}

	/**
	 * Write a message on a claim.
	 *
	 * @param int    $claim_id   The claim.
	 * @param string $body       What is being said.
	 * @param string $visibility One of self::visibilities().
	 * @param int    $author_id  Who wrote it. Defaults to the current user.
	 *
	 * @return int|WP_Error The new message ID, or why it was refused.
	 */
	public static function add( int $claim_id, string $body, string $visibility = self::TO_CUSTOMER, int $author_id = 0 ) {
		global $wpdb;

		$body = trim( $body );

		if ( '' === $body ) {
			return new WP_Error(
				'warranty_cart_message_empty',
				__( 'Write something before sending it.', 'warranty-cart' ),
				array( 'status' => 400 )
			);
		}

		if ( ! array_key_exists( $visibility, self::visibilities() ) ) {
			return new WP_Error(
				'warranty_cart_message_visibility',
				__( 'That is not a valid audience for a message.', 'warranty-cart' ),
				array( 'status' => 400 )
			);
		}

		$claim = Claims::get( $claim_id );

		if ( ! $claim ) {
			return new WP_Error(
				'warranty_cart_no_claim',
				__( 'That claim could not be found.', 'warranty-cart' ),
				array( 'status' => 404 )
			);
		}

		$author_id = $author_id > 0 ? $author_id : get_current_user_id();
		$author    = $author_id > 0 ? get_userdata( $author_id ) : false;

		$inserted = $wpdb->insert(
			self::table(),
			array(
				'claim_id'    => $claim_id,
				'author_id'   => $author_id > 0 ? $author_id : null,

				/*
				 * The name as it is today, kept as it was.
				 *
				 * Staff leave and accounts are deleted; a thread that has
				 * forgotten who answered it is evidence of nothing.
				 */
				'author_name' => $author ? $author->display_name : __( 'The shop', 'warranty-cart' ),
				'visibility'  => $visibility,
				'body'        => $body,
				'created_at'  => current_time( 'mysql' ),
			),
			array( '%d', '%d', '%s', '%s', '%s', '%s' )
		);

		if ( ! $inserted ) {
			return new WP_Error(
				'warranty_cart_message_failed',
				__( 'The message could not be saved. Please try again.', 'warranty-cart' ),
				array( 'status' => 500 )
			);
		}

		$message_id = (int) $wpdb->insert_id;

		/**
		 * Fires once a message has been written on a claim.
		 *
		 * The customer is emailed from here rather than inline, so a shop that
		 * wants to batch or suppress the mail has somewhere to do it.
		 *
		 * @param int    $message_id The message.
		 * @param object $claim      The claim it is on.
		 * @param string $visibility Who it is for.
		 */
		do_action( 'warranty_cart_claim_message_added', $message_id, $claim, $visibility );

		return $message_id;
	}

	/**
	 * The messages on a claim, oldest first.
	 *
	 * @param int  $claim_id         The claim.
	 * @param bool $include_internal Whether to include notes the customer must
	 *                               not see. Never pass true for anything a
	 *                               customer can reach.
	 *
	 * @return array<int, object>
	 */
	public static function for_claim( int $claim_id, bool $include_internal = false ): array {
		global $wpdb;

		$table = self::table();

		if ( $include_internal ) {
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery
			$rows = $wpdb->get_results(
				$wpdb->prepare( "SELECT * FROM {$table} WHERE claim_id = %d ORDER BY id ASC", $claim_id )
			);
			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery
		} else {
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery
			$rows = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT * FROM {$table} WHERE claim_id = %d AND visibility = %s ORDER BY id ASC",
					$claim_id,
					self::TO_CUSTOMER
				)
			);
			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery
		}

		return is_array( $rows ) ? $rows : array();
	}

	/**
	 * The customer-visible messages on several claims at once.
	 *
	 * The account page lists every claim a customer has filed, and asking per
	 * claim would be one query per row.
	 *
	 * @param array<int, int> $claim_ids The claims.
	 *
	 * @return array<int, array<int, object>> Claim ID to its messages.
	 */
	public static function for_claims( array $claim_ids ): array {
		global $wpdb;

		$claim_ids = array_values( array_filter( array_map( 'absint', $claim_ids ) ) );

		if ( empty( $claim_ids ) ) {
			return array();
		}

		$table        = self::table();
		$placeholders = implode( ',', array_fill( 0, count( $claim_ids ), '%d' ) );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$table} WHERE claim_id IN ({$placeholders}) AND visibility = %s ORDER BY id ASC",
				array_merge( $claim_ids, array( self::TO_CUSTOMER ) )
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery

		$grouped = array();

		foreach ( is_array( $rows ) ? $rows : array() as $row ) {
			$grouped[ (int) $row->claim_id ][] = $row;
		}

		return $grouped;
	}

	/**
	 * Remove every message on a claim.
	 *
	 * @param int $claim_id The claim.
	 *
	 * @return int How many were removed.
	 */
	public static function delete_for_claim( int $claim_id ): int {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		return (int) $wpdb->delete( self::table(), array( 'claim_id' => $claim_id ), array( '%d' ) );
	}
}
