<?php
/**
 * Scan coverage reporting.
 *
 * @package LocalGuardScanner
 */

namespace LocalGuardScanner\Application\Coverage;

/**
 * Converts low-level omission counters into safe per-scope aggregates.
 */
final class CoverageReporter {
	const VERSION = 2;
	const SCOPES  = array( 'files', 'database', 'plugins' );

	/** @var string[] */
	const DELIBERATE_REASONS = array( 'excluded_directory' );

	/**
	 * @return array{files:array<string,int>,database:array<string,int>,plugins:array<string,int>}
	 */
	public static function empty_scopes(): array {
		return array(
			'files'    => array(),
			'database' => array(),
			'plugins'  => array(),
		);
	}

	/**
	 * Normalize structured coverage for persistence.
	 *
	 * @param mixed $scopes Raw scope map.
	 * @return array{version:int,scopes:array{files:array<string,int>,database:array<string,int>,plugins:array<string,int>}}
	 */
	public function for_storage( $scopes ): array {
		return array(
			'version' => self::VERSION,
			'scopes'  => $this->normalize_scopes( $scopes ),
		);
	}

	/**
	 * @param mixed $coverage Raw decoded coverage data.
	 * @return array{version:int,structured:bool,complete:bool,verified:bool,omitted:int,reasons:array<string,int>,deliberate:array<string,int>,involuntary:array<string,int>,scopes:array<string,array{complete:bool,verified:bool,omitted:int,reasons:array<string,int>,deliberate:array<string,int>,involuntary:array<string,int>}>}
	 */
	public function summarize( $coverage ): array {
		$structured = self::is_structured( $coverage );
		$scopes     = $structured && is_array( $coverage ) ? $this->normalize_scopes( $coverage['scopes'] ) : self::empty_scopes();
		if ( ! $structured ) {
			$scopes['files'] = $this->normalize_reasons( $coverage );
		}

		$scope_summaries = array(
			'files'    => $this->summarize_reasons( $scopes['files'] ),
			'database' => $this->summarize_reasons( $scopes['database'] ),
			'plugins'  => $this->summarize_reasons( $scopes['plugins'] ),
		);
		$all_reasons     = $this->merge_counts( $scopes['files'], $scopes['database'] );
		$all_reasons     = $this->merge_counts( $all_reasons, $scopes['plugins'] );
		$overall         = $this->summarize_reasons( $all_reasons );

		return array(
			'version'     => $structured ? self::VERSION : 1,
			'structured'  => $structured,
			'scopes'      => $scope_summaries,
			'complete'    => $overall['complete'],
			'verified'    => $overall['verified'],
			'omitted'     => $overall['omitted'],
			'reasons'     => $overall['reasons'],
			'deliberate'  => $overall['deliberate'],
			'involuntary' => $overall['involuntary'],
		);
	}

	/**
	 * @param mixed $json Stored coverage JSON.
	 * @return array{version:int,structured:bool,complete:bool,verified:bool,omitted:int,reasons:array<string,int>,deliberate:array<string,int>,involuntary:array<string,int>,scopes:array<string,array{complete:bool,verified:bool,omitted:int,reasons:array<string,int>,deliberate:array<string,int>,involuntary:array<string,int>}>}
	 */
	public function from_json( $json ): array {
		if ( ! is_string( $json ) || '' === $json ) {
			return $this->summarize( array() );
		}
		return $this->summarize( json_decode( $json, true ) );
	}

	/**
	 * @param mixed $coverage Raw coverage.
	 * @return bool
	 */
	public static function is_verified( $coverage ): bool {
		$summary = ( new self() )->summarize( $coverage );
		return ! empty( $summary['verified'] );
	}

	/**
	 * @param mixed  $coverage Raw structured coverage.
	 * @param string $scope    Closed scope.
	 * @return bool
	 */
	public function scope_is_verified( $coverage, string $scope ): bool {
		if ( ! self::is_structured( $coverage ) || ! in_array( $scope, self::SCOPES, true ) ) {
			return false;
		}
		$summary = $this->summarize( $coverage );
		return ! empty( $summary['scopes'][ $scope ]['verified'] );
	}

	/**
	 * @param mixed $coverage Raw coverage.
	 * @return bool
	 */
	public static function is_structured( $coverage ): bool {
		return is_array( $coverage ) && isset( $coverage['version'], $coverage['scopes'] ) && self::VERSION === (int) $coverage['version'] && is_array( $coverage['scopes'] );
	}

	/**
	 * @param mixed $scopes Raw scope map.
	 * @return array{files:array<string,int>,database:array<string,int>,plugins:array<string,int>}
	 */
	private function normalize_scopes( $scopes ): array {
		$normalized = self::empty_scopes();
		if ( ! is_array( $scopes ) ) {
			return $normalized;
		}
		foreach ( self::SCOPES as $scope ) {
			$normalized[ $scope ] = $this->normalize_reasons( isset( $scopes[ $scope ] ) ? $scopes[ $scope ] : array() );
		}
		return $normalized;
	}

	/**
	 * @param mixed $reasons Raw reason counts.
	 * @return array<string,int>
	 */
	private function normalize_reasons( $reasons ): array {
		$normalized = array();
		if ( ! is_array( $reasons ) ) {
			return $normalized;
		}
		foreach ( $reasons as $reason => $count ) {
			$reason = strtolower( (string) preg_replace( '/[^a-z0-9_\-]/i', '', (string) $reason ) );
			$count  = max( 0, (int) $count );
			if ( '' !== $reason && $count > 0 ) {
				$normalized[ $reason ] = $count;
			}
		}
		return $normalized;
	}

	/**
	 * @param array<string,int> $reasons Normalized counts.
	 * @return array{complete:bool,verified:bool,omitted:int,reasons:array<string,int>,deliberate:array<string,int>,involuntary:array<string,int>}
	 */
	private function summarize_reasons( array $reasons ): array {
		$deliberate  = array();
		$involuntary = array();
		foreach ( $reasons as $reason => $count ) {
			if ( in_array( $reason, self::DELIBERATE_REASONS, true ) ) {
				$deliberate[ $reason ] = $count;
			} else {
				$involuntary[ $reason ] = $count;
			}
		}
		return array(
			'complete'    => empty( $reasons ),
			'verified'    => empty( $involuntary ),
			'omitted'     => array_sum( $reasons ),
			'reasons'     => $reasons,
			'deliberate'  => $deliberate,
			'involuntary' => $involuntary,
		);
	}

	/**
	 * @param array<string,int> $left  Existing counts.
	 * @param array<string,int> $right New counts.
	 * @return array<string,int>
	 */
	private function merge_counts( array $left, array $right ): array {
		foreach ( $right as $reason => $count ) {
			$left[ $reason ] = isset( $left[ $reason ] ) ? $left[ $reason ] + $count : $count;
		}
		return $left;
	}
}
