<?php
/**
 * Tool integrity validator for Safe-Clean runtime checks.
 *
 * Detects tool routing callsites whose implementation methods are missing on
 * the host class. A broken tool is blocked with a structured tool payload
 * before PHP can fatal on an undefined method call.
 *
 * @package Niwa_AI
 * @since   0.13.71
 */

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

class Niwa_AI_Tool_Integrity_Validator {

	const TRANSIENT_KEY = 'niwa_ai_tool_integrity_report';
	const TRANSIENT_TTL = 300;

	const TRAIT_FILES = array(
		'includes/admin-autonomy/traits/trait-chat-tool-executor.php',
		'includes/admin-autonomy/traits/trait-chat-tool-definitions.php',
	);

	/**
	 * @var array|null
	 */
	private static $report = null;

	public static function report( $host_instance, $force_rescan = false ) {
		if ( ! is_object( $host_instance ) ) {
			return self::empty_report( '' );
		}

		$host_class = get_class( $host_instance );
		if ( ! $force_rescan && null !== self::$report && isset( self::$report['host_class'] ) && self::$report['host_class'] === $host_class ) {
			return self::$report;
		}

		if ( ! $force_rescan && function_exists( 'get_transient' ) ) {
			$cached = get_transient( self::TRANSIENT_KEY );
			if ( is_array( $cached ) && isset( $cached['host_class'] ) && $cached['host_class'] === $host_class && self::cache_still_valid( $cached ) ) {
				$cached['cached'] = true;
				self::$report     = $cached;
				return $cached;
			}
		}

		$report       = self::run_scan( $host_instance );
		self::$report = $report;
		if ( function_exists( 'set_transient' ) ) {
			set_transient( self::TRANSIENT_KEY, $report, self::TRANSIENT_TTL );
		}
		return $report;
	}

	public static function is_tool_broken( $host_instance, $tool_name ) {
		$tool_name = sanitize_key( (string) $tool_name );
		if ( '' === $tool_name ) {
			return false;
		}

		$report = self::report( $host_instance );
		return ! empty( $report['broken'][ $tool_name ] );
	}

	public static function blocked_payload( $host_instance, $tool_name ) {
		$tool_name = sanitize_key( (string) $tool_name );
		$report    = self::report( $host_instance );
		$info      = isset( $report['broken'][ $tool_name ] ) && is_array( $report['broken'][ $tool_name ] )
			? $report['broken'][ $tool_name ]
			: array();
		$method    = isset( $info['method'] ) ? (string) $info['method'] : $tool_name . '_payload';

		return array(
			'ok'                  => false,
			'success'             => false,
			'error'               => sprintf(
				'Tool "%s" is disabled by Safe-Clean integrity check: missing host method %s::%s(). The tool appears to have a routing callsite without a completed implementation.',
				$tool_name,
				isset( $report['host_class'] ) ? (string) $report['host_class'] : '',
				$method
			),
			'policy'              => 'safe_clean_integrity_block',
			'safe_clean_disabled' => true,
			'missing_method'      => $method,
			'callsites'           => isset( $info['callsites'] ) && is_array( $info['callsites'] ) ? $info['callsites'] : array(),
		);
	}

	private static function empty_report( $host_class ) {
		return array(
			'ok'              => true,
			'broken'          => array(),
			'checked_methods' => 0,
			'host_class'      => (string) $host_class,
			'scan_ms'         => 0.0,
			'cached'          => false,
			'file_mtimes'     => array(),
		);
	}

	private static function cache_still_valid( $cached ) {
		if ( ! isset( $cached['file_mtimes'] ) || ! is_array( $cached['file_mtimes'] ) ) {
			return false;
		}
		if ( ! defined( 'NIWA_AI_DIR' ) ) {
			return false;
		}

		foreach ( self::TRAIT_FILES as $rel ) {
			$path = trailingslashit( NIWA_AI_DIR ) . $rel;
			if ( ! file_exists( $path ) ) {
				return false;
			}
			$mtime = (int) @filemtime( $path );
			if ( ! isset( $cached['file_mtimes'][ $rel ] ) || intval( $cached['file_mtimes'][ $rel ] ) !== $mtime ) {
				return false;
			}
		}

		return true;
	}

	private static function run_scan( $host_instance ) {
		$start      = microtime( true );
		$host_class = get_class( $host_instance );
		$report     = self::empty_report( $host_class );

		if ( ! defined( 'NIWA_AI_DIR' ) ) {
			$report['ok']      = false;
			$report['scan_ms'] = round( ( microtime( true ) - $start ) * 1000, 2 );
			return $report;
		}

		$callsites_by_method = array();
		$file_mtimes         = array();

		foreach ( self::TRAIT_FILES as $rel ) {
			$path = trailingslashit( NIWA_AI_DIR ) . $rel;
			if ( ! file_exists( $path ) ) {
				continue;
			}
			$file_mtimes[ $rel ] = (int) @filemtime( $path );
			$content             = @file_get_contents( $path );
			if ( false === $content ) {
				continue;
			}
			if ( ! preg_match_all( '/\$this->([a-z_][a-z0-9_]*(?:_payload|_action))\s*\(/i', $content, $matches, PREG_OFFSET_CAPTURE ) ) {
				continue;
			}
			foreach ( $matches[1] as $match ) {
				$method = strtolower( (string) $match[0] );
				$offset = (int) $match[1];
				$line   = substr_count( substr( $content, 0, $offset ), "\n" ) + 1;
				if ( ! isset( $callsites_by_method[ $method ] ) ) {
					$callsites_by_method[ $method ] = array();
				}
				$callsites_by_method[ $method ][] = array(
					'file' => $rel,
					'line' => $line,
				);
			}
		}

		$broken = array();
		foreach ( $callsites_by_method as $method => $callsites ) {
			if ( method_exists( $host_instance, $method ) ) {
				continue;
			}
			$tool_name = preg_replace( '/_(payload|action)$/i', '', $method );
			if ( '' === $tool_name ) {
				continue;
			}
			$broken[ $tool_name ] = array(
				'method'    => $method,
				'callsites' => $callsites,
			);
		}

		$report['broken']          = $broken;
		$report['checked_methods'] = count( $callsites_by_method );
		$report['file_mtimes']     = $file_mtimes;
		$report['scan_ms']         = round( ( microtime( true ) - $start ) * 1000, 2 );
		$report['ok']              = empty( $broken );

		self::log_report( $report );
		return $report;
	}

	private static function log_report( $report ) {
		if ( ! empty( $report['broken'] ) && function_exists( 'error_log' ) ) {
			error_log( '[Niwa AI Safe-Clean] Disabled broken tools: ' . implode( ', ', array_keys( $report['broken'] ) ) );
		}
	}
}
