<?php
/**
 * Niwa AI File Guard — centralised path validation, denylist, and size limits
 * for all file-related agent tools (read, write, create, edit, patch).
 *
 * Every file operation in the Admin Autonomy subsystem MUST pass through this
 * guard before touching the filesystem.
 */

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

class Niwa_AI_Admin_Autonomy_File_Guard {

    /* ── size limits ────────────────────────────────────────────────── */
    const MAX_READ_BYTES           = 2 * 1024 * 1024;   // 2 MB
    const MAX_WRITE_BYTES          = 8 * 1024 * 1024;   // 8 MB
    const MAX_ROLLBACK_INLINE_BYTES = 2 * 1024 * 1024;  // 2 MB — bigger → disk snapshot

    /* ── deny patterns (never touch these) ──────────────────────── */
    private static $denylist_exact = array(
        'wp-config.php',
        'wp-config-sample.php',
        'wp-settings.php',
        '.htaccess',
        '.htpasswd',
        'web.config',
        'php.ini',
        '.user.ini',
        '.env',
        'wp-cli.yml',
        'wp-cli.local.yml',
    );

    private static $denylist_patterns = array(
        '/\.sql$/i',
        '/\.sql\.gz$/i',
        '/\.pem$/i',
        '/\.key$/i',
        '/\.crt$/i',
        '/\.cer$/i',
        '/\.pfx$/i',
        '/\.p12$/i',
        '/\.jks$/i',
        '/\.(bak|backup|old|orig|swp|swo)$/i',
    );

    /* ── deny directories (never enter these for writes) ──────── */
    private static $denylist_dirs_for_write = array(
        'wp-admin',
        'wp-includes',
    );

    /* ── allowed roots for reads ─────────────────────────────────── */
    private static $read_allowed_relative = array(
        'wp-content/themes/',
        'wp-content/plugins/',
        'wp-content/mu-plugins/',
        'niwa-ai-workspace/',
        'wp-admin/',
        'wp-includes/',
    );

    /* ── allowed roots for writes ────────────────────────────────── */
    private static $write_allowed_relative = array(
        'wp-content/themes/',
        'wp-content/plugins/',
        'wp-content/mu-plugins/',
        'niwa-ai-workspace/',
    );

    /* ================================================================
     * PUBLIC API
     * ============================================================= */

    /**
     * Validate a path for READ operations.
     *
     * @param string $input_path  Relative to ABSPATH or absolute inside it.
     * @return array { success: bool, path: string, relative_path: string, error?: string }
     */
    public function validate_read( $input_path ) {
        return $this->validate_path( $input_path, 'read' );
    }

    /**
     * Validate a path for WRITE / EDIT operations on EXISTING files.
     *
     * @param string $input_path
     * @return array { success: bool, path: string, relative_path: string, error?: string }
     */
    public function validate_write( $input_path, $options = array() ) {
        return $this->validate_path( $input_path, 'write', $options );
    }

    /**
     * Validate a path for CREATE operations (file does not exist yet).
     *
     * @param string $input_path
     * @return array { success: bool, path: string, relative_path: string, error?: string }
     */
    public function validate_create( $input_path, $options = array() ) {
        return $this->validate_path( $input_path, 'create', $options );
    }

    /**
     * Check if a file exceeds the given byte limit.
     *
     * @param string $abs_path
     * @param int    $limit
     * @return bool  true if within limit
     */
    public function check_size( $abs_path, $limit ) {
        if ( ! is_file( $abs_path ) ) {
            return true;
        }
        $size = @filesize( $abs_path );
        return false !== $size && $size <= $limit;
    }

    /**
     * Return a human-readable risk level for display / logging.
     *
     * @param string $relative_path
     * @return string  'low' | 'medium' | 'high' | 'critical'
     */
    public function classify_risk( $relative_path ) {
        $rel = wp_normalize_path( ltrim( (string) $relative_path, '/' ) );

        if ( $this->is_wordpress_core_code_path( $rel ) ) {
            return 'critical';
        }
        if ( preg_match( '#^wp-content/uploads/#', $rel ) ) {
            return 'low';
        }
        if ( preg_match( '#^wp-content/mu-plugins/.+\.php$#i', $rel ) ) {
            return 'critical';
        }
        if ( preg_match( '#^wp-content/plugins/niwa-ai(?:-[^/]+)?/.+\.php$#i', $rel ) || preg_match( '#^wp-content/plugins/niwa-ai(?:-[^/]+)?\.php$#i', $rel ) ) {
            return 'critical';
        }
        if ( preg_match( '#^wp-content/plugins/.+\.php$#i', $rel ) ) {
            return 'high';
        }
        if ( preg_match( '#^wp-content/themes/[^/]+/.+\.php$#i', $rel ) ) {
            return 'high';
        }
        if ( preg_match( '#^wp-content/themes/[^/]+/style\.css$#', $rel ) ) {
            return 'medium';
        }
        if ( preg_match( '#\.php$#', $rel ) ) {
            return 'high';
        }

        return 'medium';
    }

    public function classify_write_safety( $relative_path, $operation = 'file_edit_patch', $args = array() ) {
        $rel       = wp_normalize_path( ltrim( (string) $relative_path, '/' ) );
        $operation = sanitize_key( (string) $operation );
        $args      = is_array( $args ) ? $args : array();
        $risk      = $this->classify_risk( $rel );
        $reasons   = array();

        if ( $this->is_wordpress_core_code_path( $rel ) ) {
            $reasons[] = 'wordpress_core_code_path';
        }
        if ( preg_match( '#^wp-content/mu-plugins/.+\.php$#i', $rel ) ) {
            $reasons[] = 'must_use_plugin_php_loads_early';
        }
        if ( preg_match( '#^wp-content/plugins/niwa-ai(?:-[^/]+)?(?:/|\.php$)#i', $rel ) && preg_match( '/\.php$/i', $rel ) ) {
            $reasons[] = 'niwa_runtime_php';
        } elseif ( preg_match( '#^wp-content/plugins/.+\.php$#i', $rel ) ) {
            $reasons[] = 'plugin_php_runtime';
        }
        if ( preg_match( '#^wp-content/themes/[^/]+/.+\.php$#i', $rel ) ) {
            $reasons[] = 'theme_php_runtime';
        }
        if ( 'file_write' === $operation && preg_match( '/\.php$/i', $rel ) ) {
            $reasons[] = 'complete_php_rewrite';
        }
        if ( isset( $args['patches'] ) && is_array( $args['patches'] ) && count( $args['patches'] ) > 5 ) {
            $reasons[] = 'multi_patch_change';
            if ( 'medium' === $risk ) {
                $risk = 'high';
            }
        }

        $requires_staged = in_array( $risk, array( 'high', 'critical' ), true )
            || in_array( 'complete_php_rewrite', $reasons, true )
            || in_array( 'multi_patch_change', $reasons, true );

        return array(
            'risk_level'              => $risk,
            'strategy'                => $requires_staged ? 'staged_safety' : 'fast_safe',
            'requires_staged_safety'  => $requires_staged,
            'requires_double_confirm' => $this->is_wordpress_core_code_path( $rel ),
            'reasons'                 => array_values( array_unique( $reasons ) ),
        );
    }

    public function is_wordpress_core_code_path( $relative_path ) {
        $rel = wp_normalize_path( ltrim( (string) $relative_path, '/' ) );
        if ( preg_match( '#^(wp-admin|wp-includes)/.+#i', $rel ) ) {
            return true;
        }

        return in_array(
            strtolower( $rel ),
            array( 'wp-settings.php', 'wp-load.php', 'wp-blog-header.php', 'wp-cron.php', 'xmlrpc.php', 'index.php' ),
            true
        );
    }

    /**
     * Return the workspace snapshots dir for rollback file copies.
     */
    public function get_snapshots_dir() {
        $workspace = class_exists( 'Niwa_AI_Admin_Autonomy_Workspace' )
            ? Niwa_AI_Admin_Autonomy_Workspace::get_private_workspace_root()
            : '';
        if ( '' === $workspace ) {
            return '';
        }
        $dir = wp_normalize_path( trailingslashit( $workspace ) . 'snapshots/' );
        if ( ! is_dir( $dir ) ) {
            wp_mkdir_p( $dir );
            @file_put_contents( $dir . '.htaccess', "Deny from all\n" );
            @file_put_contents( $dir . 'index.php', '<?php // Silence is golden.' );
        }
        return $dir;
    }

    /**
     * Store a file snapshot for rollback. Returns the snapshot path or false.
     *
     * @param string $abs_path  The file to snapshot.
     * @return string|false
     */
    public function store_snapshot( $abs_path ) {
        if ( ! is_file( $abs_path ) ) {
            return false;
        }

        $dir  = $this->get_snapshots_dir();
        if ( '' === $dir ) {
            return false;
        }
        $hash = substr( md5( $abs_path . '|' . microtime( true ) ), 0, 12 );
        $ext  = pathinfo( $abs_path, PATHINFO_EXTENSION );
        $name = $hash . ( '' !== $ext ? '.' . $ext : '' ) . '.snapshot';
        $dest = $dir . $name;

        if ( ! @copy( $abs_path, $dest ) ) {
            return false;
        }

        return $dest;
    }

    /* ================================================================
     * INTERNAL
     * ============================================================= */

    /**
     * Core validation — resolves, boundary-checks, and denylist-checks a path.
     *
     * @param string $input_path
     * @param string $mode  'read' | 'write' | 'create'
     * @return array
     */
    private function validate_path( $input_path, $mode, $options = array() ) {
        $options = is_array( $options ) ? $options : array();
        $input_path = trim( (string) $input_path );
        if ( '' === $input_path ) {
            return $this->fail( 'Path je obavezan.' );
        }

        if ( ! defined( 'ABSPATH' ) ) {
            return $this->fail( 'ABSPATH nije definisan.' );
        }

        $root        = wp_normalize_path( untrailingslashit( ABSPATH ) );
        $root_prefix = trailingslashit( $root );
        $workspace_root = class_exists( 'Niwa_AI_Admin_Autonomy_Workspace' )
            ? wp_normalize_path( Niwa_AI_Admin_Autonomy_Workspace::get_private_workspace_root() )
            : '';
        $workspace_prefix = '' !== $workspace_root ? trailingslashit( $workspace_root ) : '';

        /* ── build candidate absolute path ─────────────────────────── */
        $normalized = wp_normalize_path( str_replace( '\\', '/', $input_path ) );
        $candidate  = $normalized;

        if ( '' !== $workspace_root && ( self::workspace_relative_prefix() === $candidate || 0 === strpos( $candidate, self::workspace_relative_prefix() . '/' ) ) ) {
            $candidate = wp_normalize_path( dirname( $workspace_root ) . '/' . $candidate );
        } elseif ( ! preg_match( '#^[A-Za-z]:/#', $candidate ) && 0 !== strpos( $candidate, '/' ) ) {
            $candidate = wp_normalize_path( $root_prefix . ltrim( $candidate, '/' ) );
        }

        /* ── for 'create' the file does not exist yet ──────────────── */
        if ( 'create' === $mode ) {
            $parent_dir           = wp_normalize_path( dirname( $candidate ) );
            $nearest_existing_dir = $parent_dir;
            while ( false === realpath( $nearest_existing_dir ) && dirname( $nearest_existing_dir ) !== $nearest_existing_dir ) {
                $nearest_existing_dir = dirname( $nearest_existing_dir );
            }

            $real_parent = realpath( $nearest_existing_dir );
            if ( false === $real_parent || ! is_dir( $real_parent ) ) {
                return $this->fail( 'Ne postoji nijedan roditeljski direktorijum za kreiranje fajla: ' . $parent_dir );
            }

            $resolved_parent = wp_normalize_path( $real_parent );
            if ( ! $this->path_is_inside_allowed_boundary( $resolved_parent, $root, $root_prefix, $workspace_root, $workspace_prefix ) ) {
                return $this->fail( 'Fajl mora biti unutar lokalne WordPress instalacije ili privatnog Niwa workspace-a.' );
            }

            $relative_tail = ltrim( str_replace( trailingslashit( $resolved_parent ), '', wp_normalize_path( $candidate ) ), '/' );
            if ( '' === $relative_tail || false !== strpos( $relative_tail, '../' ) || '..' === $relative_tail ) {
                return $this->fail( 'Putanja za kreiranje fajla nije validna.' );
            }

            $resolved_path = wp_normalize_path( trailingslashit( $resolved_parent ) . $relative_tail );

            if ( file_exists( $resolved_path ) ) {
                return $this->fail( 'Fajl vec postoji. Koristi file_edit ili replace_text_in_file za izmenu.' );
            }
        } else {
            /* ── file must exist for read/write ────────────────────── */
            $resolved = realpath( $candidate );
            if ( false === $resolved ) {
                return $this->fail( 'Ciljni fajl ne postoji.' );
            }

            $resolved_path = wp_normalize_path( $resolved );

            /* boundary check */
            if ( ! $this->path_is_inside_allowed_boundary( $resolved_path, $root, $root_prefix, $workspace_root, $workspace_prefix ) ) {
                return $this->fail( 'Fajl mora biti unutar lokalne WordPress instalacije ili privatnog Niwa workspace-a.' );
            }

            if ( ! is_file( $resolved_path ) ) {
                return $this->fail( 'Putanja nije validan fajl.' );
            }
        }

        /* ── relative path from ABSPATH ────────────────────────────── */
        if ( '' !== $workspace_prefix && ( $resolved_path === $workspace_root || 0 === strpos( $resolved_path, $workspace_prefix ) ) ) {
            $workspace_tail = ltrim( substr( $resolved_path, strlen( $workspace_root ) ), '/' );
            $relative       = self::workspace_relative_prefix() . ( '' !== $workspace_tail ? '/' . $workspace_tail : '' );
        } else {
            $relative = ltrim( str_replace( $root_prefix, '', $resolved_path ), '/' );
        }

        /* ── denylist check ─────────────────────────────────────────── */
        $basename = basename( $resolved_path );
        $allow_core_write = ! empty( $options['allow_wordpress_core_write'] ) && $this->is_wordpress_core_code_path( $relative );
        if ( 'read' !== $mode && $this->is_wordpress_core_code_path( $relative ) && ! $allow_core_write ) {
            return array(
                'success'                      => false,
                'requires_double_confirmation' => true,
                'error'                        => 'Pisanje u WordPress core fajl trazi duplu potvrdu i staged safety workflow: ' . $relative,
            );
        }
        if ( $this->is_denylisted_for_mode( strtolower( $basename ), $relative, $mode ) && ! $allow_core_write ) {
            return $this->fail( 'Ovaj fajl je zasticen za trazenu operaciju: ' . $relative );
        }

        /* ── allowed-root check ────────────────────────────────────── */
        if ( 'read' === $mode ) {
            // Reads are allowed anywhere under ABSPATH (boundary already checked)
            // but we log the risk level
        } else {
            // Writes / creates must be in allowed write roots
            $in_allowed = false;
            foreach ( self::$write_allowed_relative as $allowed ) {
                if ( 0 === strpos( $relative, $allowed ) ) {
                    $in_allowed = true;
                    break;
                }
            }

            // Also allow root-level non-PHP files
            if ( ! $in_allowed && false === strpos( $relative, '/' ) && ! preg_match( '/\.php$/i', $relative ) ) {
                $in_allowed = true;
            }

            if ( ! $in_allowed ) {
                // Check directory denylist
                foreach ( self::$denylist_dirs_for_write as $dir ) {
                    if ( 0 === strpos( $relative, $dir . '/' ) || $relative === $dir ) {
                        if ( ! $allow_core_write ) {
                            return $this->fail( 'Pisanje u ' . $dir . '/ trazi duplu potvrdu jer su to WordPress core fajlovi.' );
                        }
                        $in_allowed = true;
                        break;
                    }
                }

                // Allow wp-content generally (custom directories). Runtime PHP paths still receive
                // staged-safety handling via classify_write_safety(); this branch only prevents
                // custom wp-content directories from being falsely rejected by the root whitelist.
                if ( ! $in_allowed && 0 === strpos( $relative, 'wp-content/' ) ) {
                    $in_allowed = true;
                }

                if ( ! $in_allowed ) {
                    return $this->fail( 'Pisanje je dozvoljeno samo u wp-content/ direktorijumu (teme, plugini, uploads).' );
                }
            }
        }

        return array(
            'success'       => true,
            'path'          => $resolved_path,
            'relative_path' => $relative,
            'risk_level'    => $this->classify_risk( $relative ),
        );
    }

    private function is_denylisted_for_mode( $basename_lower, $relative, $mode ) {
        $mode     = (string) $mode;
        $relative = wp_normalize_path( ltrim( (string) $relative, '/' ) );

        if ( in_array( strtolower( (string) $basename_lower ), array_map( 'strtolower', self::$denylist_exact ), true ) ) {
            return 'read' !== $mode;
        }

        foreach ( self::$denylist_patterns as $pattern ) {
            if ( ! preg_match( $pattern, $relative ) ) {
                continue;
            }

            return 'read' !== $mode || $this->is_private_material_path( $relative );
        }

        return false;
    }

    private function is_private_material_path( $relative ) {
        return (bool) preg_match( '/\.(pem|key|crt|cer|pfx|p12|jks|sql|sql\.gz)$/i', (string) $relative );
    }

    private static function workspace_relative_prefix() {
        return class_exists( 'Niwa_AI_Admin_Autonomy_Workspace' )
            ? Niwa_AI_Admin_Autonomy_Workspace::ROOT_DIRNAME
            : 'niwa-ai-workspace';
    }

    private function path_is_inside_allowed_boundary( $path, $root, $root_prefix, $workspace_root, $workspace_prefix ) {
        if ( $path === $root || 0 === strpos( $path, $root_prefix ) ) {
            return true;
        }

        return '' !== $workspace_root && ( $path === $workspace_root || 0 === strpos( $path, $workspace_prefix ) );
    }

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