<?php
/**
 * Ability: wp-agent/write-theme-file
 *
 * Schreibt eine Datei ausschliesslich in das aktive (Child-)Theme, also nach
 * wp-content/themes/<aktives-theme>/. Erlaubt sind nur die Endungen .html, .json
 * und .css. Pfad-Traversal wird sowohl lexikalisch (kein ".."-Segment, kein
 * absoluter Pfad) als auch per realpath-Prüfung gegen das Theme-Verzeichnis
 * verhindert. Alles andere wird mit WP_Error abgelehnt.
 *
 * @package WPAgentCompanion
 */

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

function wpagent_theme_file_expected_state_matches( string $target, string $expected_sha ): bool {
	if ( is_link( $target ) ) {
		return false;
	}
	$exists = file_exists( $target );
	if ( 'absent' === $expected_sha ) {
		return ! $exists;
	}
	$current_sha = $exists && is_file( $target ) ? hash_file( 'sha256', $target ) : false;
	return is_string( $current_sha ) && hash_equals( $expected_sha, $current_sha );
}

function wpagent_ability_write_theme_file(): array {
	return array(
		'label'               => __( 'Datei ins aktive Theme schreiben', 'wp-agent-companion' ),
		'description'         => __( 'Schreibt eine Datei ausschließlich innerhalb des aktiven (Child-)Themes. Nur .html, .json und .css sind erlaubt; Pfad-Traversal wird abgelehnt.', 'wp-agent-companion' ),
		'category'            => 'wp-agent',
		'input_schema'        => array(
			'type'                 => 'object',
			'properties'           => array(
				'rel_path' => array(
					'type'        => 'string',
					'maxLength'   => 255,
					'description' => __( 'Relativer Pfad innerhalb des Theme-Ordners, z. B. "patterns/hero.html" oder "assets/landing.css".', 'wp-agent-companion' ),
				),
				'contents' => array(
					'type'        => 'string',
					'maxLength'   => 1048576,
					'description' => __( 'Dateiinhalt.', 'wp-agent-companion' ),
				),
				'expected_theme'  => array( 'type' => 'string', 'maxLength' => 255 ),
				'expected_sha256' => array( 'type' => 'string', 'maxLength' => 64 ),
			),
			'required'             => array( 'rel_path', 'contents', 'expected_theme', 'expected_sha256' ),
			'additionalProperties' => false,
		),
		'output_schema'       => array(
			'type'       => 'object',
			'properties' => array(
				'path'     => array( 'type' => 'string' ),
				'rel_path' => array( 'type' => 'string' ),
				'bytes'    => array( 'type' => 'integer' ),
				'theme'    => array( 'type' => 'string' ),
				'sha256'   => array( 'type' => 'string' ),
				'verified' => array( 'type' => 'boolean' ),
			),
			'required'             => array( 'path', 'rel_path', 'bytes', 'theme', 'sha256', 'verified' ),
			'additionalProperties' => false,
		),
		'execute_callback'    => 'wpagent_exec_write_theme_file',
		'permission_callback' => 'wpagent_perm_edit_themes',
		'meta'                => array(
			'show_in_rest' => true,
			'annotations'  => array(
				'readonly'    => false,
				'destructive' => false,
				'idempotent'  => false,
			),
		),
	);
}

/**
 * @param mixed $input
 * @return array|WP_Error
 */
function wpagent_exec_write_theme_file( $input ) {
	$input          = is_array( $input ) ? $input : array();
	$rel            = (string) ( $input['rel_path'] ?? '' );
	$contents       = (string) ( $input['contents'] ?? '' );
	$expected_theme = $input['expected_theme'] ?? null;
	$expected_sha   = $input['expected_sha256'] ?? null;

	if ( '' === trim( $rel ) ) {
		return new WP_Error( 'wpagent_missing_path', __( 'rel_path fehlt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( false !== strpos( $rel, "\0" ) ) {
		return new WP_Error( 'wpagent_bad_path', __( 'Ungültiger Pfad.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( strlen( $contents ) > 1048576 ) {
		return new WP_Error( 'wpagent_contents_too_large', __( 'Dateiinhalt überschreitet das erlaubte Limit von 1 MiB.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( ! is_string( $expected_theme ) || '' === $expected_theme || ! is_string( $expected_sha ) || ( 'absent' !== $expected_sha && 1 !== preg_match( '/^[a-f0-9]{64}$/D', $expected_sha ) ) ) {
		return new WP_Error( 'wpagent_theme_file_expected_state', __( 'expected_theme und expected_sha256 müssen aus einem aktuellen, vertrauenswürdigen Lesevorgang stammen.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}

	// Lexikalische Traversal-Abwehr: Backslashes vereinheitlichen, absolute Pfade
	// und ".."-Segmente ablehnen, "."-/Leer-Segmente verwerfen.
	$normalized = str_replace( '\\', '/', $rel );
	if ( 0 === strpos( $normalized, '/' ) || preg_match( '#^[A-Za-z]:#', $normalized ) ) {
		return new WP_Error( 'wpagent_traversal', __( 'Absolute Pfade sind nicht erlaubt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$segments = array();
	foreach ( explode( '/', $normalized ) as $seg ) {
		if ( '' === $seg || '.' === $seg ) {
			continue;
		}
		if ( '..' === $seg ) {
			return new WP_Error( 'wpagent_traversal', __( 'Pfad-Traversal (..) ist nicht erlaubt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$segments[] = $seg;
	}
	if ( array() === $segments ) {
		return new WP_Error( 'wpagent_bad_path', __( 'Leerer Zielpfad.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$rel_norm = implode( '/', $segments );

	// Nur erlaubte Endungen.
	$ext     = strtolower( pathinfo( $rel_norm, PATHINFO_EXTENSION ) );
	$allowed = array( 'html', 'json', 'css' );
	if ( ! in_array( $ext, $allowed, true ) ) {
		return new WP_Error(
			'wpagent_bad_ext',
			__( 'Nur die Endungen .html, .json und .css sind erlaubt.', 'wp-agent-companion' ),
			array( 'status' => 400 )
		);
	}

	$lock = wpagent_core_mutator_acquire_lock( 'theme', 'active' );
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		if ( ! current_user_can( 'edit_themes' ) ) {
			return new WP_Error( 'wpagent_forbidden', __( 'Die Berechtigung änderte sich während der Sperrübernahme.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		if ( ! hash_equals( $expected_theme, (string) get_stylesheet() ) ) {
			return new WP_Error( 'wpagent_theme_conflict', __( 'Das aktive Theme wurde seit dem Lesen verändert. Lies die Theme-Liste neu.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		$real_theme = realpath( get_stylesheet_directory() );
		if ( false === $real_theme ) {
			return new WP_Error( 'wpagent_no_theme', __( 'Theme-Verzeichnis nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
		}
		$target = $real_theme . '/' . $rel_norm;

		// Vorhandene Symlinks am Ziel würden die realpath-Prüfung des Ordners
		// umgehen: Symlinks als Ziel grundsätzlich ablehnen.
		if ( is_link( $target ) ) {
			return new WP_Error( 'wpagent_symlink', __( 'Symlinks als Ziel sind nicht erlaubt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		if ( ! wpagent_theme_file_expected_state_matches( $target, $expected_sha ) ) {
			return new WP_Error( 'wpagent_theme_file_conflict', __( 'Die Theme-Datei wurde seit dem Lesen verändert. Lies ihren Zustand neu.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}

		// Jedes Elternsegment einzeln prüfen, BEVOR etwas angelegt wird. Ein
		// rekursives wp_mkdir_p() könnte sonst einem vorhandenen Symlink folgen und
		// bereits außerhalb des Theme-Jails Verzeichnisse erzeugen, bevor die
		// abschließende realpath-Prüfung den Ausbruch bemerkt.
		$current_parent = $real_theme;
		foreach ( array_slice( $segments, 0, -1 ) as $segment ) {
			$current_parent .= '/' . $segment;
			if ( is_link( $current_parent ) ) {
				return new WP_Error( 'wpagent_symlink', __( 'Symlinks im Zielpfad sind nicht erlaubt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			if ( file_exists( $current_parent ) && ! is_dir( $current_parent ) ) {
				return new WP_Error( 'wpagent_bad_path', __( 'Ein Elternsegment des Zielpfads ist kein Verzeichnis.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			if ( ! file_exists( $current_parent ) ) {
				if ( ! current_user_can( 'edit_themes' ) ) {
					return new WP_Error( 'wpagent_forbidden', __( 'Die Berechtigung ging vor dem Anlegen eines Zielverzeichnisses verloren.', 'wp-agent-companion' ), array( 'status' => 403 ) );
				}
				if ( ! wpagent_core_mutator_refresh_lock( 'theme', 'active', $lock ) ) {
					return new WP_Error( 'wpagent_theme_busy', __( 'Die Theme-Sperre ging vor dem Anlegen eines Zielverzeichnisses verloren.', 'wp-agent-companion' ), array( 'status' => 409 ) );
				}
				if ( ! mkdir( $current_parent, 0755 ) ) {
					return new WP_Error( 'wpagent_mkdir', __( 'Zielverzeichnis konnte nicht angelegt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
				}
			}
		}
		$parent      = dirname( $target );
		$real_parent = realpath( $parent );
		if ( false === $real_parent || 0 !== strpos( $real_parent . '/', $real_theme . '/' ) ) {
			return new WP_Error( 'wpagent_traversal', __( 'Zielpfad liegt außerhalb des Theme-Verzeichnisses.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}

		// Im selben Verzeichnis schreiben, auf Platte flushen und erst dann per
		// Rename ersetzen. So bleibt eine bestehende Datei bei Platten-/Prozess-
		// fehlern vollständig erhalten statt durch file_put_contents() teilweise
		// abgeschnitten zu werden. Re-read direkt vor der Mutation verkleinert den
		// unvermeidbaren POSIX-Race gegen nicht-kooperierende externe Schreiber.
		if ( ! wpagent_core_mutator_refresh_lock( 'theme', 'active', $lock ) ) {
			return new WP_Error( 'wpagent_theme_busy', __( 'Die Theme-Sperre ging vor dem Schreiben verloren.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		if ( ! wpagent_theme_file_expected_state_matches( $target, $expected_sha ) ) {
			return new WP_Error( 'wpagent_theme_file_conflict', __( 'Die Theme-Datei wurde vor dem Schreiben verändert. Lies ihren Zustand neu.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		if ( ! current_user_can( 'edit_themes' ) ) {
			return new WP_Error( 'wpagent_forbidden', __( 'Die Berechtigung ging unmittelbar vor dem Schreiben verloren.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		$mode = file_exists( $target ) ? ( fileperms( $target ) & 0777 ) : 0644;
		$tmp  = tempnam( $real_parent, '.wpagent-theme-' );
		if ( false === $tmp ) {
			return new WP_Error( 'wpagent_write_failed', __( 'Temporäre Datei konnte nicht angelegt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
		}
		$handle = fopen( $tmp, 'wb' );
		if ( false === $handle ) {
			wp_delete_file( $tmp );
			return new WP_Error( 'wpagent_write_failed', __( 'Temporäre Datei konnte nicht geöffnet werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
		}
		$written = 0;
		$length  = strlen( $contents );
		while ( $written < $length ) {
			$count = fwrite( $handle, substr( $contents, $written, 1048576 ) );
			if ( false === $count || 0 === $count ) {
				fclose( $handle );
				wp_delete_file( $tmp );
				return new WP_Error( 'wpagent_write_failed', __( 'Datei konnte nicht vollständig geschrieben werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
			}
			$written += $count;
		}
		$flushed = fflush( $handle );
		$synced  = ! function_exists( 'fsync' ) || fsync( $handle );
		fclose( $handle );
		if ( ! current_user_can( 'edit_themes' ) ) {
			wp_delete_file( $tmp );
			return new WP_Error( 'wpagent_forbidden', __( 'Die Berechtigung ging vor dem Ersetzen der Datei verloren.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		if ( ! wpagent_core_mutator_refresh_lock( 'theme', 'active', $lock ) ) {
			wp_delete_file( $tmp );
			return new WP_Error( 'wpagent_theme_busy', __( 'Die Theme-Sperre ging vor dem Ersetzen der Datei verloren.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		if ( ! $flushed || ! $synced || ! chmod( $tmp, $mode ) || ! rename( $tmp, $target ) ) {
			if ( file_exists( $tmp ) ) {
				wp_delete_file( $tmp );
			}
			return wpagent_core_mutator_recovery_required( 'wpagent_theme_file_recovery_required', __( 'Das Theme-Dateischreiben endete nach dem Start der Seiteneffekte nicht eindeutig. Prüfe die Datei manuell.', 'wp-agent-companion' ), '' );
		}

		$read_back = file_get_contents( $target );
		$sha256    = hash( 'sha256', $contents );
		$verified  = false !== $read_back && hash_equals( $sha256, hash( 'sha256', $read_back ) );
		if ( ! $verified || ! hash_equals( $expected_theme, (string) get_stylesheet() ) || ! wpagent_core_mutator_lock_is_owned( 'theme', 'active', $lock ) ) {
			return wpagent_core_mutator_recovery_required( 'wpagent_theme_file_recovery_required', __( 'Die Theme-Datei konnte nicht eindeutig bestätigt werden. Prüfe die Datei und das aktive Theme manuell.', 'wp-agent-companion' ), $verified ? $sha256 : '' );
		}

		return array(
			'path'     => 'wp-content/themes/' . wp_basename( $real_theme ) . '/' . $rel_norm,
			'rel_path' => $rel_norm,
			'bytes'    => $written,
			'theme'    => wp_basename( $real_theme ),
			'sha256'   => $sha256,
			'verified' => true,
		);
	} finally {
		wpagent_core_mutator_release_lock( 'theme', 'active', $lock );
	}
}
