<?php
/**
 * Logger — persists workflow run records to the logs table.
 *
 * @package CartCue
 */

namespace CartCue;

defined( 'ABSPATH' ) || exit;

class Logger {

	/**
	 * Write a log entry.
	 *
	 * @param int    $workflow_id
	 * @param string $workflow_name
	 * @param string $trigger_slug
	 * @param array  $data           Context data.
	 * @param string $status         'success' | 'skipped' | 'error'.
	 * @param string $message
	 */
	public function log(
		int $workflow_id,
		string $workflow_name,
		string $trigger_slug,
		array $data,
		string $status,
		string $message
	): void {
		global $wpdb;

		$data = apply_filters( 'cartcue_log_payload', $data, $workflow_id, $trigger_slug );
		unset( $data['cart_contents'] );
		if ( ! empty( $data['customer_email'] ) && ! apply_filters( 'cartcue_log_full_pii', false, $workflow_id ) ) {
			$email = (string) $data['customer_email'];
			$at    = strpos( $email, '@' );
			$data['customer_email'] = substr( $email, 0, 3 ) . '***@' . ( false !== $at ? substr( $email, $at + 1 ) : '' );
		}

		$customer_email = sanitize_email( $data['customer_email'] ?? '' );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$wpdb->insert(
			DB::logs(),
			[
				'workflow_id'    => $workflow_id,
				'workflow_name'  => sanitize_text_field( $workflow_name ),
				'trigger_slug'   => sanitize_key( $trigger_slug ),
				'customer_email' => $customer_email,
				'trigger_data'   => wp_json_encode( $data ),
				'status'         => sanitize_key( $status ),
				'message'        => wp_kses( $message, [] ),
				'created_at'     => current_time( 'mysql' ),
			],
			[ '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ]
		);
	}

	/**
	 * Retrieve log entries, optionally filtered.
	 *
	 * @param array $args  {
	 *     @type int    $workflow_id  Filter by workflow.
	 *     @type string $status       Filter by status.
	 *     @type int    $limit        Rows per page. Default 50.
	 *     @type int    $offset       Pagination offset.
	 * }
	 * @return array
	 */
	/**
	 * @param array<string, mixed> $args Filter args.
	 * @return array{0: string, 1: array<int, mixed>}
	 */
	private function build_logs_where( array $args ): array {
		$where  = [];
		$params = [];

		if ( ! empty( $args['workflow_id'] ) ) {
			$where[]  = 'workflow_id = %d';
			$params[] = (int) $args['workflow_id'];
		}

		if ( ! empty( $args['status'] ) ) {
			$where[]  = 'status = %s';
			$params[] = sanitize_key( (string) $args['status'] );
		}

		if ( ! empty( $args['date_from'] ) ) {
			$where[]  = 'created_at >= %s';
			$params[] = sanitize_text_field( (string) $args['date_from'] ) . ' 00:00:00';
		}

		if ( ! empty( $args['date_to'] ) ) {
			$where[]  = 'created_at <= %s';
			$params[] = sanitize_text_field( (string) $args['date_to'] ) . ' 23:59:59';
		}

		$sql = $where ? 'WHERE ' . implode( ' AND ', $where ) : '';

		return [ $sql, $params ];
	}

	public function get_logs( array $args = [] ): array {
		global $wpdb;

		$defaults = [
			'workflow_id' => 0,
			'status'      => '',
			'limit'       => 50,
			'offset'      => 0,
		];

		$args   = wp_parse_args( $args, $defaults );
		$limit  = (int) $args['limit'];
		$offset = (int) $args['offset'];

		[ $where_sql, $params ] = $this->build_logs_where( $args );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
		return $wpdb->get_results(
			// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- merged prepare args via array_merge.
			$wpdb->prepare(
				'SELECT * FROM %i ' . $where_sql . ' ORDER BY created_at DESC LIMIT %d OFFSET %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- WHERE clauses from build_logs_where().
				array_merge( [ DB::logs() ], $params, [ $limit, $offset ] )
			),
			ARRAY_A
		) ?: [];
	}

	/**
	 * Count log entries matching given filters (for pagination).
	 *
	 * @param array $args
	 * @return int
	 */
	public function count_logs( array $args = [] ): int {
		global $wpdb;

		[ $where_sql, $params ] = $this->build_logs_where( $args );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter
		return (int) $wpdb->get_var(
			$wpdb->prepare(
				'SELECT COUNT(*) FROM %i ' . $where_sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- WHERE clauses from build_logs_where().
				array_merge( [ DB::logs() ], $params )
			)
		);
	}

	/**
	 * Delete log entries older than N days.
	 *
	 * @param int $days
	 */
	public function prune( int $days = 90 ): void {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$wpdb->query(
			$wpdb->prepare(
				'DELETE FROM %i WHERE created_at < DATE_SUB(NOW(), INTERVAL %d DAY)',
				DB::logs(),
				$days
			)
		);
	}

	public function count_recent_failures( int $minutes = 1440 ): int {
		global $wpdb;

		$minutes = max( 1, $minutes );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return (int) $wpdb->get_var(
			$wpdb->prepare(
				'SELECT COUNT(*) FROM %i WHERE status = %s AND created_at >= DATE_SUB(NOW(), INTERVAL %d MINUTE)',
				DB::logs(),
				'error',
				$minutes
			)
		);
	}

	/**
	 * Human-readable label from a skip log message.
	 */
	public static function format_skip_message( string $message ): string {
		if ( preg_match( '/^\[skip:[^\]]+\]\s*(.*)$/s', $message, $matches ) ) {
			return trim( $matches[1] );
		}

		return $message;
	}

	/**
	 * Recent skipped runs for a play (newest first).
	 *
	 * @return array<int, array<string, mixed>>
	 */
	public function get_recent_skips( int $workflow_id, int $limit = 5 ): array {
		if ( $workflow_id <= 0 ) {
			return [];
		}

		return $this->get_logs(
			[
				'workflow_id' => $workflow_id,
				'status'      => 'skipped',
				'limit'       => max( 1, $limit ),
				'offset'      => 0,
			]
		);
	}

	/**
	 * Aggregate skip reasons for a play (most recent occurrence first).
	 *
	 * @return array<int, array{reason: string, message: string, count: int, last_at: string}>
	 */
	public function get_skip_summary( int $workflow_id, int $limit = 5 ): array {
		global $wpdb;

		if ( $workflow_id <= 0 ) {
			return [];
		}

		$limit = max( 1, $limit );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				'SELECT message, COUNT(*) AS skip_count, MAX(created_at) AS last_at
				 FROM %i
				 WHERE workflow_id = %d AND status = %s
				 GROUP BY message
				 ORDER BY last_at DESC
				 LIMIT %d',
				DB::logs(),
				$workflow_id,
				'skipped',
				$limit
			),
			ARRAY_A
		) ?: [];

		$summary = [];
		foreach ( $rows as $row ) {
			$raw     = (string) ( $row['message'] ?? '' );
			$reason  = 'unknown';
			if ( preg_match( '/^\[skip:([^\]]+)\]/', $raw, $matches ) ) {
				$reason = sanitize_key( $matches[1] );
			}
			$summary[] = [
				'reason'  => $reason,
				'message' => self::format_skip_message( $raw ),
				'count'   => (int) ( $row['skip_count'] ?? 0 ),
				'last_at' => (string) ( $row['last_at'] ?? '' ),
			];
		}

		return $summary;
	}
}
