<?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_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',
					'description' => __( 'Relativer Pfad innerhalb des Theme-Ordners, z. B. "patterns/hero.html" oder "assets/landing.css".', 'wp-agent-companion' ),
				),
				'contents' => array(
					'type'        => 'string',
					'description' => __( 'Dateiinhalt.', 'wp-agent-companion' ),
				),
			),
			'required'   => array( 'rel_path', 'contents' ),
		),
		'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' ),
			),
		),
		'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'  => true,
			),
		),
	);
}

/**
 * @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'] ?? '' );

	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 ) );
	}

	// Aktives (Child-)Theme-Verzeichnis; get_stylesheet_directory liefert bei
	// aktivem Child-Theme dessen Ordner, sonst den des aktiven Themes.
	$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 ) );
	}

	// 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 )
		);
	}

	$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 ) );
	}

	// 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 ) && ! 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.
	$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 ( ! $flushed || ! $synced || ! chmod( $tmp, $mode ) || ! rename( $tmp, $target ) ) {
		if ( file_exists( $tmp ) ) {
			wp_delete_file( $tmp );
		}
		return new WP_Error( 'wpagent_write_failed', __( 'Datei konnte nicht atomar ersetzt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}

	$read_back = file_get_contents( $target );
	$sha256    = hash( 'sha256', $contents );
	$verified  = false !== $read_back && hash_equals( $sha256, hash( 'sha256', $read_back ) );
	if ( ! $verified ) {
		return new WP_Error( 'wpagent_write_unverified', __( 'Die geschriebene Datei konnte nicht bestätigt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}

	$real_target = realpath( $target );
	if ( false === $real_target ) {
		$real_target = $real_parent . '/' . wp_basename( $target );
	}

	return array(
		'path'     => $real_target,
		'rel_path' => $rel_norm,
		'bytes'    => $written,
		'theme'    => wp_basename( $real_theme ),
		'sha256'   => $sha256,
		'verified' => true,
	);
}
