<?php
/**
 * Measures the CSS/JS an addon actually loads on a real front-end page.
 *
 * A naive "sum every CSS/JS file the plugin ships on disk" wildly overcounts
 * (an addon may ship megabytes of per-widget, minified+unminified, RTL and admin
 * assets that never load together). Instead we fetch a real page and record only
 * the assets the page truly enqueues, mapped back to the plugin that owns them.
 * For an addon whose widgets are used nowhere, whatever it still loads is waste.
 *
 * This is an estimate from one sampled page; caching/optimization plugins that
 * combine or defer assets can change what actually ships, which the UI discloses.
 */

defined( 'ABSPATH' ) || exit;

class TFWA_Footprint {

	/**
	 * Human-readable size. Pure.
	 */
	public static function format_bytes( int $bytes ): string {
		if ( $bytes <= 0 ) {
			return '0 KB';
		}
		if ( $bytes >= 1048576 ) {
			return number_format( $bytes / 1048576, 1 ) . ' MB';
		}
		return max( 1, (int) round( $bytes / 1024 ) ) . ' KB';
	}

	/**
	 * Extract plugin-owned CSS/JS asset URLs from page HTML, grouped by plugin
	 * slug. Pure — the plugins directory URL path is passed in by the caller
	 * (derived from plugins_url()) rather than hard-coded, so installs with a
	 * custom content directory (e.g. Bedrock's /app) are matched correctly.
	 *
	 * @param string $html              Page HTML to scan.
	 * @param string $plugins_url_path  URL path of the plugins directory, e.g. "/wp-content/plugins".
	 * @return array<string, string[]> slug => list of distinct asset URLs.
	 */
	public static function parse_plugin_assets( string $html, string $plugins_url_path ): array {
		$out = array();

		$plugins_url_path = rtrim( $plugins_url_path, '/' );
		if ( '' === $plugins_url_path ) {
			return $out;
		}

		if ( preg_match_all(
			'#(?:href|src)=["\']([^"\']*' . preg_quote( $plugins_url_path, '#' ) . '/([^/]+)/[^"\']+?\.(?:css|js))(?:\?[^"\']*)?["\']#i',
			$html,
			$matches,
			PREG_SET_ORDER
		) ) {
			foreach ( $matches as $hit ) {
				$out[ $hit[2] ][ strtok( $hit[1], '?' ) ] = true;
			}
		}

		foreach ( $out as $slug => $set ) {
			$out[ $slug ] = array_keys( $set );
		}

		return $out;
	}

	/**
	 * Map an asset URL to its local file path via the content directory. Pure —
	 * the content directory's URL path is passed in by the caller (derived from
	 * content_url()) rather than hard-coded to "/wp-content/", so installs with a
	 * custom content directory (e.g. Bedrock's /app) resolve to the right file.
	 *
	 * @param string $url              Asset URL.
	 * @param string $content_dir      Local filesystem path of the content directory (e.g. WP_CONTENT_DIR).
	 * @param string $content_url_path URL path of the content directory, e.g. "/wp-content".
	 */
	public static function url_to_path( string $url, string $content_dir, string $content_url_path ): string {
		$url               = strtok( $url, '?' );
		$content_url_path  = rtrim( $content_url_path, '/' );

		if ( '' === $content_url_path ) {
			return '';
		}

		$anchor = $content_url_path . '/';
		$pos    = strpos( $url, $anchor );

		if ( false === $pos ) {
			return '';
		}

		return rtrim( $content_dir, '/' ) . substr( $url, $pos + strlen( $content_url_path ) );
	}

	/**
	 * Pages to sample. The home page is representative for an addon that loads
	 * globally; a couple of recent singular URLs catch front-page-only themes.
	 *
	 * @return string[]
	 */
	public static function sample_urls(): array {
		$urls = array( home_url( '/' ) );

		$recent = get_posts(
			array(
				'numberposts' => 1,
				'post_status' => 'publish',
				'post_type'   => array( 'page', 'post' ),
				'fields'      => 'ids',
			)
		);
		foreach ( $recent as $id ) {
			$link = get_permalink( $id );
			if ( $link ) {
				$urls[] = $link;
			}
		}

		return array_values( array_unique( $urls ) );
	}

	/**
	 * Fetch sample pages and record, per plugin slug, the distinct CSS/JS the
	 * pages actually load and their total size on disk.
	 *
	 * @param string[] $urls
	 * @return array<string, array{files:int, bytes:int}>
	 */
	public static function measure( array $urls ): array {
		$seen = array(); // slug => [ path => bytes ]

		// Derive the URL paths from WP's own APIs rather than hard-coding
		// "/wp-content" so installs with a custom content directory (e.g.
		// Bedrock's /app, or a moved WP_CONTENT_DIR/URL) still match correctly.
		$plugins_url_path = (string) wp_parse_url( plugins_url(), PHP_URL_PATH );
		$content_url_path = (string) wp_parse_url( content_url(), PHP_URL_PATH );

		foreach ( $urls as $url ) {
			// Short timeout: this runs synchronously on the final scan batch, so a
			// slow loopback must not stall (let alone time out) the whole scan. A
			// page that doesn't answer in a few seconds is simply skipped.
			$response = wp_remote_get( $url, array( 'timeout' => 4, 'redirection' => 2 ) );

			if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
				continue;
			}

			foreach ( self::parse_plugin_assets( wp_remote_retrieve_body( $response ), $plugins_url_path ) as $slug => $asset_urls ) {
				foreach ( $asset_urls as $asset_url ) {
					$path = self::url_to_path( $asset_url, WP_CONTENT_DIR, $content_url_path );
					if ( '' !== $path && is_file( $path ) ) {
						$seen[ $slug ][ $path ] = (int) filesize( $path );
					}
				}
			}
		}

		$out = array();
		foreach ( $seen as $slug => $paths ) {
			$out[ $slug ] = array(
				'files' => count( $paths ),
				'bytes' => array_sum( $paths ),
			);
		}

		return $out;
	}
}
