<?php
/**
 * Create Media Tool.
 *
 * Creates a new media library attachment from an existing local file via sideload.
 * The source file must be within an allowed path. Pairs with download_file
 * for remote-origin workflows: download_file -> create_media.
 *
 * @package   Agenistrator
 * @since     1.0.1
 * @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;
}

/**
 * Create_Media_Tool class.
 */
class Create_Media_Tool extends Tool {

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

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

	/**
	 * Get the tool description.
	 *
	 * @return string The tool description.
	 * @since     1.0.1
	 */
	public function get_description() {
		return 'Create a media library attachment from an existing local file. Use download_file first to fetch remote files.';
	}

	/**
	 * Get additional description instructions for the LLM.
	 *
	 * @return string Additional instructions appended to description.
	 * @since     1.0.1
	 */
	public function get_description_instructions() {
		return 'Set delete_source=true when the source file is temporary (or is otherwise no longer needed) so it is removed once the attachment is created; otherwise remove it yourself with delete_file.';
	}

	/**
	 * Get the parameters schema for this tool.
	 *
	 * Note: the path parameter description embeds the current value of
	 * WordPress_Utils::get_allowed_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.1
	 */
	public function get_parameters() {
		$paths_desc = WordPress_Utils::format_paths_description( WordPress_Utils::get_allowed_paths() );
		return array(
			'type'       => 'object',
			'properties' => array(
				'path'          => array(
					'type'        => 'string',
					'description' => 'Source file path (relative to WordPress root or absolute). Must be within one of: ' . $paths_desc . '. Use ' . DIRECTORY_SEPARATOR . ' as directory separator.',
				),
				'title'         => array(
					'type'        => 'string',
					'description' => 'Title for the media item (default: filename)',
				),
				'alt'           => array(
					'type'        => 'string',
					'description' => 'Alt text for the media (images) in plain text format',
				),
				'caption'       => array(
					'type'        => 'string',
					'description' => 'Caption for the media in plain text format',
				),
				'description'   => array(
					'type'        => 'string',
					'description' => 'Description of the media in plain text format',
				),
				'post_id'       => array(
					'type'        => 'integer',
					'description' => 'Post ID to attach the media to (default: 0, unattached)',
				),
				'delete_source' => array(
					'type'        => 'boolean',
					'description' => 'Delete the source file after the attachment is successfully created (default: false)',
				),
			),
			'required'   => array( 'path' ),
		);
	}

	/**
	 * Get the default enabled status for this tool.
	 *
	 * @return bool True if enabled by default, false otherwise.
	 * @since     1.0.1
	 */
	public function get_default_enabled_status() {
		return false;
	}

	/**
	 * Check if the current user can execute this tool.
	 *
	 * @return bool True if the current user has permission.
	 * @since     1.0.1
	 */
	public function current_user_can_execute() {
		return current_user_can( 'upload_files' );
	}

	/**
	 * Check if this tool is read-only.
	 *
	 * @return bool True if the tool only reads data.
	 * @since     1.0.1
	 */
	public function is_read_only() {
		return false;
	}

	/**
	 * Execute the tool with the given arguments.
	 *
	 * @param array $args The tool arguments.
	 *
	 * @return array The tool execution result.
	 * @since     1.0.1
	 */
	protected function execute( $args ) {
		$path          = $args['path'];
		$title         = $args['title'] ?? null;
		$post_id       = intval( $args['post_id'] ?? 0 );
		$delete_source = (bool) ( $args['delete_source'] ?? false );

		if ( empty( $path ) ) {
			return $this->error_response( 'Path is required' );
		}

		// Resolve path (support both relative and absolute paths)
		if ( strncmp( $path, DIRECTORY_SEPARATOR, strlen( DIRECTORY_SEPARATOR ) ) !== 0 ) {
			$full_path = ABSPATH . $path;
		} else {
			$full_path = $path;
		}

		$full_path = WordPress_Utils::is_allowed_path( $full_path );
		if ( false === $full_path ) {
			return $this->error_response(
				'Invalid path: must be within one of: ' . WordPress_Utils::format_paths_description( WordPress_Utils::get_allowed_paths() )
			);
		}

		if ( ! is_file( $full_path ) || ! is_readable( $full_path ) ) {
			return $this->error_response( 'File not found or not readable: ' . $path );
		}

		require_once ABSPATH . 'wp-admin/includes/image.php';
		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/media.php';

		$name_hint = wp_basename( $full_path );

		// Sniff the real MIME from the file; prefer core's corrected filename
		// when the input extension mismatches the actual content.
		$type_info = wp_check_filetype_and_ext( $full_path, $name_hint );
		if ( ! empty( $type_info['proper_filename'] ) ) {
			$name_hint = $type_info['proper_filename'];
		}

		// If the hint has no extension but a valid one was sniffed, append it so
		// core's extension-based validation in _wp_handle_upload() passes.
		if ( ! empty( $type_info['ext'] ) && '' === pathinfo( $name_hint, PATHINFO_EXTENSION ) ) {
			$name_hint .= '.' . $type_info['ext'];
		}

		// Mirror core's check in _wp_handle_upload() to return a clear error up-front
		// instead of the opaque "Sorry, you are not allowed to upload this file type."
		if ( ( empty( $type_info['ext'] ) || empty( $type_info['type'] ) ) && ! current_user_can( 'unfiltered_upload' ) ) {
			return $this->error_response(
				'File type not allowed: ' . $path
				. ' (detected MIME: ' . ( empty( $type_info['type'] ) ? 'unknown' : $type_info['type'] ) . ')'
			);
		}

		// Stage a copy in temp so media_handle_sideload() can move it without
		// touching the source; delete_source is handled explicitly after success.
		$tmp_file = wp_tempnam( $name_hint );
		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- silenced copy with proper fallback; TODO: possibly migrate to WP_Filesystem later
		if ( empty( $tmp_file ) || ! @copy( $full_path, $tmp_file ) ) {
			if ( ! empty( $tmp_file ) ) {
				// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.unlink_unlink -- best-effort cleanup; TODO: possibly migrate to WP_Filesystem later
				@unlink( $tmp_file );
			}
			return $this->error_response( 'Failed to stage file for sideload' );
		}

		$file = array(
			'tmp_name' => $tmp_file,
			'name'     => $name_hint,
		);

		// media_handle_sideload() uses $desc as the attachment post_title when set,
		// and moves the temp file into the uploads directory on success.
		$attachment_id = media_handle_sideload( $file, $post_id, $title );

		if ( is_wp_error( $attachment_id ) ) {
			// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.unlink_unlink -- best-effort cleanup; TODO: possibly migrate to WP_Filesystem later
			@unlink( $tmp_file );
			return $this->error_response( 'Failed to create media: ' . $attachment_id->get_error_message() );
		}

		// Apply optional metadata.
		$update_args      = array( 'ID' => $attachment_id );
		$has_post_updates = false;
		if ( isset( $args['caption'] ) ) {
			$update_args['post_excerpt'] = sanitize_textarea_field( $args['caption'] );
			$has_post_updates            = true;
		}
		if ( isset( $args['description'] ) ) {
			$update_args['post_content'] = sanitize_textarea_field( $args['description'] );
			$has_post_updates            = true;
		}
		if ( $has_post_updates ) {
			wp_update_post( $update_args );
		}
		if ( isset( $args['alt'] ) ) {
			// Use wp_slash() to prevent stripslashes corruption in update_post_meta.
			// See: https://developer.wordpress.org/reference/functions/update_post_meta/#workaround
			update_post_meta( $attachment_id, '_wp_attachment_image_alt', wp_slash( sanitize_text_field( $args['alt'] ) ) );
		}

		if ( $delete_source ) {
			// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.unlink_unlink -- best-effort cleanup; TODO: possibly migrate to WP_Filesystem later
			@unlink( $full_path );
		}

		$attachment = get_post( $attachment_id );

		return $this->success_response(
			'Media attachment created',
			array(
				'attachment_id' => $attachment_id,
				'url'           => wp_get_attachment_url( $attachment_id ),
				'title'         => $attachment->post_title,
				'file'          => get_attached_file( $attachment_id ),
				'mime_type'     => get_post_mime_type( $attachment_id ),
			)
		);
	}
}
