<?php
/**
 * Staged file transaction for risky code writes.
 *
 * Keeps the public file tools fast while moving high-risk PHP/runtime writes
 * through a copy, verification, deploy, post-check, and rollback path.
 */

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

class Niwa_AI_Admin_Autonomy_Staged_File_Transaction {
    private $workspace_root;

    public function __construct( $workspace_root = '' ) {
        $workspace_root = trim( (string) $workspace_root );
        if ( '' === $workspace_root && class_exists( 'Niwa_AI_Admin_Autonomy_Workspace' ) ) {
            $workspace_root = Niwa_AI_Admin_Autonomy_Workspace::get_private_workspace_root();
        }

        $this->workspace_root = '' !== $workspace_root
            ? rtrim( wp_normalize_path( $workspace_root ), '/' )
            : '';
    }

    public function commit_content( $args ) {
        $args          = is_array( $args ) ? $args : array();
        $target_path   = isset( $args['target_path'] ) ? (string) $args['target_path'] : '';
        $relative_path = isset( $args['relative_path'] ) ? wp_normalize_path( ltrim( (string) $args['relative_path'], '/' ) ) : '';
        $content       = isset( $args['content'] ) ? (string) $args['content'] : '';
        $operation     = isset( $args['operation'] ) ? sanitize_key( (string) $args['operation'] ) : 'file_write';
        $rollback      = isset( $args['rollback_entry'] ) && is_array( $args['rollback_entry'] ) ? $args['rollback_entry'] : array();
        $expected_hash = isset( $args['expected_hash'] ) ? (string) $args['expected_hash'] : '';
        $risk          = isset( $args['risk'] ) && is_array( $args['risk'] ) ? $args['risk'] : array();

        if ( '' === $target_path || '' === $relative_path ) {
            return $this->fail( 'Staged commit target path is missing.' );
        }
        if ( '' === $this->workspace_root ) {
            return $this->fail( 'Niwa workspace root is not available for staged write.' );
        }

        $job_id      = 'stage-' . gmdate( 'Ymd-His' ) . '-' . substr( md5( $relative_path . microtime( true ) ), 0, 10 );
        $staging_dir = $this->workspace_root . '/code-staging/' . $job_id;
        $staged_path = trailingslashit( $staging_dir ) . $this->safe_relative_filename( $relative_path );
        if ( ! Niwa_AI_Admin_Autonomy_Workspace::ensure_private_directory( dirname( $staged_path ) ) ) {
            return $this->fail( 'Cannot create staged write directory.' );
        }

        $manifest = array(
            'id'                 => $job_id,
            'operation'          => $operation,
            'relative_path'      => $relative_path,
            'target_path'        => $target_path,
            'staged_path'        => $staged_path,
            'created_at_utc'     => gmdate( 'Y-m-d H:i:s' ),
            'risk_level'         => isset( $risk['risk_level'] ) ? (string) $risk['risk_level'] : '',
            'safety_strategy'    => isset( $risk['strategy'] ) ? (string) $risk['strategy'] : 'staged_safety',
            'risk_reasons'       => isset( $risk['reasons'] ) && is_array( $risk['reasons'] ) ? array_values( $risk['reasons'] ) : array(),
            'expected_live_hash' => $expected_hash,
            'status'             => 'prepared',
        );

        if ( is_file( $target_path ) ) {
            if ( ! Niwa_AI_Admin_Autonomy_Workspace::copy_private_file( $target_path, $staged_path . '.original' ) ) {
                return $this->fail( 'Cannot copy original file into staged workspace.' );
            }
        }

        $written = Niwa_AI_Admin_Autonomy_Workspace::write_private_file( $staged_path, $content, LOCK_EX );
        if ( false === $written ) {
            return $this->fail( 'Cannot write staged file copy.' );
        }
        $manifest['status']        = 'edited';
        $manifest['staged_bytes']  = intval( $written );
        $manifest['staged_sha256'] = hash_file( 'sha256', $staged_path );
        $this->write_manifest( $staging_dir, $manifest );

        $verification = $this->verify_staged_file( $staged_path, $relative_path );
        $manifest['verification'] = $verification;
        if ( empty( $verification['success'] ) ) {
            $manifest['status'] = 'failed_verification';
            $this->write_manifest( $staging_dir, $manifest );
            return array(
                'success'      => false,
                'error'        => isset( $verification['error'] ) ? (string) $verification['error'] : 'Staged verification failed.',
                'staged_job_id' => $job_id,
                'staged_path'   => $staged_path,
                'verification'  => $verification,
            );
        }

        clearstatcache( true, $target_path );
        if ( '' !== $expected_hash && is_file( $target_path ) && md5_file( $target_path ) !== $expected_hash ) {
            $manifest['status'] = 'failed_live_hash_changed';
            $this->write_manifest( $staging_dir, $manifest );
            return array(
                'success'      => false,
                'error'        => 'Live file changed after preview. Repeat the operation.',
                'staged_job_id' => $job_id,
            );
        }

        $manifest['status'] = 'committing';
        $this->write_manifest( $staging_dir, $manifest );

        $deploy = $this->deploy_staged_file( $staged_path, $target_path );
        if ( empty( $deploy['success'] ) ) {
            $manifest['status'] = 'failed_commit';
            $manifest['deploy'] = $deploy;
            $this->write_manifest( $staging_dir, $manifest );
            return array(
                'success'      => false,
                'error'        => isset( $deploy['error'] ) ? (string) $deploy['error'] : 'Staged deploy failed.',
                'staged_job_id' => $job_id,
            );
        }

        $this->invalidate_runtime_file_cache( $target_path );
        $post_check = $this->post_commit_check( $target_path, $relative_path );
        $manifest['status']     = ! empty( $post_check['success'] ) ? 'healthy' : 'post_check_failed';
        $manifest['deploy']     = $deploy;
        $manifest['post_check'] = $post_check;
        $this->write_manifest( $staging_dir, $manifest );

        if ( empty( $post_check['success'] ) && ! empty( $rollback ) ) {
            $rollback_result = $this->restore_rollback_entry( $rollback, $target_path );
            $this->invalidate_runtime_file_cache( $target_path );
            $manifest['status']          = ! empty( $rollback_result['success'] ) ? 'rolled_back' : 'rollback_failed';
            $manifest['rollback_result'] = $rollback_result;
            $this->write_manifest( $staging_dir, $manifest );

            return array(
                'success'          => false,
                'error'            => 'Post-commit health check failed. Rollback was attempted.',
                'staged_job_id'     => $job_id,
                'post_check'        => $post_check,
                'rollback_result'   => $rollback_result,
                'rollback_entries'  => array( $rollback ),
                'executed_actions'  => array(
                    array(
                        'type'          => $operation,
                        'path'          => $target_path,
                        'staged_job_id' => $job_id,
                        'rolled_back'   => ! empty( $rollback_result['success'] ),
                    ),
                ),
            );
        }

        return array(
            'success'         => true,
            'staged_job_id'   => $job_id,
            'staged_path'     => $staged_path,
            'verification'    => $verification,
            'post_check'      => $post_check,
            'bytes_written'   => intval( isset( $deploy['bytes_written'] ) ? $deploy['bytes_written'] : $written ),
            'rollback_entry'  => $rollback,
            'manifest_path'   => trailingslashit( $staging_dir ) . 'manifest.json',
        );
    }

    private function verify_staged_file( $staged_path, $relative_path ) {
        $relative_path = wp_normalize_path( (string) $relative_path );
        $checks        = array();

        if ( preg_match( '/\.php$/i', $relative_path ) ) {
            $lint = $this->run_php_lint( $staged_path );
            $checks[] = $lint;
            if ( empty( $lint['success'] ) ) {
                return array(
                    'success' => false,
                    'checks'  => $checks,
                    'error'   => isset( $lint['error'] ) ? (string) $lint['error'] : 'PHP lint failed.',
                );
            }
        } elseif ( preg_match( '/\.json$/i', $relative_path ) ) {
            json_decode( (string) file_get_contents( $staged_path ), true );
            $ok = JSON_ERROR_NONE === json_last_error();
            $checks[] = array(
                'type'    => 'json_parse',
                'success' => $ok,
                'message' => $ok ? 'JSON parse passed.' : json_last_error_msg(),
            );
            if ( ! $ok ) {
                return array( 'success' => false, 'checks' => $checks, 'error' => json_last_error_msg() );
            }
        }

        return array(
            'success' => true,
            'checks'  => $checks,
        );
    }

    private function run_php_lint( $path ) {
        if ( ! function_exists( 'proc_open' ) || ! function_exists( 'proc_close' ) || ! function_exists( 'proc_get_status' ) ) {
            if ( function_exists( 'opcache_compile_file' ) && filter_var( ini_get( 'opcache.enable' ), FILTER_VALIDATE_BOOLEAN ) ) {
                $errors = array();
                $handler = function ( $errno, $errstr ) use ( &$errors ) {
                    $errors[] = trim( (string) $errstr );
                    return true;
                };
                set_error_handler( $handler );
                $ok = false;
                try {
                    $ok = @opcache_compile_file( $path );
                } catch ( \Throwable $e ) {
                    $errors[] = $e->getMessage();
                    $ok = false;
                }
                restore_error_handler();

                return array(
                    'type'    => 'php_lint',
                    'engine'  => 'opcache_compile_file',
                    'success' => ! empty( $ok ),
                    'message' => ! empty( $ok ) ? 'PHP syntax check passed via opcache_compile_file.' : '',
                    'error'   => ! empty( $ok ) ? '' : $this->truncate( implode( "\n", array_filter( $errors ) ), 1200 ),
                );
            }

            $source = @file_get_contents( $path );
            if ( false !== $source ) {
                try {
                    token_get_all( $source );
                    return array(
                        'type'    => 'php_lint',
                        'engine'  => 'php_tokenizer_fallback',
                        'success' => true,
                        'message' => 'Tokenizer fallback completed. Full php -l is unavailable because proc_open is disabled.',
                        'error'   => '',
                    );
                } catch ( \Throwable $e ) {
                    return array(
                        'type'    => 'php_lint',
                        'engine'  => 'php_tokenizer_fallback',
                        'success' => false,
                        'error'   => $this->truncate( $e->getMessage(), 1200 ),
                    );
                }
            }

            return array(
                'type'    => 'php_lint',
                'success' => false,
                'error'   => 'PHP lint is not available because proc_open/proc_close/proc_get_status is disabled.',
            );
        }

        $php = defined( 'PHP_BINARY' ) && '' !== PHP_BINARY ? PHP_BINARY : 'php';
        $descriptors = array(
            0 => array( 'pipe', 'r' ),
            1 => array( 'pipe', 'w' ),
            2 => array( 'pipe', 'w' ),
        );
        $process = @proc_open( array( $php, '-l', $path ), $descriptors, $pipes, dirname( $path ), null, array( 'bypass_shell' => true ) );
        if ( ! is_resource( $process ) ) {
            return array(
                'type'    => 'php_lint',
                'success' => false,
                'error'   => 'PHP lint process could not be started.',
            );
        }

        fclose( $pipes[0] );
        $stdout = stream_get_contents( $pipes[1] );
        $stderr = stream_get_contents( $pipes[2] );
        fclose( $pipes[1] );
        fclose( $pipes[2] );
        $exit_code = proc_close( $process );
        $output    = trim( (string) $stdout . "\n" . (string) $stderr );

        return array(
            'type'      => 'php_lint',
            'success'   => 0 === intval( $exit_code ),
            'exit_code' => intval( $exit_code ),
            'message'   => $this->truncate( $output, 1200 ),
            'error'     => 0 === intval( $exit_code ) ? '' : $this->truncate( $output, 1200 ),
        );
    }

    private function deploy_staged_file( $staged_path, $target_path ) {
        $parent = dirname( $target_path );
        if ( ! is_dir( $parent ) && ! wp_mkdir_p( $parent ) ) {
            return $this->fail( 'Cannot create deploy target directory.' );
        }

        $tmp_path = $target_path . '.niwa_stage_' . substr( md5( microtime( true ) ), 0, 8 );
        if ( ! @copy( $staged_path, $tmp_path ) ) {
            return $this->fail( 'Cannot copy staged file into deploy temp file.' );
        }

        if ( preg_match( '/\.php$/i', $target_path ) ) {
            $lint = $this->run_php_lint( $tmp_path );
            if ( empty( $lint['success'] ) ) {
                @unlink( $tmp_path );
                return array(
                    'success' => false,
                    'error'   => isset( $lint['error'] ) ? (string) $lint['error'] : 'Deploy temp PHP lint failed.',
                    'lint'    => $lint,
                );
            }
        }

        if ( ! @rename( $tmp_path, $target_path ) ) {
            @unlink( $tmp_path );
            return $this->fail( 'Deploy rename failed.' );
        }

        @chmod( $target_path, 0644 );
        return array(
            'success'       => true,
            'bytes_written' => intval( @filesize( $target_path ) ),
        );
    }

    private function post_commit_check( $target_path, $relative_path ) {
        $checks = array();
        if ( preg_match( '/\.php$/i', (string) $relative_path ) ) {
            $lint = $this->run_php_lint( $target_path );
            $checks[] = $lint;
            if ( empty( $lint['success'] ) ) {
                return array(
                    'success' => false,
                    'checks'  => $checks,
                    'error'   => isset( $lint['error'] ) ? (string) $lint['error'] : 'Live PHP lint failed.',
                );
            }
        }

        $loopback = $this->run_loopback_health_check();
        if ( ! empty( $loopback ) ) {
            $checks[] = $loopback;
            if ( empty( $loopback['success'] ) ) {
                return array(
                    'success' => false,
                    'checks'  => $checks,
                    'error'   => isset( $loopback['error'] ) ? (string) $loopback['error'] : 'Loopback health check failed.',
                );
            }
        }

        return array(
            'success' => true,
            'checks'  => $checks,
        );
    }

    private function run_loopback_health_check() {
        if ( ! function_exists( 'wp_remote_get' ) || ! function_exists( 'home_url' ) ) {
            return array();
        }

		$response = Niwa_AI_Safe_HTTP::get(
			home_url( '/' ),
			array(
				'timeout'     => 6,
				'redirection' => 2,
				'headers'     => array(
					'Cache-Control' => 'no-cache',
					'X-Niwa-Health' => 'staged-write',
				),
			),
			128 * 1024
		);

        if ( is_wp_error( $response ) ) {
            return array(
                'type'    => 'loopback_home',
                'success' => false,
                'error'   => $response->get_error_message(),
            );
        }

        $code = function_exists( 'wp_remote_retrieve_response_code' ) ? intval( wp_remote_retrieve_response_code( $response ) ) : 0;
        return array(
            'type'        => 'loopback_home',
            'success'     => $code > 0 && $code < 500,
            'status_code' => $code,
            'error'       => $code >= 500 ? 'Home loopback returned HTTP ' . $code : '',
        );
    }

    private function restore_rollback_entry( $entry, $target_path ) {
        $entry = is_array( $entry ) ? $entry : array();
        if ( ! empty( $entry['type'] ) && 'file_delete' === (string) $entry['type'] ) {
            return array( 'success' => ! is_file( $target_path ) || @unlink( $target_path ) );
        }
        if ( ! empty( $entry['snapshot_path'] ) && is_file( (string) $entry['snapshot_path'] ) ) {
            return array( 'success' => @copy( (string) $entry['snapshot_path'], $target_path ) );
        }
        if ( array_key_exists( 'previous_content', $entry ) ) {
            return array( 'success' => false !== @file_put_contents( $target_path, (string) $entry['previous_content'], LOCK_EX ) );
        }

        return array( 'success' => false, 'error' => 'No restorable rollback payload was available.' );
    }

    private function invalidate_runtime_file_cache( $path ) {
        clearstatcache( true, $path );
        if ( function_exists( 'opcache_invalidate' ) ) {
            @opcache_invalidate( $path, true );
        }
        if ( function_exists( 'wp_cache_flush' ) ) {
            @wp_cache_flush();
        }
    }

    private function write_manifest( $staging_dir, $manifest ) {
        Niwa_AI_Admin_Autonomy_Workspace::write_private_file(
            trailingslashit( $staging_dir ) . 'manifest.json',
            wp_json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES )
        );
    }

    private function safe_relative_filename( $relative_path ) {
        $relative_path = wp_normalize_path( ltrim( (string) $relative_path, '/' ) );
        $parts = array();
        foreach ( explode( '/', $relative_path ) as $part ) {
            $part = preg_replace( '/[^A-Za-z0-9._-]/', '_', $part );
            if ( '' !== $part && '.' !== $part && '..' !== $part ) {
                $parts[] = $part;
            }
        }

        return ! empty( $parts ) ? implode( '/', $parts ) : 'staged-file';
    }

    private function truncate( $text, $limit ) {
        $text  = trim( (string) $text );
        $limit = max( 50, intval( $limit ) );
        return strlen( $text ) > $limit ? substr( $text, 0, $limit - 3 ) . '...' : $text;
    }

    private function fail( $message ) {
        return array(
            'success' => false,
            'error'   => (string) $message,
        );
    }
}
