<?php
/**
 * Report assembly: one payload combining both engines, consumed by the
 * dashboard, Site Health, WP-CLI, and the REST endpoint.
 *
 * @package Upgrade_Pilot
 */

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

/**
 * Class UPilot_Report
 */
class UPilot_Report {

	/**
	 * Verdict engine.
	 *
	 * @var UPilot_Verdict
	 */
	private $verdict;

	/**
	 * Snapshot engine.
	 *
	 * @var UPilot_Snapshot
	 */
	private $snapshot;

	/**
	 * Constructor.
	 *
	 * @param UPilot_Verdict  $verdict  Verdict engine.
	 * @param UPilot_Snapshot $snapshot Snapshot engine.
	 */
	public function __construct( UPilot_Verdict $verdict, UPilot_Snapshot $snapshot ) {
		$this->verdict  = $verdict;
		$this->snapshot = $snapshot;
	}

	/**
	 * The machine-readable export shape.
	 *
	 * One canonical schema, used by `wp upgrade-pilot scan --format=json`, by
	 * `wp upgrade-pilot report --format=json`, and by GET /upgrade-pilot/v1/report.
	 * All three are free and ungated. Keeping the shape in one place is what stops
	 * the CLI and the REST endpoint drifting into two different payloads.
	 *
	 * Pure: it reshapes a report that has already been assembled, and reads nothing
	 * else. `schema` is versioned so a consumer can pin to it.
	 *
	 * @param array $report Assembled report payload.
	 * @return array
	 */
	public static function shape( $report ) {
		return array(
			'schema'       => 'upgrade-pilot/report@1',
			'site'         => home_url(),
			'generated_at' => isset( $report['generated_at'] ) ? gmdate( 'c', (int) $report['generated_at'] ) : null,
			'target_php'   => isset( $report['target_php'] ) ? $report['target_php'] : null,
			'verdict'      => isset( $report['verdict'] ) ? $report['verdict'] : null,
			'headline'     => isset( $report['headline'] ) ? $report['headline'] : null,
			'server'       => isset( $report['server_items'] ) ? $report['server_items'] : array(),
			'plugins'      => isset( $report['metadata']['items'] ) ? $report['metadata']['items'] : array(),
			'scan_counts'  => isset( $report['scan_counts'] ) ? $report['scan_counts'] : array(),
			'trust'        => array(
				'unacked_critical' => isset( $report['trust']['unacked_critical'] ) ? $report['trust']['unacked_critical'] : 0,
				'unacked_warning'  => isset( $report['trust']['unacked_warning'] ) ? $report['trust']['unacked_warning'] : 0,
			),
		);
	}

	/**
	 * Full combined report.
	 *
	 * @param bool $refresh Force fresh assembly.
	 * @return array
	 */
	public function get( $refresh = false ) {
		$report = $this->verdict->assemble( $refresh );

		$events = $this->snapshot->get_events( 20 );

		// Count ALL unacknowledged events, not just the display window —
		// an old unacknowledged critical must never disappear from the verdict.
		//
		// The table name MUST come from UPilot_Store, not from $wpdb->prefix. On a
		// network $wpdb->prefix is the CURRENT SUBSITE's prefix (wp_3_), while the
		// events table is network-wide and lives on the base prefix (wp_). Reading
		// the wrong one means every subsite silently reports zero critical events —
		// a plugin whose job is to warn you would quietly stop warning you.
		global $wpdb;
		$events_table = UPilot_Store::table( 'events' );
		$counts       = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- own table; single cheap COUNT.
			"SELECT severity, COUNT(*) AS n FROM {$events_table} WHERE acknowledged = 0 GROUP BY severity", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- own table name.
			OBJECT_K
		);
		$critical = isset( $counts['critical'] ) ? (int) $counts['critical']->n : 0;
		$warning  = isset( $counts['warning'] ) ? (int) $counts['warning']->n : 0;

		$report['trust'] = array(
			'recent_events'    => $events,
			'unacked_critical' => $critical,
			'unacked_warning'  => $warning,
			'last_run'         => (int) UPilot_Store::get_option( 'upilot_last_snapshot_run', 0 ),
		);

		return $report;
	}
}
