<?php
/**
 * File tools — read/write inside theme + plugin folders, with snapshots.
 *
 * Sandbox is wp-content/themes and wp-content/plugins (both plus child themes).
 * Path traversal blocked. Extension allowlist enforced for writes.
 * PHP files syntax-checked via tokenizer before save.
 */

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

class PCD_PC_Tools_Files {

	const ALLOWED_EXT = [ 'php', 'css', 'js', 'json', 'html', 'txt', 'svg', 'md' ];

	public static function register(): void {

		PCD_PC_Tools::register(
			'wp_file_read',
			'Read a file inside wp-content/themes or wp-content/plugins.',
			[
				'type' => 'object',
				'required' => [ 'path' ],
				'properties' => [ 'path' => [ 'type' => 'string', 'description' => 'Path relative to wp-content (e.g. themes/bricks/header.php)' ] ],
			],
			[ __CLASS__, 'read' ],
			[ 'cap' => 'edit_themes', 'category' => 'files' ]
		);

		PCD_PC_Tools::register(
			'wp_file_write',
			'Write or overwrite a file inside wp-content/themes or wp-content/plugins. PHP files are syntax-checked before save. Snapshots prior content for undo.',
			[
				'type' => 'object',
				'required' => [ 'path', 'content' ],
				'properties' => [
					'path'    => [ 'type' => 'string' ],
					'content' => [ 'type' => 'string' ],
				],
			],
			[ __CLASS__, 'write' ],
			[ 'cap' => 'edit_themes', 'category' => 'files' ]
		);

		PCD_PC_Tools::register(
			'wp_file_edit',
			'Find-and-replace inside an existing file. old_content must match exactly once. Snapshots prior content.',
			[
				'type' => 'object',
				'required' => [ 'path', 'old_content', 'new_content' ],
				'properties' => [
					'path'        => [ 'type' => 'string' ],
					'old_content' => [ 'type' => 'string' ],
					'new_content' => [ 'type' => 'string' ],
				],
			],
			[ __CLASS__, 'edit' ],
			[ 'cap' => 'edit_themes', 'category' => 'files' ]
		);

		PCD_PC_Tools::register(
			'wp_file_list',
			'List files under a wp-content subpath. Optional glob pattern.',
			[
				'type' => 'object',
				'required' => [ 'path' ],
				'properties' => [
					'path'    => [ 'type' => 'string' ],
					'pattern' => [ 'type' => 'string' ],
				],
			],
			[ __CLASS__, 'listing' ],
			[ 'cap' => 'edit_themes', 'category' => 'files' ]
		);

		PCD_PC_Tools::register(
			'wp_file_outline',
			'Structural outline of a code file — classes, functions, WordPress hooks, template tags, HTML landmarks — with line numbers. Lets the AI navigate big files without reading them whole.',
			[
				'type' => 'object',
				'required' => [ 'path' ],
				'properties' => [
					'path' => [ 'type' => 'string' ],
				],
			],
			[ __CLASS__, 'outline' ],
			[ 'cap' => 'edit_themes', 'category' => 'files' ]
		);
	}

	private static function resolve( string $path ) {
		if ( '' === $path ) {
			return new WP_Error( 'bad_path', 'Path required.' );
		}
		if ( strpos( $path, '..' ) !== false ) {
			return new WP_Error( 'traversal', 'Path traversal not allowed.' );
		}
		// Sandbox: themes/* or plugins/* relative to wp-content.
		$path = ltrim( $path, '/' );
		if ( ! preg_match( '#^(themes|plugins|mu-plugins)/#', $path ) ) {
			return new WP_Error( 'out_of_sandbox', 'Path must be under themes/, plugins/, or mu-plugins/.' );
		}

		$full = WP_CONTENT_DIR . '/' . $path;
		$dir  = realpath( dirname( $full ) );
		if ( ! $dir || strpos( $dir, realpath( WP_CONTENT_DIR ) ) !== 0 ) {
			return new WP_Error( 'out_of_sandbox', 'Resolved path escapes wp-content.' );
		}
		return $full;
	}

	private static function validate_php( string $content, string $label ) {
		try {
			@token_get_all( $content, TOKEN_PARSE ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
		} catch ( \Error $e ) {
			return new WP_Error( 'php_syntax', "Syntax error in {$label}: " . $e->getMessage() );
		}
		return true;
	}

	public static function read( array $args ) {
		$full = self::resolve( $args['path'] );
		if ( is_wp_error( $full ) ) {
			return $full;
		}
		if ( ! file_exists( $full ) ) {
			return new WP_Error( 'not_found', "File not found: {$args['path']}" );
		}
		$content = file_get_contents( $full ); // phpcs:ignore WordPress.WP.AlternativeFunctions
		return [ 'path' => $args['path'], 'content' => $content, 'size' => strlen( $content ) ];
	}

	public static function write( array $args ) {
		$full = self::resolve( $args['path'] );
		if ( is_wp_error( $full ) ) {
			return $full;
		}
		$ext = strtolower( pathinfo( $full, PATHINFO_EXTENSION ) );
		if ( ! in_array( $ext, self::ALLOWED_EXT, true ) ) {
			return new WP_Error( 'forbidden_ext', "Extension not allowed: {$ext}" );
		}
		if ( 'php' === $ext ) {
			$ok = self::validate_php( $args['content'], basename( $full ) );
			if ( is_wp_error( $ok ) ) {
				return $ok;
			}
		}

		$prior_content = file_exists( $full ) ? file_get_contents( $full ) : null; // phpcs:ignore WordPress.WP.AlternativeFunctions
		$snapshot_id = null;
		if ( null !== $prior_content ) {
			$snapshot_id = PCD_PC_Snapshot::capture( 'file', $full, $prior_content, 'File before write: ' . basename( $full ) );
		}

		if ( ! is_dir( dirname( $full ) ) ) {
			wp_mkdir_p( dirname( $full ) );
		}

		$ok = file_put_contents( $full, $args['content'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions
		if ( false === $ok ) {
			return new WP_Error( 'write_failed', 'Could not write file.' );
		}

		return [
			'path'        => $args['path'],
			'bytes'       => $ok,
			'snapshot_id' => $snapshot_id,
			'created'     => null === $prior_content,
		];
	}

	public static function edit( array $args ) {
		$full = self::resolve( $args['path'] );
		if ( is_wp_error( $full ) ) {
			return $full;
		}
		if ( ! file_exists( $full ) ) {
			return new WP_Error( 'not_found', "File not found: {$args['path']}" );
		}
		$content = file_get_contents( $full ); // phpcs:ignore WordPress.WP.AlternativeFunctions
		$count   = substr_count( $content, $args['old_content'] );
		if ( 0 === $count ) {
			return new WP_Error( 'no_match', 'old_content not found in file.' );
		}
		if ( $count > 1 ) {
			return new WP_Error( 'multiple_matches', "old_content matches {$count} times. Provide more context to make it unique." );
		}
		$updated = str_replace( $args['old_content'], $args['new_content'], $content );

		if ( 'php' === strtolower( pathinfo( $full, PATHINFO_EXTENSION ) ) ) {
			$ok = self::validate_php( $updated, basename( $full ) );
			if ( is_wp_error( $ok ) ) {
				return $ok;
			}
		}

		$snapshot_id = PCD_PC_Snapshot::capture( 'file', $full, $content, 'File before edit: ' . basename( $full ) );
		$ok = file_put_contents( $full, $updated ); // phpcs:ignore WordPress.WP.AlternativeFunctions
		if ( false === $ok ) {
			return new WP_Error( 'write_failed', 'Could not write file.' );
		}
		return [ 'path' => $args['path'], 'snapshot_id' => $snapshot_id, 'bytes' => $ok ];
	}

	public static function listing( array $args ) {
		$full = self::resolve( rtrim( $args['path'], '/' ) . '/.' );
		if ( is_wp_error( $full ) ) {
			return $full;
		}
		$dir = WP_CONTENT_DIR . '/' . ltrim( $args['path'], '/' );
		if ( ! is_dir( $dir ) ) {
			return new WP_Error( 'not_found', "Directory not found: {$args['path']}" );
		}

		$pattern = $args['pattern'] ?? null;
		$out     = [];
		$it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ) );
		$base_len = strlen( $dir ) + 1;
		foreach ( $it as $f ) {
			if ( $f->isDir() ) {
				continue;
			}
			$rel = substr( $f->getPathname(), $base_len );
			if ( $pattern && ! fnmatch( $pattern, $rel, FNM_PATHNAME | FNM_CASEFOLD ) ) {
				continue;
			}
			$out[] = [ 'path' => $rel, 'size' => $f->getSize() ];
			if ( count( $out ) >= 1000 ) {
				break;
			}
		}
		return [ 'base' => $args['path'], 'count' => count( $out ), 'files' => $out ];
	}

	public static function outline( array $args ) {
		$full = self::resolve( $args['path'] );
		if ( is_wp_error( $full ) ) {
			return $full;
		}
		if ( ! file_exists( $full ) ) {
			return new WP_Error( 'not_found', "File not found: {$args['path']}" );
		}

		$content = file_get_contents( $full ); // phpcs:ignore WordPress.WP.AlternativeFunctions
		$lines   = explode( "\n", $content );
		$ext     = strtolower( pathinfo( $full, PATHINFO_EXTENSION ) );
		$out     = [];

		foreach ( $lines as $i => $line ) {
			$ln = $i + 1;

			if ( 'php' === $ext ) {
				// Class definitions.
				if ( preg_match( '/^\s*(?:abstract\s+|final\s+)?class\s+(\w+)/', $line, $m ) ) {
					$out[] = [ 'line' => $ln, 'type' => 'class', 'name' => $m[1] ];
				}
				// Function / method definitions.
				if ( preg_match( '/^\s*(?:public\s+|private\s+|protected\s+|static\s+)*function\s+(\w+)\s*\(/', $line, $m ) ) {
					$out[] = [ 'line' => $ln, 'type' => 'function', 'name' => $m[1] ];
				}
				// WordPress hooks.
				if ( preg_match( '/\b(add_action|add_filter|do_action|apply_filters)\s*\(\s*[\'"]([^\'"]+)[\'"]/', $line, $m ) ) {
					$out[] = [ 'line' => $ln, 'type' => 'hook', 'name' => $m[2], 'detail' => $m[1] ];
				}
				// Template parts.
				if ( preg_match( '/\bget_template_part\s*\(\s*[\'"]([^\'"]+)[\'"]/', $line, $m ) ) {
					$out[] = [ 'line' => $ln, 'type' => 'template_part', 'name' => $m[1] ];
				}
				// Template tags.
				if ( preg_match( '/\b(get_header|get_footer|get_sidebar|the_content|the_title|wp_head|wp_footer|the_loop|have_posts)\s*\(/', $line, $m ) ) {
					$out[] = [ 'line' => $ln, 'type' => 'template_tag', 'name' => $m[1] . '()' ];
				}
				// HTML landmarks inside PHP templates.
				if ( preg_match( '/<(header|footer|main|nav|aside|section|article)[\s>]/i', $line, $m ) ) {
					$detail = '';
					if ( preg_match( '/(?:id|class)\s*=\s*["\']([^"\']+)["\']/', $line, $a ) ) {
						$detail = $a[1];
					}
					$out[] = [ 'line' => $ln, 'type' => 'landmark', 'name' => '<' . strtolower( $m[1] ) . '>', 'detail' => $detail ];
				}
			}

			if ( 'css' === $ext ) {
				if ( preg_match( '/^\/\*\s*-{2,}/', $line ) || preg_match( '/^\/\*\s*={2,}/', $line ) ) {
					$out[] = [ 'line' => $ln, 'type' => 'comment_section' ];
				}
				if ( preg_match( '/@media\s*(.+?)\s*\{/', $line, $m ) ) {
					$out[] = [ 'line' => $ln, 'type' => 'media_query', 'name' => '@media ' . mb_substr( $m[1], 0, 80 ) ];
				}
			}

			if ( 'js' === $ext ) {
				if ( preg_match( '/^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)/', $line, $m ) ) {
					$out[] = [ 'line' => $ln, 'type' => 'function', 'name' => $m[1] ];
				}
				if ( preg_match( '/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(/', $line, $m ) ) {
					$out[] = [ 'line' => $ln, 'type' => 'arrow_function', 'name' => $m[1] ];
				}
				if ( preg_match( '/^\s*(?:export\s+)?class\s+(\w+)/', $line, $m ) ) {
					$out[] = [ 'line' => $ln, 'type' => 'class', 'name' => $m[1] ];
				}
				if ( preg_match( '/\baddEventListener\s*\(\s*[\'"](\w+)[\'"]/', $line, $m ) ) {
					$out[] = [ 'line' => $ln, 'type' => 'event_listener', 'name' => $m[1] ];
				}
			}

			if ( 'html' === $ext ) {
				if ( preg_match( '/<(header|footer|main|nav|aside|section|article)[\s>]/i', $line, $m ) ) {
					$out[] = [ 'line' => $ln, 'type' => 'landmark', 'name' => '<' . strtolower( $m[1] ) . '>' ];
				}
			}
		}

		return [
			'path'        => $args['path'],
			'type'        => $ext,
			'total_lines' => count( $lines ),
			'outline'     => $out,
		];
	}
}
