<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class ASA_Semgrep_Rules {

	const OPTION_KEY = 'asa_semgrep_rule_settings';
	const CRON_HOOK  = 'asa_semgrep_rule_sync_event';

	public static function get_default_settings() {
		return array(
			'auto_sync_enabled' => 0,
			'registry_refs'     => array(),
			'remote_sources'    => array(),
			'local_rule_files'  => array(),
			'downloaded_files'  => array(),
			'last_sync'         => array(),
		);
	}

	public function init() {
		add_action( 'init', array( $this, 'maybe_schedule_events' ) );
		add_action( 'admin_post_asa_save_semgrep_rule_settings', array( $this, 'handle_save_settings' ) );
		add_action( 'admin_post_asa_sync_semgrep_rules', array( $this, 'handle_sync' ) );
		add_action( 'admin_post_asa_upload_semgrep_rules', array( $this, 'handle_upload' ) );
		add_action( self::CRON_HOOK, array( __CLASS__, 'cron_sync' ) );
	}

	public static function get_settings() {
		$settings = get_option( self::OPTION_KEY, array() );
		return wp_parse_args( is_array( $settings ) ? $settings : array(), self::get_default_settings() );
	}

	public function maybe_schedule_events() {
		$settings = self::get_settings();
		$enabled  = ! empty( $settings['auto_sync_enabled'] ) && ! empty( $settings['remote_sources'] );
		$next     = wp_next_scheduled( self::CRON_HOOK );
		if ( $enabled && ! $next ) {
			wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK );
		} elseif ( ! $enabled && $next ) {
			wp_clear_scheduled_hook( self::CRON_HOOK );
		}
	}

	public static function cron_sync() {
		ASA_Logger::log( 'configuration', 'info', 'semgrep', 'cron_sync', __( 'Automatic Semgrep community rule synchronization started.', 'aegisify-audit-agent' ), array(), 10 );
		self::sync_remote_sources();
	}

	public static function get_runtime_configs() {
		$settings = self::get_settings();
		$configs  = array();

		foreach ( (array) $settings['registry_refs'] as $ref ) {
			$ref = trim( (string) $ref );
			if ( '' !== $ref ) {
				$configs[] = $ref;
			}
		}

		foreach ( array_merge( (array) $settings['local_rule_files'], (array) $settings['downloaded_files'] ) as $file_path ) {
			$file_path = wp_normalize_path( (string) $file_path );
			if ( '' !== $file_path && file_exists( $file_path ) && is_readable( $file_path ) ) {
				$configs[] = $file_path;
			}
		}

		return array_values( array_unique( $configs ) );
	}

	public static function sync_remote_sources() {
		$settings         = self::get_settings();
		ASA_Logger::log( 'configuration', 'info', 'semgrep', 'sync_remote_sources', __( 'Semgrep community rule synchronization started.', 'aegisify-audit-agent' ), array( 'remote_source_count' => count( (array) $settings['remote_sources'] ) ), 10 );
		$existing_files   = array_values( array_filter( array_map( 'wp_normalize_path', (array) $settings['downloaded_files'] ) ) );
		$downloaded_files = array();
		$messages         = array();
		$download_count   = 0;

		foreach ( (array) $settings['remote_sources'] as $source_url ) {
			$download = self::download_rule_bundle( $source_url );
			if ( empty( $download['files'] ) ) {
				$messages[] = ! empty( $download['message'] ) ? $download['message'] : __( 'A Semgrep rule source could not be imported.', 'aegisify-audit-agent' );
				continue;
			}
			$download_count   += count( $download['files'] );
			$downloaded_files = array_merge( $downloaded_files, $download['files'] );
		}

		if ( $download_count > 0 ) {
			self::clean_existing_downloaded_files( $existing_files );
			$settings['downloaded_files'] = array_values( array_unique( array_filter( array_map( 'wp_normalize_path', $downloaded_files ) ) ) );
			$status  = 'success';
			/* translators: %d: Number of external Semgrep rule files imported. */
			$message = sprintf( __( 'Imported %d external Semgrep rule file(s).', 'aegisify-audit-agent' ), $download_count );
		} else {
			$settings['downloaded_files'] = $existing_files;
			$status  = ! empty( $settings['remote_sources'] ) ? 'empty' : 'config_missing';
			$message = ! empty( $messages ) ? implode( ' | ', $messages ) : __( 'No external Semgrep rules were imported.', 'aegisify-audit-agent' );
		}

		$settings['last_sync'] = array(
			'status'     => $status,
			'message'    => sanitize_text_field( $message ),
			'synced_at'  => current_time( 'mysql' ),
			'file_count' => $download_count,
		);
		update_option( self::OPTION_KEY, $settings, false );
		ASA_Logger::log( 'configuration', 'success' === $status ? 'success' : 'warning', 'semgrep', 'sync_remote_sources', $message, array( 'file_count' => $download_count ), 10 );
		return $settings['last_sync'];
	}

	private static function get_rules_base_dir() {
		$upload_dir = wp_upload_dir();
		$base_dir   = trailingslashit( $upload_dir['basedir'] ) . 'aegisify-audit-agent/semgrep-community';
		wp_mkdir_p( $base_dir );
		return wp_normalize_path( $base_dir );
	}

	private static function clean_existing_downloaded_files( array $files ) {
		foreach ( $files as $file_path ) {
			$file_path = wp_normalize_path( (string) $file_path );
			if ( '' !== $file_path && file_exists( $file_path ) ) {
				self::delete_file( $file_path );
			}
		}
	}

	private static function download_rule_bundle( $source_url ) {
		$result = array(
			'files'   => array(),
			'message' => '',
		);
		$source_url = esc_url_raw( (string) $source_url );
		if ( '' === $source_url ) {
			$result['message'] = __( 'External Semgrep source URL was empty.', 'aegisify-audit-agent' );
			ASA_Logger::log( 'network', 'warning', 'semgrep', 'download_rule_bundle', $result['message'], array(), 10 );
			return $result;
		}
		if ( 0 !== strpos( $source_url, 'https://' ) ) {
			$result['message'] = __( 'External Semgrep source URLs must use HTTPS.', 'aegisify-audit-agent' );
			ASA_Logger::log( 'network', 'warning', 'semgrep', 'download_rule_bundle', $result['message'], array( 'source_url' => $source_url ), 10 );
			return $result;
		}

		ASA_Logger::log( 'network', 'info', 'semgrep', 'download_rule_bundle', __( 'Downloading a Semgrep community rule bundle from a remote HTTPS source.', 'aegisify-audit-agent' ), array( 'source_url' => $source_url ), 10 );
		$response = wp_safe_remote_get(
			$source_url,
			array(
				'timeout'     => 30,
				'sslverify'   => true,
				'redirection' => 3,
			)
		);
		if ( is_wp_error( $response ) ) {
			$result['message'] = $response->get_error_message();
			ASA_Logger::log( 'network', 'error', 'semgrep', 'download_rule_bundle', __( 'Downloading a Semgrep community rule bundle failed.', 'aegisify-audit-agent' ), array( 'source_url' => $source_url, 'message' => $response->get_error_message() ), 10 );
			return $result;
		}
		$code = (int) wp_remote_retrieve_response_code( $response );
		if ( 200 !== $code ) {
			/* translators: %d: HTTP status code returned by the external Semgrep source. */
			$result['message'] = sprintf( __( 'External Semgrep source returned HTTP %d.', 'aegisify-audit-agent' ), $code );
			ASA_Logger::log( 'network', 'error', 'semgrep', 'download_rule_bundle', $result['message'], array( 'source_url' => $source_url, 'http_code' => $code ), 10 );
			return $result;
		}

		$tmp_file = wp_tempnam( 'asa-semgrep-rule' );
		if ( ! $tmp_file ) {
			$result['message'] = __( 'Unable to allocate temporary storage for the Semgrep rule download.', 'aegisify-audit-agent' );
			ASA_Logger::log( 'system', 'error', 'semgrep', 'download_rule_bundle', $result['message'], array( 'source_url' => $source_url ), 10 );
			return $result;
		}
		$written = @file_put_contents( $tmp_file, (string) wp_remote_retrieve_body( $response ) );
		if ( false === $written ) {
			self::delete_file( $tmp_file );
			$result['message'] = __( 'Unable to write the downloaded Semgrep rule package.', 'aegisify-audit-agent' );
			ASA_Logger::log( 'system', 'error', 'semgrep', 'download_rule_bundle', $result['message'], array( 'source_url' => $source_url ), 10 );
			return $result;
		}

		$filename         = self::determine_download_filename( $source_url, $response );
		$result['files']  = self::store_rule_bundle( $tmp_file, $filename );
		self::delete_file( $tmp_file );
		if ( empty( $result['files'] ) ) {
			$result['message'] = __( 'The downloaded Semgrep rule package did not contain supported YAML or JSON rule files.', 'aegisify-audit-agent' );
			ASA_Logger::log( 'network', 'warning', 'semgrep', 'download_rule_bundle', $result['message'], array( 'source_url' => $source_url ), 10 );
		} else {
			ASA_Logger::log( 'network', 'success', 'semgrep', 'download_rule_bundle', __( 'A Semgrep community rule bundle was downloaded successfully.', 'aegisify-audit-agent' ), array( 'source_url' => $source_url, 'file_count' => count( $result['files'] ) ), 10 );
		}
		return $result;
	}

	private static function determine_download_filename( $source_url, $response ) {
		$path     = (string) wp_parse_url( $source_url, PHP_URL_PATH );
		$filename = sanitize_file_name( basename( $path ) );
		if ( '' !== $filename && preg_match( '/\.(?:ya?ml|json|zip)$/i', $filename ) ) {
			return $filename;
		}

		$headers = wp_remote_retrieve_headers( $response );
		$content_type = '';
			if ( is_object( $headers ) && method_exists( $headers, 'offsetGet' ) ) {
				$content_type = (string) $headers->offsetGet( 'content-type' );
			} elseif ( is_array( $headers ) && ! empty( $headers['content-type'] ) ) {
				$content_type = (string) $headers['content-type'];
			}
		$content_type = strtolower( $content_type );
		if ( false !== strpos( $content_type, 'zip' ) ) {
			return 'downloaded-rules.zip';
		}
		if ( false !== strpos( $content_type, 'json' ) ) {
			return 'downloaded-rules.json';
		}
		return 'downloaded-rules.yaml';
	}

	public static function store_uploaded_bundle( array $file ) {
		if ( empty( $file ) ) {
			return array();
		}
		require_once ABSPATH . 'wp-admin/includes/file.php';
		$uploaded = wp_handle_upload(
			$file,
			array(
				'test_form' => false,
				'mimes'     => array(
					'yaml' => 'text/yaml',
					'yml'  => 'text/yaml',
					'json' => 'application/json',
					'zip'  => 'application/zip',
				),
			)
		);
		if ( ! empty( $uploaded['error'] ) || empty( $uploaded['file'] ) ) {
			return array();
		}
		$stored = self::store_rule_bundle( (string) $uploaded['file'], (string) $file['name'] );
		self::delete_file( $uploaded['file'] );
		return $stored;
	}

	private static function store_rule_bundle( $source_path, $original_name ) {
		$stored_files  = array();
		$source_path   = (string) $source_path;
		$original_name = sanitize_file_name( (string) $original_name );
		if ( '' === $source_path || ! file_exists( $source_path ) ) {
			return $stored_files;
		}
		$base_dir = self::get_rules_base_dir();
		$ext      = strtolower( pathinfo( $original_name, PATHINFO_EXTENSION ) );

		if ( 'zip' === $ext && class_exists( 'ZipArchive' ) ) {
			$tmp_dir = trailingslashit( get_temp_dir() ) . 'asa-semgrep-' . wp_generate_password( 8, false, false );
			wp_mkdir_p( $tmp_dir );
			$zip = new ZipArchive();
			if ( true === $zip->open( $source_path ) ) {
				$zip->extractTo( $tmp_dir );
				$zip->close();
				$iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $tmp_dir, RecursiveDirectoryIterator::SKIP_DOTS ) );
				foreach ( $iterator as $file_info ) {
					if ( ! $file_info->isFile() ) {
						continue;
					}
					$filename = $file_info->getFilename();
					if ( ! preg_match( '/\.(?:ya?ml|json)$/i', $filename ) ) {
						continue;
					}
					$target = $base_dir . '/' . wp_unique_filename( $base_dir, sanitize_file_name( $filename ) );
					if ( @copy( $file_info->getPathname(), $target ) ) {
						$stored_files[] = wp_normalize_path( $target );
					}
				}
			}
			self::delete_path_recursive( $tmp_dir );
			return array_values( array_unique( $stored_files ) );
		}

		if ( preg_match( '/\.(?:ya?ml|json)$/i', $original_name ) ) {
			$target = $base_dir . '/' . wp_unique_filename( $base_dir, $original_name );
			if ( @copy( $source_path, $target ) ) {
				$stored_files[] = wp_normalize_path( $target );
			}
		}

		return array_values( array_unique( $stored_files ) );
	}

	private static function delete_path_recursive( $path ) {
		$path = wp_normalize_path( (string) $path );
		if ( '' === $path || ! file_exists( $path ) ) {
			return;
		}
		if ( is_file( $path ) ) {
			self::delete_file( $path );
			return;
		}
		$iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $path, RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST );
		foreach ( $iterator as $item ) {
			if ( $item->isDir() ) {
				self::delete_directory( $item->getPathname() );
			} else {
				self::delete_file( $item->getPathname() );
			}
		}
		self::delete_directory( $path );
	}


	private static function ensure_wp_file_api() {
		if ( ! function_exists( 'wp_delete_file' ) || ! function_exists( 'WP_Filesystem' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}
	}

	private static function delete_file( $file_path ) {
		$file_path = wp_normalize_path( (string) $file_path );
		if ( '' === $file_path || ! file_exists( $file_path ) || ! is_file( $file_path ) ) {
			return;
		}
		self::ensure_wp_file_api();
		wp_delete_file( $file_path );
	}

	private static function delete_directory( $path ) {
		$path = wp_normalize_path( (string) $path );
		if ( '' === $path || ! is_dir( $path ) ) {
			return;
		}
		self::ensure_wp_file_api();
		global $wp_filesystem;
		WP_Filesystem();
		if ( isset( $wp_filesystem ) && is_object( $wp_filesystem ) && method_exists( $wp_filesystem, 'rmdir' ) ) {
			$wp_filesystem->rmdir( $path, true );
			return;
		}
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- Narrow fallback when the WordPress filesystem API is unavailable.
		@rmdir( $path );
	}

	private static function get_uploaded_rule_file() {
		if ( empty( $_FILES['asa_semgrep_rule_file'] ) || ! is_array( $_FILES['asa_semgrep_rule_file'] ) ) {
			return array();
		}

		$file = wp_unslash( $_FILES['asa_semgrep_rule_file'] );
		return array(
			'name'     => isset( $file['name'] ) ? sanitize_file_name( (string) $file['name'] ) : '',
			'type'     => isset( $file['type'] ) ? sanitize_mime_type( (string) $file['type'] ) : '',
			'tmp_name' => isset( $file['tmp_name'] ) ? wp_normalize_path( sanitize_text_field( (string) $file['tmp_name'] ) ) : '',
			'error'    => isset( $file['error'] ) ? absint( $file['error'] ) : 0,
			'size'     => isset( $file['size'] ) ? absint( $file['size'] ) : 0,
		);
	}

	public function handle_save_settings() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Unauthorized.', 'aegisify-audit-agent' ) );
		}
		check_admin_referer( 'asa_save_semgrep_rule_settings_action', 'asa_save_semgrep_rule_settings_nonce' );
		$raw_sources = isset( $_POST['asa_semgrep_sources'] ) ? sanitize_textarea_field( wp_unslash( $_POST['asa_semgrep_sources'] ) ) : '';
		$lines       = preg_split( '/\r\n|\r|\n/', $raw_sources );
		$lines       = array_filter( array_map( 'trim', is_array( $lines ) ? $lines : array() ) );
		$registry_refs  = array();
		$remote_sources = array();
		foreach ( $lines as $line ) {
			if ( preg_match( '/^(?:p|r)\/[A-Za-z0-9._\/-]+$/', $line ) ) {
				$registry_refs[] = $line;
				continue;
			}
			$line_url = esc_url_raw( $line );
			if ( '' !== $line_url && 0 === strpos( $line_url, 'https://' ) ) {
				$remote_sources[] = $line_url;
			}
		}
		$settings = self::get_settings();
		$settings['auto_sync_enabled'] = isset( $_POST['asa_semgrep_auto_sync_enabled'] ) ? 1 : 0;
		$settings['registry_refs']     = array_values( array_unique( $registry_refs ) );
		$settings['remote_sources']    = array_values( array_unique( $remote_sources ) );
		update_option( self::OPTION_KEY, $settings, false );
		$this->maybe_schedule_events();
		ASA_Logger::log( 'configuration', 'success', 'semgrep', 'save_rule_settings', __( 'Semgrep community rule settings were updated from wp-admin.', 'aegisify-audit-agent' ), array( 'registry_ref_count' => count( $settings['registry_refs'] ), 'remote_source_count' => count( $settings['remote_sources'] ), 'auto_sync_enabled' => ! empty( $settings['auto_sync_enabled'] ) ), 10 );
		wp_safe_redirect( add_query_arg( array( 'page' => 'aegisify-audit-agent', 'asa_msg' => 'semgrep_sources_saved' ), admin_url( 'admin.php' ) ) );
		exit;
	}

	public function handle_sync() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Unauthorized.', 'aegisify-audit-agent' ) );
		}
		check_admin_referer( 'asa_sync_semgrep_rules_action', 'asa_sync_semgrep_rules_nonce' );
		$result = self::sync_remote_sources();
		ASA_Logger::log( 'configuration', ( ! empty( $result['status'] ) && 'success' === $result['status'] ) ? 'success' : 'warning', 'semgrep', 'manual_sync', __( 'A manual Semgrep community rule synchronization request completed.', 'aegisify-audit-agent' ), array( 'status' => ! empty( $result['status'] ) ? (string) $result['status'] : '', 'file_count' => ! empty( $result['file_count'] ) ? absint( $result['file_count'] ) : 0 ), 10 );
		wp_safe_redirect( add_query_arg( array( 'page' => 'aegisify-audit-agent', 'asa_msg' => ( ! empty( $result['status'] ) && 'success' === $result['status'] ) ? 'semgrep_rules_synced' : 'semgrep_rules_failed' ), admin_url( 'admin.php' ) ) );
		exit;
	}

	public function handle_upload() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Unauthorized.', 'aegisify-audit-agent' ) );
		}
		check_admin_referer( 'asa_upload_semgrep_rules_action', 'asa_upload_semgrep_rules_nonce' );
		$uploaded = self::get_uploaded_rule_file();
		$files    = self::store_uploaded_bundle( $uploaded );
		$settings = self::get_settings();
		$settings['local_rule_files'] = array_values( array_unique( array_merge( (array) $settings['local_rule_files'], $files ) ) );
		update_option( self::OPTION_KEY, $settings, false );
		ASA_Logger::log( 'configuration', ! empty( $files ) ? 'success' : 'warning', 'semgrep', 'upload_rule_bundle', ! empty( $files ) ? __( 'A local Semgrep community rule bundle was uploaded successfully.', 'aegisify-audit-agent' ) : __( 'A local Semgrep community rule upload did not produce any usable rule files.', 'aegisify-audit-agent' ), array( 'uploaded_file_count' => count( $files ) ), 10 );
		wp_safe_redirect( add_query_arg( array( 'page' => 'aegisify-audit-agent', 'asa_msg' => ! empty( $files ) ? 'semgrep_rules_uploaded' : 'semgrep_rules_failed' ), admin_url( 'admin.php' ) ) );
		exit;
	}
}
