<?php
/**
 * Write File Tool.
 *
 * Write content to a file within the uploads or system temp directories.
 * Enforces MIME validation for uploads destinations (extension gate before
 * the write, content-based sniff after). Extends Abstract_File_Writer_Tool
 * for memory-efficient multi-mode writes with automatic backup.
 *
 * @package   Agenistrator
 * @since     1.0.0
 * @author    Alex Ruban <alex@maximum.guru>
 * @author    Maximum Software Inc <contact@maximum.software>
 * @license   GPL-3.0-only
 * @link      https://agenistrator.com/
 *
 * SPDX-FileCopyrightText: 2025-2026 Alex Ruban <alex@maximum.guru>
 * SPDX-FileCopyrightText: 2025-2026 Maximum Software Inc <contact@maximum.software>
 * SPDX-License-Identifier: GPL-3.0-only
 */

namespace Agenistrator;

// Direct access prevention
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Write_File_Tool class.
 */
class Write_File_Tool extends Abstract_File_Writer_Tool {

	/**
	 * Get the tool ID.
	 *
	 * @return string The tool ID.
	 * @since     1.0.0
	 */
	public function get_id() {
		return 'write_file';
	}

	/**
	 * Get the tool name.
	 *
	 * @return string The tool name.
	 * @since     1.0.0
	 */
	public function get_name() {
		return __( 'Write File', 'agenistrator' );
	}

	/**
	 * Get the tool description.
	 *
	 * @return string The tool description.
	 * @since     1.0.0
	 */
	public function get_description() {
		return 'Write content to a file within the WordPress uploads directory or the system temporary directory.';
	}

	/**
	 * Get usage instructions for the LLM.
	 *
	 * @return string Instructions for using this tool.
	 * @since     1.0.0
	 */
	public function get_instructions() {
		return parent::get_instructions()
			. ' Writes to the uploads directory enforce MIME validation: extensions must be on WordPress\'s allowed list and the written content must match a real MIME type. Temporary files you create under the system temp directory should be removed (e.g. with delete_file) when they are no longer needed.';
	}

	/**
	 * Get the parameters schema for this tool.
	 *
	 * Note: the file parameter description embeds the current value of
	 * WordPress_Utils::get_allowed_save_paths(). Its return value is typically
	 * stable, but a poorly behaved plugin filtering wp_upload_dir() dynamically
	 * could cause it to change between requests, invalidating LLM provider
	 * prompt caches and increasing API costs.
	 *
	 * @return array Parameters schema in JSON Schema format.
	 * @since     1.0.0
	 */
	public function get_parameters() {
		$paths_desc                                  = WordPress_Utils::format_paths_description( WordPress_Utils::get_allowed_save_paths() );
		$params                                      = parent::get_parameters();
		$params['properties']['file']['description'] = 'File path (relative to WordPress root or absolute). Must be within one of: ' . $paths_desc . '. Use ' . DIRECTORY_SEPARATOR . ' as directory separator.';
		$params['properties']['create_dirs']         = array(
			'type'        => 'boolean',
			'description' => 'Create parent directories if they do not exist. Default: false.',
		);
		return $params;
	}

	/**
	 * Execute the tool with the given arguments.
	 *
	 * @param array $args The tool arguments.
	 *
	 * @return array The tool execution result.
	 * @since     1.0.0
	 */
	protected function execute( $args ) {
		// Check if file modifications are disabled
		if ( ! wp_is_file_mod_allowed( 'file_edit' ) ) {
			return $this->error_response( 'File modifications are disabled' );
		}

		$path        = $args['file'];
		$create_dirs = ! empty( $args['create_dirs'] );

		// Convert relative paths to absolute
		if ( strncmp( $path, DIRECTORY_SEPARATOR, strlen( DIRECTORY_SEPARATOR ) ) !== 0 ) {
			$path = ABSPATH . $path;
		}

		// Restrict writes to uploads and temp directories per plugin guidelines
		$real_path = WordPress_Utils::is_allowed_save_path( $path );
		if ( false === $real_path ) {
			return $this->error_response(
				'Invalid path: must be within one of: ' . WordPress_Utils::format_paths_description( WordPress_Utils::get_allowed_save_paths() )
			);
		}

		// Pre-write extension gate for uploads-targeted writes
		if ( WordPress_Utils::is_in_uploads( $real_path ) ) {
			$ext_error = $this->validate_extension_for_uploads( $real_path );
			if ( null !== $ext_error ) {
				return $this->error_response( $ext_error );
			}
		}

		// Create parent directories if needed
		$dir = dirname( $real_path );
		if ( ! is_dir( $dir ) ) {
			if ( ! $create_dirs ) {
				return $this->error_response( "Directory does not exist: {$dir}. Set create_dirs=true to create it." );
			}
			if ( ! wp_mkdir_p( $dir ) ) {
				return $this->error_response( "Failed to create directory: {$dir}" );
			}
		}

		try {
			$data = $this->execute_mode( $real_path, $args );
		} catch ( \Throwable $e ) {
			return $this->error_response( $e->getMessage() );
		}

		return $this->success_response( 'File updated successfully', $data );
	}

	/**
	 * Sniff content MIME for uploads destinations.
	 *
	 * Mirrors core's _wp_handle_upload() behaviour: checks that the file's
	 * actual contents correspond to an allowed MIME and that the filename's
	 * extension matches. Rejects the write (via exception) when either fails.
	 *
	 * @param string $file_path Absolute path to the written file.
	 * @param string $mode      Mode used for the write.
	 * @throws \InvalidArgumentException If the content MIME does not validate.
	 * @since     1.0.1
	 */
	protected function validate_write( $file_path, $mode ) {
		unset( $mode );
		if ( ! WordPress_Utils::is_in_uploads( $file_path ) ) {
			return;
		}
		if ( current_user_can( 'unfiltered_upload' ) ) {
			return;
		}

		$filetype = wp_check_filetype_and_ext( $file_path, basename( $file_path ) );
		if ( empty( $filetype['ext'] ) || empty( $filetype['type'] ) ) {
			$message = String_Utils::replace_tags(
				'File content does not match an allowed MIME type for the uploads directory (detected MIME: {mime})',
				array( 'mime' => empty( $filetype['type'] ) ? 'unknown' : $filetype['type'] )
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \InvalidArgumentException( $message );
		}
		if ( ! empty( $filetype['proper_filename'] ) ) {
			$message = String_Utils::replace_tags(
				'Filename extension does not match file content; expected name: {expected}',
				array( 'expected' => $filetype['proper_filename'] )
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \InvalidArgumentException( $message );
		}
	}

	/**
	 * Validate the file extension is on WordPress's allowed uploads MIME list.
	 *
	 * Called before any write is attempted so an invalid extension fails
	 * fast without creating a backup or touching the destination. Uses
	 * get_allowed_mime_types() so that the upload_mimes filter and the
	 * unfiltered_html capability are honoured the same way core's upload
	 * handler honours them.
	 *
	 * @param string $file_path Absolute resolved path.
	 * @return string|null Error message, or null when the extension is allowed.
	 * @since     1.0.1
	 */
	private function validate_extension_for_uploads( $file_path ) {
		if ( current_user_can( 'unfiltered_upload' ) ) {
			return null;
		}
		$ext = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) );
		if ( '' === $ext ) {
			return 'Files written to the uploads directory must have a recognised file extension.';
		}
		$allowed = get_allowed_mime_types();
		foreach ( array_keys( $allowed ) as $pattern ) {
			foreach ( explode( '|', $pattern ) as $allowed_ext ) {
				if ( $ext === $allowed_ext ) {
					return null;
				}
			}
		}
		return 'File extension ".' . $ext . '" is not allowed in the uploads directory.';
	}
}
