<?php
/**
 * Daily vendor integrity monitor for admin-autonomy.
 *
 * @package Niwa_AI
 */

defined( 'ABSPATH' ) || exit;

class Niwa_AI_Admin_Autonomy_Vendor_Integrity_Monitor {
	const HOOK            = 'niwa_ai_admin_autonomy_check_vendor_integrity';
	const MANIFEST_OPTION = 'niwa_ai_admin_autonomy_vendor_manifest';
	const STATUS_OPTION   = 'niwa_ai_admin_autonomy_vendor_lock_status';
	const LOCK_NAME       = 'vendor_integrity_response';
	const SCAN_LOCK_NAME  = 'vendor_integrity_scan';
	const LOCK_TTL        = 86400;

	/** @var array<string,callable> */
	private $callbacks;

	public function __construct( $callbacks = array() ) {
		$this->callbacks = is_array( $callbacks ) ? $callbacks : array();
	}

	public function run() {
		if ( ! $this->acquire_lock( self::SCAN_LOCK_NAME, 300 ) ) {
			return array(
				'success' => true,
				'skipped' => true,
				'reason'  => 'scan_locked',
			);
		}

		try {
			$previous = get_option( self::MANIFEST_OPTION, array() );
			$previous = is_array( $previous ) ? $previous : array();
			$current  = $this->build_manifest();

			if ( empty( $current['digest'] ) ) {
				return $this->record_status(
					array(
						'locked'     => false,
						'checked_at' => time(),
						'error'      => 'vendor_manifest_unavailable',
					)
				);
			}

			if ( empty( $previous['digest'] ) ) {
				update_option( self::MANIFEST_OPTION, $current, false );
				return $this->record_status(
					array(
						'locked'      => false,
						'initialized' => true,
						'checked_at'  => time(),
						'digest'      => $current['digest'],
						'file_count'  => $current['file_count'],
					)
				);
			}

			if ( hash_equals( (string) $previous['digest'], (string) $current['digest'] ) ) {
				$status = get_option( self::STATUS_OPTION, array() );
				$status = is_array( $status ) ? $status : array();
				$locked_until = intval( isset( $status['detected_at'] ) ? $status['detected_at'] : 0 ) + self::LOCK_TTL;
				if ( ! empty( $status['locked'] ) && time() < $locked_until && isset( $status['digest'] ) && hash_equals( (string) $status['digest'], (string) $current['digest'] ) ) {
					$status['success']    = true;
					$status['changed']    = false;
					$status['checked_at'] = time();
					update_option( self::STATUS_OPTION, $status, false );
					return $status;
				}

				return $this->record_status(
					array(
						'locked'      => false,
						'changed'     => false,
						'checked_at'  => time(),
						'digest'      => $current['digest'],
						'file_count'  => $current['file_count'],
					)
				);
			}

			$changes       = $this->diff_manifests( $previous, $current );
			$lock_acquired = $this->acquire_lock( self::LOCK_NAME, self::LOCK_TTL );
			$status        = array(
				'locked'          => true,
				'changed'         => true,
				'lock_name'       => self::LOCK_NAME,
				'lock_ttl'        => self::LOCK_TTL,
				'lock_acquired'   => $lock_acquired,
				'detected_at'     => time(),
				'previous_digest' => (string) $previous['digest'],
				'digest'          => (string) $current['digest'],
				'file_count'      => $current['file_count'],
				'changes'         => $changes,
			);
			update_option( self::STATUS_OPTION, $status, false );

			$enqueue = $this->enqueue_agent_task( $current, $previous, $changes, $lock_acquired );
			$status['agent_task'] = $enqueue;
			update_option( self::STATUS_OPTION, $status, false );
			update_option( self::MANIFEST_OPTION, $current, false );

			return array_merge(
				array(
					'success' => true,
					'changed' => true,
				),
				$status
			);
		} finally {
			$this->release_lock( self::SCAN_LOCK_NAME );
		}
	}

	private function build_manifest() {
		$vendor_dir = defined( 'NIWA_AI_DIR' ) ? NIWA_AI_DIR . 'vendor' : '';
		if ( '' === $vendor_dir || ! is_dir( $vendor_dir ) ) {
			return array();
		}

		$files        = array();
		$digest_files = array();
		$root         = wp_normalize_path( realpath( $vendor_dir ) );
		$it           = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator( $vendor_dir, FilesystemIterator::SKIP_DOTS ),
			RecursiveIteratorIterator::LEAVES_ONLY
		);

		foreach ( $it as $file ) {
			if ( ! $file instanceof SplFileInfo || ! $file->isFile() ) {
				continue;
			}

			$path = $file->getPathname();
			if ( ! is_readable( $path ) ) {
				continue;
			}

			$real = wp_normalize_path( realpath( $path ) );
			$rel  = ltrim( substr( $real, strlen( $root ) ), '/' );
			if ( '' === $rel ) {
				continue;
			}

			$hash = hash_file( 'sha256', $path );
			$size = $file->getSize();

			$files[ $rel ] = array(
				'sha256' => $hash,
				'size'   => $size,
				'mtime'  => $file->getMTime(),
			);
			$digest_files[ $rel ] = array(
				'sha256' => $hash,
				'size'   => $size,
			);
		}

		ksort( $files, SORT_STRING );
		ksort( $digest_files, SORT_STRING );
		$encoded = wp_json_encode( $digest_files );

		return array(
			'version'    => defined( 'NIWA_AI_VERSION' ) ? (string) NIWA_AI_VERSION : '',
			'built_at'   => time(),
			'file_count' => count( $files ),
			'digest'     => hash( 'sha256', (string) $encoded ),
			'files'      => $files,
		);
	}

	private function diff_manifests( $previous, $current ) {
		$old = isset( $previous['files'] ) && is_array( $previous['files'] ) ? $previous['files'] : array();
		$new = isset( $current['files'] ) && is_array( $current['files'] ) ? $current['files'] : array();

		$added    = array_values( array_diff( array_keys( $new ), array_keys( $old ) ) );
		$removed  = array_values( array_diff( array_keys( $old ), array_keys( $new ) ) );
		$modified = array();

		foreach ( $new as $path => $meta ) {
			if ( ! isset( $old[ $path ] ) ) {
				continue;
			}
			$old_hash = isset( $old[ $path ]['sha256'] ) ? (string) $old[ $path ]['sha256'] : '';
			$new_hash = isset( $meta['sha256'] ) ? (string) $meta['sha256'] : '';
			if ( $old_hash !== $new_hash ) {
				$modified[] = $path;
			}
		}

		sort( $added, SORT_STRING );
		sort( $removed, SORT_STRING );
		sort( $modified, SORT_STRING );

		return array(
			'added_count'    => count( $added ),
			'removed_count'  => count( $removed ),
			'modified_count' => count( $modified ),
			'added'          => array_slice( $added, 0, 25 ),
			'removed'        => array_slice( $removed, 0, 25 ),
			'modified'       => array_slice( $modified, 0, 25 ),
		);
	}

	private function enqueue_agent_task( $current, $previous, $changes, $lock_acquired ) {
		$message = array(
			'success'        => true,
			'source'         => 'vendor_integrity_monitor',
			'channel'        => 'admin_ui',
			'chat_id'        => '',
			'message_id'     => abs( crc32( 'vendor-integrity:' . (string) $current['digest'] ) ),
			'update_id'      => 0,
			'raw_message_id' => 'vendor-integrity:' . (string) $current['digest'],
			'date'           => time(),
			'from_id'        => '',
			'user_role'      => 'admin',
			'kind'           => 'vendor_integrity_change',
			'text'           => $this->build_agent_prompt( $current, $previous, $changes, $lock_acquired ),
			'text_only'      => '',
			'caption'        => '',
			'media_refs'     => array(),
			'reply_media_refs' => array(),
			'has_user_text'  => true,
			'context_payload' => array(
				'source'                  => 'vendor_integrity_monitor',
				'force_action'            => true,
				'vendor_integrity_digest' => (string) $current['digest'],
				'vendor_integrity_lock'   => self::LOCK_NAME,
			),
		);
		$message['text_only'] = $message['text'];

		return $this->call( 'enqueue_actor_message', array( $message ), array(
			'success' => false,
			'error'   => 'enqueue_callback_unavailable',
		) );
	}

	private function build_agent_prompt( $current, $previous, $changes, $lock_acquired ) {
		$payload = array(
			'event'           => 'vendor_integrity_change',
			'plugin_locked'   => (bool) $lock_acquired,
			'lock_name'       => self::LOCK_NAME,
			'previous_digest' => isset( $previous['digest'] ) ? (string) $previous['digest'] : '',
			'current_digest'  => isset( $current['digest'] ) ? (string) $current['digest'] : '',
			'file_count'      => isset( $current['file_count'] ) ? intval( $current['file_count'] ) : 0,
			'changes'         => $changes,
		);

		return implode(
			"\n",
			array(
				'Vendor integrity monitor detected a changed vendor manifest.',
				'Use main_coding_shell_run first to inspect git/vendor state and explain whether the dependency change is expected.',
				'Do not modify first-party files unless the inspection proves a required repair.',
				'The plugin mutation lane is locked through the vendor_integrity_response lock while this task is investigated.',
				'Structured event JSON:',
				(string) wp_json_encode( $payload ),
			)
		);
	}

	private function record_status( $status ) {
		$status = is_array( $status ) ? $status : array();
		update_option( self::STATUS_OPTION, $status, false );

		return array_merge(
			array(
				'success' => true,
			),
			$status
		);
	}

	private function acquire_lock( $name, $ttl ) {
		return (bool) $this->call( 'acquire_lock', array( $name, $ttl ), false );
	}

	private function release_lock( $name ) {
		$this->call( 'release_lock', array( $name ), null );
	}

	private function call( $name, $args = array(), $default = null ) {
		return Niwa_AI_Admin_Autonomy_Callbacks::call( $this->callbacks, $name, $args, $default );
	}
}
