<?php
/**
 * Auditor: establishes, per form folder, what a stranger with the URL would
 * actually get — as a served fact, via the site's own loopback.
 *
 * Verdict vocabulary (honesty rules):
 *  - exposed              direct URL returned the file. Confirmed, server-visible.
 *  - protected            direct URL was refused (401/403) AND our rules are installed.
 *  - installed_unverified rules written, but the server still serves the file
 *                         (e.g. nginx/IIS ignore .htaccess) or verification
 *                         couldn't run — NEVER upgraded to "protected" silently.
 *  - impaired             the loopback itself is broken; no claim made either way.
 *  - no_uploads           nothing to audit.
 *
 * @package Upload_Vault
 */

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

/**
 * Class Uvgf_Auditor
 */
class Uvgf_Auditor {

	const SAMPLES_PER_FOLDER = 2;

	/**
	 * Run a full audit: index, probe, persist. Returns the audit array.
	 *
	 * @return array
	 */
	public static function audit() {
		$loopback_ok = Uvgf_Fetcher::self_check();
		$sync        = Uvgf_Indexer::sync();
		$folders     = Uvgf_Indexer::form_folders();
		$base_url    = Uvgf_Locator::base_url();
		$installed   = Uvgf_Protector::rules_installed();

		$previous = get_option( 'uvgf_audit', array() );
		$prev_map = isset( $previous['folders'] ) && is_array( $previous['folders'] ) ? $previous['folders'] : array();

		$results     = array();
		$exposed_n   = 0;
		$listing_n   = 0;
		$new_exposed = array();

		foreach ( $folders as $folder => $info ) {
			$row = array(
				'folder'   => $folder,
				'form_id'  => (int) $info['form_id'],
				'files'    => (int) $info['files'],
				'bytes'    => (int) $info['bytes'],
				'status'   => 'unknown',
				'http'     => 0,
				'listing'  => false,
				'sample'   => $info['sample_relpath'],
				'excluded' => Uvgf_Protector::is_excluded( $folder ),
			);

			if ( $loopback_ok ) {
				$probe         = Uvgf_Fetcher::probe_file( $base_url . '/' . self::url_encode_relpath( $info['sample_relpath'] ) );
				$row['http']   = (int) $probe['status'];
				$listing_res   = Uvgf_Fetcher::probe_listing( $base_url . '/' . rawurlencode( $folder ) );
				$row['listing'] = (bool) $listing_res['listing'];

				if ( 'exposed' === $probe['result'] ) {
					$row['status'] = 'exposed';
					$exposed_n++;
				} elseif ( 'blocked' === $probe['result'] ) {
					$row['status'] = 'protected';
				} elseif ( 'missing' === $probe['result'] ) {
					// The sampled file 404s over HTTP while existing on disk —
					// commonly an off-webroot base or CDN rewrite. No exposure claim.
					$row['status'] = 'unreachable';
				} else {
					$row['status'] = 'unknown';
				}

				if ( $row['listing'] ) {
					$listing_n++;
				}

				// Registry blanket status for the folder.
				if ( in_array( $row['status'], array( 'exposed', 'protected' ), true ) ) {
					Uvgf_Indexer::set_folder_status( $folder, $row['status'] );
				}

				// New exposure = exposed now, and previously absent or not exposed.
				$was = isset( $prev_map[ $folder ]['status'] ) ? $prev_map[ $folder ]['status'] : '';
				if ( 'exposed' === $row['status'] && 'exposed' !== $was && ! $row['excluded'] ) {
					$new_exposed[] = $folder;
				}
			}

			$results[ $folder ] = $row;
		}

		$verdict = self::verdict( $loopback_ok, $installed, $results );

		$audit = array(
			'checked_at'  => time(),
			'loopback_ok' => $loopback_ok,
			'installed'   => $installed,
			'server'      => Uvgf_Locator::server_type(),
			'gf_active'   => Uvgf_Locator::gf_active(),
			'verdict'     => $verdict,
			'exposed'     => $exposed_n,
			'listings'    => $listing_n,
			'new_exposed' => $new_exposed,
			'indexed'     => $sync['indexed'],
			'folders'     => $results,
		);

		update_option( 'uvgf_audit', $audit, false );

		// Re-read after self_check(): it wrote loopback_ok/loopback_error to the
		// same option, and we must not clobber those with a stale copy.
		$state               = get_option( 'uvgf_state', array() );
		$state['last_audit'] = time();
		$state['verdict']    = $verdict;
		$state['protected']  = $installed ? 1 : 0;
		update_option( 'uvgf_state', $state, false );

		/**
		 * Fires after an audit completes.
		 *
		 * @param array $audit The stored audit array.
		 */
		do_action( 'uvgf_audit_complete', $audit );

		return $audit;
	}

	/**
	 * Site verdict from folder results.
	 *
	 * @param bool  $loopback_ok Loopback health.
	 * @param bool  $installed   Are our deny rules installed?
	 * @param array $results     Folder rows.
	 * @return string
	 */
	private static function verdict( $loopback_ok, $installed, $results ) {
		if ( empty( $results ) ) {
			return 'no_uploads';
		}
		if ( ! $loopback_ok ) {
			return 'impaired';
		}

		$exposed   = 0;
		$protected = 0;
		foreach ( $results as $row ) {
			if ( ! empty( $row['excluded'] ) ) {
				continue;
			}
			if ( 'exposed' === $row['status'] ) {
				$exposed++;
			} elseif ( 'protected' === $row['status'] ) {
				$protected++;
			}
		}

		if ( $exposed > 0 ) {
			return $installed ? 'installed_unverified' : 'exposed';
		}
		if ( $protected > 0 ) {
			return 'protected';
		}
		return $installed ? 'installed_unverified' : 'unknown';
	}

	/**
	 * Encode each path segment for URL use, preserving slashes.
	 *
	 * @param string $relpath Relative path.
	 * @return string
	 */
	public static function url_encode_relpath( $relpath ) {
		return implode( '/', array_map( 'rawurlencode', explode( '/', $relpath ) ) );
	}

	/**
	 * Human labels for verdicts (shared by report page, Site Health, CLI).
	 *
	 * @return array
	 */
	public static function verdict_labels() {
		return array(
			'exposed'              => __( 'Exposed — direct upload URLs are served to anyone who has them', 'buildsbyluke-upload-vault-for-gravity-forms' ),
			'protected'            => __( 'Protected — direct requests are refused; files served via the Upload Vault proxy', 'buildsbyluke-upload-vault-for-gravity-forms' ),
			'installed_unverified' => __( 'Rules installed, NOT verified — this server is still serving direct URLs (see server instructions)', 'buildsbyluke-upload-vault-for-gravity-forms' ),
			'impaired'             => __( 'Audit impaired — this site cannot loopback-request itself, so no claim is made', 'buildsbyluke-upload-vault-for-gravity-forms' ),
			'no_uploads'           => __( 'No Gravity Forms uploads found', 'buildsbyluke-upload-vault-for-gravity-forms' ),
			'unknown'              => __( 'Undetermined — probes returned unexpected results', 'buildsbyluke-upload-vault-for-gravity-forms' ),
		);
	}
}
