<?php

namespace Waypoint\Agent\Rest\Handler;

use Waypoint\Agent\Monitor\Status_Data;

/// SPEC.md §9.2 (Skymarshal, statenweb/skymarshal) — "Only these commands
/// exist... enforced in the API *and* in the plugin, for every role
/// including super admin." Skymarshal's own allow-list
/// (packages/core/src/actions/wp-cli-commands.ts) is the canonical list
/// this mirrors; this class does NOT trust Skymarshal's request to have
/// already validated command_id/args — every check here runs again,
/// independently, because a compromised or buggy caller is exactly the
/// threat model D4 exists for.
///
/// No `wp` binary involved anywhere — this runs inside a normal PHP-FPM
/// request, so every command is the direct WP core function equivalent of
/// its `wp ...` description, not a shell-out.
class Wp_Cli {

	private const ALLOWED_OPTION_NAMES = [
		'siteurl',
		'home',
		'blogname',
		'blogdescription',
		'template',
		'stylesheet',
		'active_plugins',
		'db_version',
		'WPLANG',
	];

	public function handle( \WP_REST_Request $request ): \WP_REST_Response {
		$command_id = (string) $request->get_param( 'command_id' );
		$args       = $request->get_param( 'args' );
		$args       = is_array( $args ) ? $args : [];

		switch ( $command_id ) {
			case 'plugin.list':
				return $this->ok( ( new Status_Data() )->plugin_list() );
			case 'theme.list':
				return $this->ok( ( new Status_Data() )->theme_list() );
			case 'cron.event_list':
				return $this->ok( $this->cron_event_list() );
			case 'option.get':
				return $this->option_get( $args );
			case 'core.check_update':
				return $this->ok( $this->core_check_update() );
			case 'plugin.activate':
				return $this->plugin_activate( $args );
			case 'plugin.deactivate':
				return $this->plugin_deactivate( $args );
			case 'core.update_db':
				return $this->ok( $this->core_update_db() );
			case 'cache.flush':
				wp_cache_flush();
				return $this->ok( [ 'flushed' => true ] );
			case 'rewrite.flush':
				flush_rewrite_rules( false );
				return $this->ok( [ 'flushed' => true ] );
			case 'transient.delete_all':
				return $this->ok( $this->transient_delete_all() );
			default:
				// Matches CLAUDE.md invariant #12 / SPEC.md §9.2's own
				// acceptance criterion literally: "a request for an
				// unknown WP-CLI command id is rejected by ... the
				// plugin." 400, not 404 — the route exists, the command
				// id doesn't.
				return rest_ensure_response( new \WP_Error(
					'waypoint_agent_unknown_wp_cli_command',
					sprintf( 'Unknown WP-CLI command id "%s".', $command_id ),
					[ 'status' => 400 ]
				) );
		}
	}

	private function ok( mixed $data ): \WP_REST_Response {
		return rest_ensure_response( [ 'ok' => true, 'data' => $data ] );
	}

	private function error( string $code, string $message, int $status ): \WP_REST_Response {
		return rest_ensure_response( new \WP_Error( $code, $message, [ 'status' => $status ] ) );
	}

	/// wp cron event list --format=json's real shape: hook, next run (ISO
	/// 8601 UTC), the schedule name (or "non-repeating"), and any args.
	private function cron_event_list(): array {
		$crons = _get_cron_array();
		if ( ! is_array( $crons ) ) {
			return [];
		}
		$out = [];
		foreach ( $crons as $timestamp => $hooks ) {
			foreach ( (array) $hooks as $hook => $events ) {
				foreach ( (array) $events as $event ) {
					$out[] = [
						'hook'     => $hook,
						'next_run' => gmdate( 'c', (int) $timestamp ),
						'schedule' => $event['schedule'] ?? 'non-repeating',
						'args'     => $event['args'] ?? [],
					];
				}
			}
		}
		return $out;
	}

	private function option_get( array $args ): \WP_REST_Response {
		$name = isset( $args['name'] ) ? (string) $args['name'] : '';
		if ( ! in_array( $name, self::ALLOWED_OPTION_NAMES, true ) ) {
			return $this->error(
				'waypoint_agent_option_not_allowed',
				sprintf( 'Option "%s" is not on the allow-listed option-name list.', $name ),
				400
			);
		}
		return $this->ok( [ 'name' => $name, 'value' => get_option( $name ) ] );
	}

	/// wp core check-update --format=json: forces a real check (the `true`
	/// arg to wp_version_check(), same as WP-CLI's own implementation —
	/// not the cached transient a plain page load would read) and reports
	/// whether the site's actual latest-known core version is newer.
	private function core_check_update(): array {
		if ( ! function_exists( 'wp_version_check' ) ) {
			require_once ABSPATH . 'wp-includes/update.php';
		}
		wp_version_check( [], true );
		$updates = get_site_transient( 'update_core' );
		$latest  = null;
		if ( is_object( $updates ) && ! empty( $updates->updates ) ) {
			foreach ( $updates->updates as $update ) {
				if ( isset( $update->response ) && 'upgrade' === $update->response ) {
					$latest = $update->version ?? null;
					break;
				}
			}
		}
		return [
			'current_version'  => get_bloginfo( 'version' ),
			'latest_version'   => $latest,
			'update_available' => null !== $latest,
		];
	}

	private function resolve_plugin_file( string $slug ): ?string {
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		foreach ( array_keys( get_plugins() ) as $file ) {
			$file_slug = dirname( $file ) === '.' ? basename( $file, '.php' ) : dirname( $file );
			if ( $file_slug === $slug ) {
				return $file;
			}
		}
		return null;
	}

	private function plugin_activate( array $args ): \WP_REST_Response {
		$slug = isset( $args['slug'] ) ? (string) $args['slug'] : '';
		$file = $this->resolve_plugin_file( $slug );
		if ( null === $file ) {
			return $this->error( 'waypoint_agent_plugin_not_found', sprintf( 'No installed plugin matches slug "%s".', $slug ), 404 );
		}
		$result = activate_plugin( $file );
		if ( is_wp_error( $result ) ) {
			return $this->error( 'waypoint_agent_plugin_activate_failed', $result->get_error_message(), 500 );
		}
		return $this->ok( [ 'slug' => $slug, 'file' => $file, 'active' => is_plugin_active( $file ) ] );
	}

	private function plugin_deactivate( array $args ): \WP_REST_Response {
		$slug = isset( $args['slug'] ) ? (string) $args['slug'] : '';
		$file = $this->resolve_plugin_file( $slug );
		if ( null === $file ) {
			return $this->error( 'waypoint_agent_plugin_not_found', sprintf( 'No installed plugin matches slug "%s".', $slug ), 404 );
		}
		deactivate_plugins( $file ); // no return value — never errors even if already inactive
		return $this->ok( [ 'slug' => $slug, 'file' => $file, 'active' => is_plugin_active( $file ) ] );
	}

	/// wp core update-db: exactly what that command runs under the hood.
	private function core_update_db(): array {
		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		$before = get_option( 'db_version' );
		wp_upgrade();
		return [ 'db_version_before' => $before, 'db_version_after' => get_option( 'db_version' ) ];
	}

	/// wp transient delete --all: WP-CLI's own implementation is a direct
	/// options-table delete, not a per-transient loop through the API
	/// (which would re-trigger autoload/cache churn for no benefit) —
	/// matched here rather than reinvented.
	private function transient_delete_all(): array {
		global $wpdb;
		$deleted = (int) $wpdb->query( $wpdb->prepare(
			"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
			$wpdb->esc_like( '_transient_' ) . '%',
			$wpdb->esc_like( '_transient_timeout_' ) . '%'
		) );
		wp_cache_flush();
		return [ 'deleted' => $deleted ];
	}
}
