<?php
/**
 * Class Google\Site_Kit\Core\Email_Reporting\Email_Template_Formatter
 *
 * @package   Google\Site_Kit\Core\Email_Reporting
 * @copyright 2025 Google LLC
 * @license   https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
 * @link      https://sitekit.withgoogle.com
 */

namespace Google\Site_Kit\Core\Email_Reporting;

use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Golinks\Golinks;
use Google\Site_Kit\Core\Util\BC_Functions;
use Google\Site_Kit\Core\User\Email_Reporting_Settings;
use WP_Error;
use WP_Post;
use WP_User;

/**
 * Formats email report data for template rendering.
 *
 * @since 1.170.0
 * @access private
 * @ignore
 */
class Email_Template_Formatter {

	/**
	 * Plugin context instance.
	 *
	 * @since 1.170.0
	 *
	 * @var Context
	 */
	private $context;

	/**
	 * Email report section builder.
	 *
	 * @since 1.170.0
	 *
	 * @var Email_Report_Section_Builder
	 */
	private $section_builder;

	/**
	 * Golinks instance.
	 *
	 * @since 1.174.0
	 *
	 * @var Golinks
	 */
	private $golinks;

	/**
	 * Email notices resolver.
	 *
	 * @since 1.175.0
	 *
	 * @var Email_Notices
	 */
	private $email_notices;

	/**
	 * Constructor.
	 *
	 * @since 1.170.0
	 * @since 1.174.0 Added golinks dependency.
	 * @since 1.175.0 Added email notices dependency.
	 *
	 * @param Context                      $context         Plugin context.
	 * @param Email_Report_Section_Builder $section_builder Section builder instance.
	 * @param Golinks                      $golinks         Golinks instance.
	 * @param Email_Notices                $email_notices   Email notices resolver.
	 */
	public function __construct( Context $context, Email_Report_Section_Builder $section_builder, Golinks $golinks, Email_Notices $email_notices ) {
		$this->context         = $context;
		$this->section_builder = $section_builder;
		$this->golinks         = $golinks;
		$this->email_notices   = $email_notices;
	}

	/**
	 * Builds sections from raw payload grouped by module.
	 *
	 * @since 1.170.0
	 * @since 1.186.0 Removed $user parameter, locale switching is now handled by the Email_Log_Processor.
	 *
	 * @param array   $raw_payload Raw payload.
	 * @param WP_Post $email_log   Email log post.
	 * @return array|WP_Error Sections array or WP_Error.
	 */
	public function build_sections( $raw_payload, WP_Post $email_log ) {
		$sections = array();

		if ( ! is_array( $raw_payload ) ) {
			return $sections;
		}

		foreach ( $raw_payload as $module_slug => $module_payload ) {
			if ( is_object( $module_payload ) ) {
				$module_payload = (array) $module_payload;
			}

			try {
				$module_sections = $this->section_builder->build_sections(
					$module_slug,
					array( $module_payload ),
					$email_log
				);
			} catch ( \Throwable $exception ) {
				return new WP_Error( 'email_report_section_build_failed', $exception->getMessage() );
			}

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

			if ( ! empty( $module_sections ) ) {
				$sections = array_merge( $sections, $module_sections );
			}
		}

		return $sections;
	}

	/**
	 * Builds template payload for rendering.
	 *
	 * @since 1.170.0
	 *
	 * @param array   $sections   Sections.
	 * @param string  $frequency  Frequency slug.
	 * @param array   $date_range Date range.
	 * @param WP_User $user      User receiving the report.
	 * @return array|WP_Error Template payload or WP_Error.
	 */
	public function build_template_payload( $sections, $frequency, $date_range, WP_User $user ) {
		$sections_payload = $this->prepare_sections_payload( $sections, $date_range );

		if ( empty( $sections_payload ) ) {
			return new WP_Error(
				'email_report_no_data',
				__( 'No email report data available.', 'google-site-kit' )
			);
		}

		$sections_map = new Sections_Map( $this->context, $sections_payload, $this->golinks );
		if ( empty( $sections_map->get_sections() ) ) {
			return new WP_Error(
				'email_report_no_data',
				__( 'No email report data available.', 'google-site-kit' )
			);
		}

		return array(
			'sections_payload' => $sections_payload,
			'template_data'    => $this->prepare_template_data(
				$frequency,
				$date_range,
				$user
			),
		);
	}

	/**
	 * Prepares section payload for the template renderer.
	 *
	 * @since 1.170.0
	 *
	 * @param array $sections   Section instances.
	 * @param array $date_range Date range used for the report.
	 * @return array Section payload for the template.
	 */
	private function prepare_sections_payload( $sections, $date_range ) {
		$payload        = array();
		$change_context = $this->get_change_context_label( $date_range );

		foreach ( $sections as $section ) {
			$values           = $section->get_values();
			$labels           = $section->get_labels();
			$trends           = $section->get_trends();
			$event_names      = $section->get_event_names();
			$dimensions       = $section->get_dimensions();
			$dimension_values = $section->get_dimension_values();
			$change           = isset( $trends[0] ) ? $this->parse_change_value( $trends[0] ) : null;
			$changes          = is_array( $trends ) ? array_map( array( $this, 'parse_change_value' ), $trends ) : array();

			$first_dimension_value = '';
			if ( isset( $dimension_values[0] ) ) {
				$first_dimension_value = is_array( $dimension_values[0] )
					? ( $dimension_values[0]['label'] ?? '' )
					: $dimension_values[0];
			}

			$section_key = $section->get_section_key();
			$label       = isset( $labels[0] ) ? $labels[0] : $section->get_title();
			$event_name  = isset( $event_names[0] ) ? $event_names[0] : '';

			$payload[ $section_key ] = array(
				'value'            => isset( $values[0] ) ? $values[0] : '',
				'values'           => $values,
				'label'            => $label,
				'event_name'       => $event_name,
				'dimension'        => isset( $dimensions[0] ) ? $dimensions[0] : '',
				'dimension_value'  => $first_dimension_value,
				'dimension_values' => $dimension_values ?? array(),
				'change'           => $change,
				'changes'          => $changes,
				'change_context'   => $change_context,
			);
		}

		return $payload;
	}

	/**
	 * Parses a change value into float.
	 *
	 * Handles both the canonical period-decimal percentage strings the
	 * section builder emits (e.g. `6.52%`) and locale-formatted ones that
	 * may still arrive from previously stored section data under a
	 * decimal-comma locale (e.g. `6,52%` for `es_CO`/`de_DE`). The decimal
	 * separator is identified structurally — the last `.` or `,` followed
	 * by exactly two digits, since trend percentages are always formatted
	 * with two decimal places — rather than assumed from the active
	 * locale, which would misread an already-canonical value under a
	 * decimal-comma locale.
	 *
	 * @since 1.170.0
	 * @since 1.186.0 Hardened decimal-separator detection to support decimal-comma locales.
	 *
	 * @param mixed $change Change value.
	 * @return float|null Parsed change, or null when missing or unparseable.
	 */
	private function parse_change_value( $change ) {
		if ( null === $change || '' === $change ) {
			return null;
		}

		if ( is_string( $change ) ) {
			$change = trim( str_replace( '%', '', $change ) );

			if ( preg_match( '/^([+-]?[\d.,]*)[.,](\d{2})$/', $change, $matches ) ) {
				$integer_part = str_replace( array( '.', ',' ), '', $matches[1] );
				$change       = $integer_part . '.' . $matches[2];
			}
		}

		if ( ! is_numeric( $change ) ) {
			return null;
		}

		return floatval( $change );
	}

	/**
	 * Builds a change context label based on the date range.
	 *
	 * @since 1.170.0
	 *
	 * @param array $date_range Date range.
	 * @return string Change context label.
	 */
	private function get_change_context_label( array $date_range ) {
		// Prefer the compare period length since the change badge references the previous window.
		if ( ! empty( $date_range['compareStartDate'] ) && ! empty( $date_range['compareEndDate'] ) ) {
			$days = $this->calculate_period_length_from_range(
				array(
					'startDate' => $date_range['compareStartDate'],
					'endDate'   => $date_range['compareEndDate'],
				)
			);
		} else {
			$days = $this->calculate_period_length_from_range( $date_range );
		}

		if ( null === $days || $days <= 0 ) {
			return __( 'Compared to previous period', 'google-site-kit' );
		}

		return sprintf(
			/* translators: %s: Number of days. */
			__( 'Compared to previous %s days', 'google-site-kit' ),
			number_format_i18n( $days )
		);
	}

	/**
	 * Calculates inclusive day length from a date range.
	 *
	 * @since 1.170.0
	 *
	 * @param array $date_range Date range with startDate/endDate.
	 * @return int|null Number of days or null on failure.
	 */
	private function calculate_period_length_from_range( $date_range ) {
		if ( empty( $date_range['startDate'] ) || empty( $date_range['endDate'] ) ) {
			return null;
		}

		try {
			$start = new \DateTime( $date_range['startDate'] );
			$end   = new \DateTime( $date_range['endDate'] );
		} catch ( \Exception $e ) {
			return null;
		}

		$diff = $start->diff( $end );
		if ( false === $diff ) {
			return null;
		}

		return $diff->days + 1;
	}

	/**
	 * Builds template data for simple email rendering.
	 *
	 * Simple emails share the same structure (subject, preheader, site, CTA, footer)
	 * but differ in their content. The $email_data array provides the variable content.
	 *
	 * @since 1.173.0
	 *
	 * @param string $subject   Email subject line.
	 * @param string $preheader Email preheader text.
	 * @param array  $email_data {
	 *     Additional email-specific data.
	 *
	 *     @type string $learn_more_url   URL for the learn more link.
	 *     @type string $cta_label        Label for the primary call-to-action button.
	 *     @type string $cta_url          URL for the primary call-to-action button.
	 *     @type string $footer_copy      Footer copy text.
	 *     @type array  $body_format_args Format arguments for plain text body (e.g., frequency, first_report_date).
	 *     @type array  $custom_data      Any additional custom data for the template.
	 * }
	 * @return array Template data for simple email.
	 */
	public function prepare_simple_email_data( $subject, $preheader, $email_data = array() ) {
		$site_domain        = $this->get_site_domain();
		$dashboard_url      = $this->golinks->get_url( 'dashboard' );
		$email_settings_url = $this->golinks->get_url( 'manage-subscription-email-reporting' );

		$data = array(
			'subject'                => $subject,
			'preheader'              => $preheader,
			'site'                   => array(
				'domain' => $site_domain,
				'url'    => $this->context->get_reference_site_url(),
			),
			'learn_more_url'         => $email_data['learn_more_url'] ?? '',
			'primary_call_to_action' => array(
				'label' => $email_data['cta_label'] ?? __( 'Get your report', 'google-site-kit' ),
				'url'   => $email_data['cta_url'] ?? $dashboard_url,
			),
			'footer'                 => array(
				'copy'            => $email_data['footer_copy'] ?? '',
				'unsubscribe_url' => $email_settings_url,
			),
		);

		return $data;
	}

	/**
	 * Builds template data for the subscription confirmation email.
	 *
	 * @since 1.174.0
	 * @since 1.186.0 Wrapped the unsubscribe link within the localized footer copy.
	 *
	 * @param string $frequency Frequency slug.
	 * @return array Template data.
	 */
	public function prepare_subscription_confirmation_template_data( $frequency ) {
		$site_domain        = $this->get_site_domain();
		$frequency_label    = $this->get_frequency_label( $frequency );
		$first_report_date  = $this->get_first_report_date_label( $frequency );
		$dashboard_url      = $this->golinks->get_url( 'dashboard' );
		$email_settings_url = $this->golinks->get_url( 'manage-subscription-email-reporting' );

		return array(
			'subject'                => sprintf(
				/* translators: %s: Site domain. */
				__( 'Success! You’re subscribed to Site Kit reports for %s', 'google-site-kit' ),
				$site_domain
			),
			'preheader'              => __( 'Your subscription is confirmed and your first report is on the way.', 'google-site-kit' ),
			'site'                   => array(
				'domain' => $site_domain,
				'url'    => $this->context->get_reference_site_url(),
			),
			'title'                  => Content_Map::get_title( 'subscription-confirmation' ),
			'body'                   => Content_Map::get_body_with_args(
				'subscription-confirmation',
				array(
					$frequency_label,
					$first_report_date,
				)
			),
			'learn_more_url'         => 'https://sitekit.withgoogle.com/support/?doc=email-reporting',
			'primary_call_to_action' => array(
				'label' => __( 'View dashboard', 'google-site-kit' ),
				'url'   => $dashboard_url,
			),
			'footer'                 => array(
				'copy'            => sprintf(
					/* translators: 1: Unsubscribe link URL, 2: Unsubscribe link inline style CSS. */
					__( 'You received this email because you signed up to receive email reports from Site Kit. If you do not want to receive these emails in the future you can <a class="link" href="%1$s" style="%2$s">unsubscribe</a>.', 'google-site-kit' ),
					$email_settings_url,
					'text-decoration:none;'
				),
				'unsubscribe_url' => $email_settings_url,
			),
			'graphic'                => Content_Map::get_graphic_config( 'subscription-confirmation' ),
			'footer_type'            => 'standard',
		);
	}

	/**
	 * Builds template data for rendering.
	 *
	 * @since 1.170.0
	 * @since 1.186.0 Wrapped the unsubscribe link within the localized footer copy.
	 *
	 * @param string  $frequency  Frequency slug.
	 * @param array   $date_range Date range.
	 * @param WP_User $user       User receiving the report.
	 * @return array Template data.
	 */
	private function prepare_template_data( $frequency, $date_range, WP_User $user ) {
		$dashboard_url      = $this->golinks->get_url( 'dashboard' );
		$email_settings_url = $this->golinks->get_url( 'manage-subscription-email-reporting' );

		return array(
			'subject'                => $this->build_subject( $frequency ),
			'preheader'              => __( 'See the latest highlights from Site Kit.', 'google-site-kit' ),
			'site'                   => array(
				'domain' => $this->get_site_domain(),
				'url'    => $this->context->get_reference_site_url(),
			),
			'date_range'             => array(
				'label'   => $this->build_date_label( $date_range ),
				'context' => $this->get_change_context_label( $date_range ),
			),
			'header_notices'         => $this->email_notices->get_header_notices( $user ),
			'primary_call_to_action' => array(
				'label' => __( 'View dashboard', 'google-site-kit' ),
				'url'   => $dashboard_url,
			),
			'footer'                 => array(
				'copy'            => sprintf(
					/* translators: 1: Unsubscribe link URL, 2: Unsubscribe link inline style CSS. */
					__( 'You received this email because you signed up to receive email reports from Site Kit. If you do not want to receive these emails in the future you can <a class="link" href="%1$s" style="%2$s">unsubscribe</a>.', 'google-site-kit' ),
					$email_settings_url,
					'text-decoration:none;'
				),
				'unsubscribe_url' => $email_settings_url,
			),
		);
	}

	/**
	 * Builds a human readable date label.
	 *
	 * @since 1.170.0
	 *
	 * @param array $date_range Date range.
	 * @return string Date label.
	 */
	private function build_date_label( array $date_range ) {
		if ( empty( $date_range['startDate'] ) || empty( $date_range['endDate'] ) ) {
			return '';
		}

		$format_date = static function ( $value ) {
			$timezone = BC_Functions::wp_timezone();
			$date     = date_create_immutable( $value, $timezone );
			if ( ! $date ) {
				return $value;
			}

			if ( function_exists( 'wp_date' ) ) {
				return wp_date( 'M j', $date->getTimestamp(), $timezone );
			}

			return $date->format( 'M j' );
		};

		return sprintf(
			'%s – %s',
			$format_date( $date_range['startDate'] ),
			$format_date( $date_range['endDate'] )
		);
	}

	/**
	 * Builds an email subject for the report.
	 *
	 * @since 1.170.0
	 *
	 * @param string $frequency Frequency slug.
	 * @return string Email subject.
	 */
	private function build_subject( $frequency ) {
		$frequency_label = $this->get_frequency_label( $frequency );
		$site_domain     = $this->get_site_domain();

		return sprintf(
			/* translators: 1: Report frequency, 2: Site domain. */
			__( 'Your %1$s Site Kit report for %2$s', 'google-site-kit' ),
			$frequency_label,
			$site_domain
		);
	}

	/**
	 * Gets a friendly frequency label.
	 *
	 * @since 1.170.0
	 *
	 * @param string $frequency Frequency slug.
	 * @return string Frequency label.
	 */
	public function get_frequency_label( $frequency ) {
		switch ( $frequency ) {
			case Email_Reporting_Settings::FREQUENCY_MONTHLY:
				return __( 'monthly', 'google-site-kit' );
			case Email_Reporting_Settings::FREQUENCY_QUARTERLY:
				return __( 'quarterly', 'google-site-kit' );
			case Email_Reporting_Settings::FREQUENCY_WEEKLY:
			default:
				return __( 'weekly', 'google-site-kit' );
		}
	}

	/**
	 * Gets a human-readable label for when the first report will be sent.
	 *
	 * Returns a localized string describing when the user can expect their
	 * first report based on their selected frequency.
	 *
	 * @since 1.173.0
	 *
	 * @param string $frequency Frequency slug (weekly, monthly, quarterly).
	 * @return string First report date label.
	 */
	public function get_first_report_date_label( $frequency ) {
		switch ( $frequency ) {
			case Email_Reporting_Settings::FREQUENCY_MONTHLY:
				return __( '1st of the following month', 'google-site-kit' );

			case Email_Reporting_Settings::FREQUENCY_QUARTERLY:
				return $this->get_next_quarter_label();

			case Email_Reporting_Settings::FREQUENCY_WEEKLY:
			default:
				return $this->get_next_week_start_label();
		}
	}

	/**
	 * Gets the label for the next week start day.
	 *
	 * Uses WordPress start_of_week setting to determine which day
	 * of the week reports are sent.
	 *
	 * @since 1.173.0
	 *
	 * @return string Label like "next Monday".
	 */
	private function get_next_week_start_label() {
		$start_of_week = (int) get_option( 'start_of_week', 0 );

		global $wp_locale;

		$day_name = $wp_locale->get_weekday( $start_of_week );

		return sprintf(
			/* translators: %s: Day of the week (e.g., "Monday"). */
			__( 'next %s', 'google-site-kit' ),
			$day_name
		);
	}

	/**
	 * Gets the label for the next quarterly report date.
	 *
	 * Quarters are: Q1 (Jan-Mar), Q2 (Apr-Jun), Q3 (Jul-Sep), Q4 (Oct-Dec).
	 * Returns "1st of {month}" for the first month of the next quarter.
	 *
	 * @since 1.173.0
	 *
	 * @return string Label like "1st of April".
	 */
	private function get_next_quarter_label() {
		$current_month = (int) gmdate( 'n' );

		// Determine the first month of the next quarter.
		if ( $current_month <= 3 ) {
			// Current: Q1 (Jan-Mar), Next: Q2 starts April.
			$next_quarter_month = 4;
		} elseif ( $current_month <= 6 ) {
			// Current: Q2 (Apr-Jun), Next: Q3 starts July.
			$next_quarter_month = 7;
		} elseif ( $current_month <= 9 ) {
			// Current: Q3 (Jul-Sep), Next: Q4 starts October.
			$next_quarter_month = 10;
		} else {
			// Current: Q4 (Oct-Dec), Next: Q1 starts January.
			$next_quarter_month = 1;
		}

		// Get localized month name.
		$timestamp  = mktime( 0, 0, 0, $next_quarter_month, 1 );
		$month_name = wp_date( 'F', $timestamp );

		return sprintf(
			/* translators: %s: Month name (e.g., "April"). */
			__( '1st of %s', 'google-site-kit' ),
			$month_name
		);
	}

	/**
	 * Gets the site domain including subdirectory context.
	 *
	 * @since 1.170.0
	 *
	 * @return string Site domain string.
	 */
	private function get_site_domain() {
		$site_url = $this->context->get_reference_site_url();
		$parsed   = wp_parse_url( $site_url );

		if ( empty( $parsed['host'] ) ) {
			return $site_url;
		}

		$domain = $parsed['host'];

		if ( ! empty( $parsed['path'] ) && '/' !== $parsed['path'] ) {
			$domain .= untrailingslashit( $parsed['path'] );
		}

		return $domain;
	}
}
