<?php

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

class Niwa_AI_Admin_Autonomy_Workspace {
    const ROOT_DIRNAME = 'niwa-ai-workspace';
    const STATE_OPTION_KEY = Niwa_AI_Constants::OPT_AUTONOMY_WORKSPACE_STATE;
    private const COMPANION_MEMORY_DIR = 'companion';
    private const COMPANION_PROFILE_FILE = 'profile.md';
    private const COMPANION_IDENTITY_FILE = 'identity.md';
    private const COMPANION_USER_FILE = 'user.md';
    private const COMPANION_RELATIONSHIP_FILE = 'relationship.md';
    private const COMPANION_DIARY_FILE = 'diary.md';
    private const COMPANION_DECISIONS_FILE = 'decisions.md';
    private const OPERATIONAL_MEMORY_DIR = 'operational';
    private const OPERATIONAL_RULES_FILE = 'rules.md';
    private const OPERATIONAL_LEARNINGS_FILE = 'learnings.md';
    private const DIARY_DIR = 'diary';
    private const DIARY_CURRENT_DIR = 'current';
    private const DIARY_ARCHIVED_DIR = 'archived';
    private const DECISION_LOG_FILE = 'decisions.md';
    private const MAIN_PROMPT_BOOTSTRAP_FILES = array(
        'COMPANION_MEMORY.md',
        'OPERATIONAL_MEMORY.md',
        'OPERATIVE_HEARTBEAT.md',
        'AGENTS.md',
        'TOOLS.md',
    );
    private const STOCK_MANAGER_PROMPT_BOOTSTRAP_FILES = array(
        'OPERATIONAL_MEMORY.md',
        'STOCK_OPERATIVE_HEARTBEAT.md',
        'AGENTS.md',
        'TOOLS.md',
    );
    private const SUBAGENT_PROMPT_BOOTSTRAP_FILES = array(
        'AGENTS.md',
        'TOOLS.md',
        'IDENTITY.md',
    );
    private const MAIN_PROMPT_BOOTSTRAP_LIMITS = array(
        'COMPANION_MEMORY.md' => 1000,
        'OPERATIONAL_MEMORY.md' => 1200,
        'OPERATIVE_HEARTBEAT.md' => 1400,
        'AGENTS.md'   => 700,
        'TOOLS.md'    => 700,
    );
    private const STOCK_MANAGER_PROMPT_BOOTSTRAP_LIMITS = array(
        'OPERATIONAL_MEMORY.md' => 1400,
        'STOCK_OPERATIVE_HEARTBEAT.md' => 1400,
        'AGENTS.md'   => 700,
        'TOOLS.md'    => 700,
    );
    private const SUBAGENT_PROMPT_BOOTSTRAP_LIMITS = array(
        'AGENTS.md'   => 900,
        'TOOLS.md'    => 900,
        'IDENTITY.md' => 700,
    );
    private const MAIN_PROMPT_BOOTSTRAP_TOTAL = 4200;
    private const STOCK_MANAGER_PROMPT_BOOTSTRAP_TOTAL = 3600;
    private const SUBAGENT_PROMPT_BOOTSTRAP_TOTAL = 2600;
    private const PRIVATE_PERMISSIONS_MARKER = '.private-permissions-v2';

    private const BOOTSTRAP_FILES = array(
        'AGENTS.md',
        'SOUL.md',
        'TOOLS.md',
        'IDENTITY.md',
        'USER.md',
        'HEARTBEAT.md',
        'OPERATIVE_HEARTBEAT.md',
        'STOCK_OPERATIVE_HEARTBEAT.md',
        'MEMORY.md',
    );

    private $workspace_ensured = false;
    private $memory_text_repair_ran = false;

    public static function get_private_data_root() {
        if ( ! defined( 'ABSPATH' ) ) {
            throw new RuntimeException( 'WordPress root is unavailable for Niwa private storage.' );
        }

        $install_root = rtrim( str_replace( '\\', '/', (string) ABSPATH ), '/' );
        if ( '' === $install_root ) {
            throw new RuntimeException( 'WordPress root is unavailable for Niwa private storage.' );
        }

        $parent      = dirname( $install_root );
        $real_parent = realpath( $parent );
        if ( false !== $real_parent ) {
            $parent       = str_replace( '\\', '/', $real_parent );
            $install_root = trailingslashit( $parent ) . basename( $install_root );
        }
        $key = substr( hash( 'sha256', strtolower( $install_root ) ), 0, 12 );

        $root = rtrim( str_replace( '\\', '/', $parent ), '/' ) . '/.niwa-ai-private-' . $key;
        if ( is_link( $root ) ) {
            throw new RuntimeException( 'Niwa private storage root cannot be a symlink.' );
        }
        return $root;
    }

    public static function get_private_workspace_root() {
        $root = self::get_private_data_root();
        return '' === $root ? '' : trailingslashit( $root ) . self::ROOT_DIRNAME;
    }

    public function ensure_workspace() {
        // Skip if already ensured this request.
        if ( $this->workspace_ensured ) {
            return $this->get_paths();
        }

        $paths = $this->get_paths();
        if ( empty( $paths['root'] ) ) {
            throw new RuntimeException( 'Niwa private workspace root is unavailable.' );
        }
        $this->migrate_legacy_public_workspace( $paths['root'] );
        $this->migrate_legacy_public_logs();
        foreach (
            array(
                dirname( $paths['root'] ),
                $paths['root'],
                $paths['memory'],
                trailingslashit( $paths['memory'] ) . self::DIARY_DIR,
                trailingslashit( $paths['memory'] ) . self::DIARY_DIR . '/' . self::DIARY_CURRENT_DIR,
                trailingslashit( $paths['memory'] ) . self::DIARY_DIR . '/' . self::DIARY_ARCHIVED_DIR,
                $paths['reports'],
                $paths['snapshots'],
                $paths['sessions'],
                $paths['skills'],
                trailingslashit( $paths['memory'] ) . self::COMPANION_MEMORY_DIR,
                trailingslashit( $paths['memory'] ) . self::OPERATIONAL_MEMORY_DIR,
            ) as $directory
        ) {
            if ( ! $this->ensure_private_directory( $directory ) ) {
                throw new RuntimeException( 'Niwa private workspace directory is unsafe or unavailable.' );
            }
        }
        $this->ensure_bootstrap_files();
        $this->ensure_layered_memory_files();
        self::ensure_private_data_permissions();

        $this->workspace_ensured = true;

        return $paths;
    }

    public function get_paths() {
        $base_dir = self::get_private_workspace_root();

        return array(
            'root'            => $base_dir,
            'root_url'        => '',
            'memory'          => trailingslashit( $base_dir ) . 'memory',
            'memory_url'      => '',
            'reports'         => trailingslashit( $base_dir ) . 'reports',
            'reports_url'     => '',
            'snapshots'       => trailingslashit( $base_dir ) . 'snapshots',
            'snapshots_url'   => '',
            'sessions'        => trailingslashit( $base_dir ) . 'sessions',
            'sessions_url'    => '',
            'skills'          => trailingslashit( $base_dir ) . 'skills',
            'skills_url'      => '',
        );
    }

    protected function get_legacy_public_workspace_root() {
        $uploads = function_exists( 'wp_upload_dir' ) ? wp_upload_dir( null, false ) : array();
        return ! empty( $uploads['basedir'] )
            ? trailingslashit( (string) $uploads['basedir'] ) . self::ROOT_DIRNAME
            : '';
    }

    private function migrate_legacy_public_workspace( $private_root ) {
        $private_root = rtrim( str_replace( '\\', '/', (string) $private_root ), '/' );
        $legacy_root  = rtrim( str_replace( '\\', '/', $this->get_legacy_public_workspace_root() ), '/' );
        if ( '' === $private_root || '' === $legacy_root || ! is_dir( $legacy_root ) || $private_root === $legacy_root ) {
            return;
        }

        $this->write_public_workspace_deny_files( $legacy_root );
        if ( ! self::secure_private_tree( $legacy_root ) ) {
            return;
        }
        self::ensure_private_directory( dirname( $private_root ) );

        if ( ! file_exists( $private_root ) ) {
            if ( @rename( $legacy_root, $private_root ) ) {
                self::secure_private_tree( $private_root );
                return;
            }

            if ( $this->copy_directory_verified( $legacy_root, $private_root ) ) {
                $this->remove_directory_tree( $legacy_root );
                self::secure_private_tree( $private_root );
            }
            return;
        }

        $archive = dirname( $private_root ) . '/legacy-' . self::ROOT_DIRNAME . '-' . gmdate( 'Ymd-His' ) . '-' . substr( hash( 'sha256', $legacy_root . microtime( true ) ), 0, 8 );
        if ( @rename( $legacy_root, $archive ) ) {
            $this->merge_missing_directory_files( $archive, $private_root );
            self::secure_private_tree( $archive );
            self::secure_private_tree( $private_root );
            return;
        }

        if ( $this->copy_directory_verified( $legacy_root, $archive ) ) {
            $this->merge_missing_directory_files( $archive, $private_root );
            $this->remove_directory_tree( $legacy_root );
            self::secure_private_tree( $archive );
            self::secure_private_tree( $private_root );
        }
    }

    private function migrate_legacy_public_logs() {
        if ( ! defined( 'WP_CONTENT_DIR' ) ) {
            return;
        }

        $legacy = rtrim( str_replace( '\\', '/', (string) WP_CONTENT_DIR ), '/' ) . '/uploads/niwa-ai/logs';
        $base   = self::get_private_data_root();
        if ( '' === $base || ! is_dir( $legacy ) ) {
            return;
        }

        $this->write_public_workspace_deny_files( $legacy );
        if ( ! self::secure_private_tree( $legacy ) ) {
            return;
        }
        self::ensure_private_directory( $base );
        $destination = trailingslashit( $base ) . 'logs';
        if ( ! file_exists( $destination ) && @rename( $legacy, $destination ) ) {
            self::secure_private_tree( $destination );
            return;
        }

        $archive = trailingslashit( $base ) . 'legacy-public-logs-' . gmdate( 'Ymd-His' ) . '-' . substr( hash( 'sha256', $legacy . microtime( true ) ), 0, 8 );
        if ( @rename( $legacy, $archive ) ) {
            self::secure_private_tree( $archive );
            return;
        }
        if ( $this->copy_directory_verified( $legacy, $archive ) ) {
            $this->remove_directory_tree( $legacy );
            self::secure_private_tree( $archive );
        }
    }

    private function copy_directory_verified( $source, $destination ) {
        $source      = rtrim( (string) $source, "\\/" );
        $destination = rtrim( (string) $destination, "\\/" );
        $old_umask = umask( 0077 );
        try {
            if ( ! is_dir( $source ) || ! wp_mkdir_p( $destination ) ) {
                return false;
            }
            $iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator( $source, FilesystemIterator::SKIP_DOTS ),
                RecursiveIteratorIterator::SELF_FIRST
            );
            foreach ( $iterator as $item ) {
                if ( $item->isLink() ) {
                    return false;
                }
                $relative = substr( str_replace( '\\', '/', $item->getPathname() ), strlen( str_replace( '\\', '/', $source ) ) + 1 );
                $target   = $destination . '/' . $relative;
                if ( $item->isDir() ) {
                    if ( ! wp_mkdir_p( $target ) ) {
                        return false;
                    }
                    continue;
                }
                if ( ! @copy( $item->getPathname(), $target ) || hash_file( 'sha256', $item->getPathname() ) !== hash_file( 'sha256', $target ) ) {
                    return false;
                }
            }
        } catch ( Exception $e ) {
            return false;
        } finally {
            umask( $old_umask );
        }

        return true;
    }

    private function merge_missing_directory_files( $source, $destination ) {
        if ( ! is_dir( $source ) || ! is_dir( $destination ) ) {
            return;
        }

        $old_umask = umask( 0077 );
        try {
            $iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator( $source, FilesystemIterator::SKIP_DOTS ),
                RecursiveIteratorIterator::SELF_FIRST
            );
            foreach ( $iterator as $item ) {
                if ( $item->isLink() ) {
                    return false;
                }
                $relative = substr( str_replace( '\\', '/', $item->getPathname() ), strlen( str_replace( '\\', '/', rtrim( $source, "\\/" ) ) ) + 1 );
                $target   = rtrim( $destination, "\\/" ) . '/' . $relative;
                if ( $item->isDir() ) {
                    wp_mkdir_p( $target );
                } elseif ( ! file_exists( $target ) ) {
                    wp_mkdir_p( dirname( $target ) );
                    @copy( $item->getPathname(), $target );
                }
            }
        } catch ( Exception $e ) {
            return;
        } finally {
            umask( $old_umask );
        }
    }

    private function remove_directory_tree( $directory ) {
        if ( ! is_dir( $directory ) ) {
            return true;
        }

        try {
            $iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator( $directory, FilesystemIterator::SKIP_DOTS ),
                RecursiveIteratorIterator::CHILD_FIRST
            );
            foreach ( $iterator as $item ) {
                $removed = $item->isDir() && ! $item->isLink()
                    ? @rmdir( $item->getPathname() )
                    : @unlink( $item->getPathname() );
                if ( ! $removed ) {
                    return false;
                }
            }
        } catch ( Exception $e ) {
            return false;
        }

        return @rmdir( $directory );
    }

    private function write_public_workspace_deny_files( $legacy_root ) {
        @file_put_contents( trailingslashit( $legacy_root ) . '.htaccess', "Require all denied\nDeny from all\n", LOCK_EX );
        @file_put_contents(
            trailingslashit( $legacy_root ) . 'web.config',
            '<?xml version="1.0" encoding="UTF-8"?><configuration><system.webServer><security><authorization><remove users="*" roles="" verbs=""/><add accessType="Deny" users="*"/></authorization></security></system.webServer></configuration>',
            LOCK_EX
        );
        @file_put_contents( trailingslashit( $legacy_root ) . 'index.php', "<?php\nhttp_response_code( 404 );\nexit;\n", LOCK_EX );
    }

    public static function ensure_private_data_permissions() {
        return self::apply_private_data_permissions( self::get_private_data_root() );
    }

    public static function refresh_private_data_permissions() {
        $root = rtrim( (string) self::get_private_data_root(), "\\/" );
        if ( '' === $root || ! is_dir( $root ) ) {
            return false;
        }
        @unlink( trailingslashit( $root ) . self::PRIVATE_PERMISSIONS_MARKER );
        return self::apply_private_data_permissions( $root );
    }

    private static function apply_private_data_permissions( $root ) {
        $root   = rtrim( (string) $root, "\\/" );
        $marker = trailingslashit( $root ) . self::PRIVATE_PERMISSIONS_MARKER;
        if ( '' === $root || ! is_dir( $root ) || is_file( $marker ) ) {
            return is_file( $marker );
        }

        $protected = self::secure_private_tree( $root );
        if ( $protected ) {
            $old_umask = umask( 0077 );
            try {
                $marked = @file_put_contents( $marker, "private permissions applied\n", LOCK_EX );
            } finally {
                umask( $old_umask );
            }
            if ( false !== $marked ) {
                @chmod( $marker, 0600 );
                return true;
            }
        }
        return false;
    }

    private static function secure_private_tree( $root ) {
        if ( ! is_dir( $root ) || is_link( $root ) ) {
            return false;
        }

        $protected = @chmod( $root, 0700 );
        try {
            $iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator( $root, FilesystemIterator::SKIP_DOTS ),
                RecursiveIteratorIterator::SELF_FIRST
            );
            foreach ( $iterator as $item ) {
                if ( $item->isLink() ) {
                    return false;
                }
                $protected = @chmod( $item->getPathname(), $item->isDir() ? 0700 : 0600 ) && $protected;
            }
        } catch ( Exception $e ) {
            $protected = false;
        }

        return $protected;
    }

    public static function ensure_private_directory( $directory ) {
        if ( is_link( $directory ) ) {
            return false;
        }
        $old_umask = umask( 0077 );
        try {
            $ready = is_dir( $directory ) || wp_mkdir_p( $directory );
        } finally {
            umask( $old_umask );
        }
        if ( $ready ) {
            @chmod( $directory, 0700 );
        }
        return $ready;
    }

    public static function write_private_file( $path, $content, $flags = 0 ) {
        $old_umask = umask( 0077 );
        try {
            $written = @file_put_contents( $path, $content, $flags );
        } finally {
            umask( $old_umask );
        }
        if ( false !== $written ) {
            @chmod( $path, 0600 );
        }
        return $written;
    }

    public static function open_private_file( $path, $mode ) {
        $old_umask = umask( 0077 );
        try {
            $handle = @fopen( $path, $mode );
        } finally {
            umask( $old_umask );
        }
        if ( is_resource( $handle ) ) {
            @chmod( $path, 0600 );
        }
        return $handle;
    }

    public static function copy_private_file( $source, $destination ) {
        $old_umask = umask( 0077 );
        try {
            $copied = @copy( $source, $destination );
        } finally {
            umask( $old_umask );
        }
        if ( $copied ) {
            @chmod( $destination, 0600 );
        }
        return $copied;
    }

    public function ensure_bootstrap_files() {
        $paths = $this->get_paths();
        $this->ensure_private_directory( $paths['root'] );

        foreach ( self::BOOTSTRAP_FILES as $filename ) {
            $path = trailingslashit( $paths['root'] ) . $filename;
            if ( file_exists( $path ) ) {
                if ( in_array( $filename, array( 'SOUL.md', 'IDENTITY.md', 'USER.md' ), true ) ) {
                    $this->maybe_upgrade_legacy_identity_stub( $filename, $path );
                }
                if ( 'HEARTBEAT.md' === $filename ) {
                    $this->maybe_upgrade_legacy_heartbeat_stub( $path );
                }
                continue;
            }

            $this->write_private_file( $path, $this->get_default_bootstrap_content( $filename ) );
        }
    }

    public function get_bootstrap_payload( $mode = 'main' ) {
        // ensure_bootstrap_files() is already called by ensure_workspace(); skip redundant check if already done.
        if ( ! $this->workspace_ensured ) {
            $this->ensure_bootstrap_files();
        }
        $mode = 'subagent' === $mode ? 'subagent' : ( 'stock_manager' === $mode ? 'stock_manager' : 'main' );
        if ( 'subagent' === $mode ) {
            $filenames = array( 'AGENTS.md', 'TOOLS.md' );
        } elseif ( 'stock_manager' === $mode ) {
            $filenames = array( 'AGENTS.md', 'TOOLS.md', 'OPERATIONAL_MEMORY.md', 'STOCK_OPERATIVE_HEARTBEAT.md' );
        } else {
            $filenames = array( 'AGENTS.md', 'COMPANION_MEMORY.md', 'TOOLS.md', 'OPERATIONAL_MEMORY.md', 'OPERATIVE_HEARTBEAT.md' );
        }
        $payload   = array();

        foreach ( $filenames as $filename ) {
            if ( 'OPERATIONAL_MEMORY.md' === $filename ) {
                $content = $this->get_operational_prompt_summary( 1400 );
            } elseif ( 'COMPANION_MEMORY.md' === $filename ) {
                $content = $this->get_companion_prompt_summary( 1100 );
            } else {
                $record = $this->read_bootstrap_file( $filename );
                $content = empty( $record['success'] ) ? '' : (string) $record['content'];
            }

            if ( '' !== trim( $content ) ) {
                $payload[ $filename ] = $content;
            }
        }

        return $payload;
    }

    public function get_identity_profile() {
        $state   = $this->get_workspace_state();
        $profile = isset( $state['identity_profile'] ) && is_array( $state['identity_profile'] ) ? $state['identity_profile'] : array();
        $profile = wp_parse_args( $profile, $this->get_default_identity_profile() );

        if ( empty( $profile['completed_at'] ) && $this->has_custom_identity_bootstrap() ) {
            $profile['completed_at']   = 'inferred-from-workspace';
            $profile['onboarding_step'] = 'complete';
            $profile['legacy_inferred'] = true;
        }

        return $profile;
    }

    public function save_identity_profile( $profile, $sync_bootstrap_files = true ) {
        $defaults = $this->get_default_identity_profile();
        $profile  = wp_parse_args( is_array( $profile ) ? $profile : array(), $defaults );
        $profile  = array(
            'assistant_name'     => class_exists( 'Niwa_AI_Identity_Name' ) ? Niwa_AI_Identity_Name::normalize( isset( $profile['assistant_name'] ) ? $profile['assistant_name'] : '' ) : '',
            'assistant_role'     => $this->sanitize_text_block( isset( $profile['assistant_role'] ) ? $profile['assistant_role'] : $defaults['assistant_role'], 300 ),
            'assistant_identity' => $this->sanitize_text_block( isset( $profile['assistant_identity'] ) ? $profile['assistant_identity'] : '', 1200 ),
            'companion_summary'  => $this->sanitize_text_block( isset( $profile['companion_summary'] ) ? $profile['companion_summary'] : '', 1600 ),
            'founder_preferences' => $this->sanitize_text_list( isset( $profile['founder_preferences'] ) ? $profile['founder_preferences'] : array(), 10, 220 ),
            'communication_preferences' => $this->sanitize_text_list( isset( $profile['communication_preferences'] ) ? $profile['communication_preferences'] : array(), 8, 200 ),
            'communication_style' => $this->sanitize_text_block( isset( $profile['communication_style'] ) ? $profile['communication_style'] : '', 800 ),
            'memory_priorities'  => $this->sanitize_text_list( isset( $profile['memory_priorities'] ) ? $profile['memory_priorities'] : array(), 8, 200 ),
            'decision_principles' => $this->sanitize_text_list( isset( $profile['decision_principles'] ) ? $profile['decision_principles'] : array(), 8, 220 ),
            'motivators'         => $this->sanitize_text_list( isset( $profile['motivators'] ) ? $profile['motivators'] : array(), 10, 220 ),
            'boundaries'         => $this->sanitize_text_list( isset( $profile['boundaries'] ) ? $profile['boundaries'] : array(), 10, 220 ),
            'diary_focus'        => $this->sanitize_text_list( isset( $profile['diary_focus'] ) ? $profile['diary_focus'] : array(), 8, 180 ),
            'current_focus'      => $this->sanitize_text_block( isset( $profile['current_focus'] ) ? $profile['current_focus'] : '', 500 ),
            'current_focus_areas' => $this->sanitize_text_list( isset( $profile['current_focus_areas'] ) ? $profile['current_focus_areas'] : array(), 10, 220 ),
            'relationship_notes' => $this->sanitize_text_block( isset( $profile['relationship_notes'] ) ? $profile['relationship_notes'] : '', 1200 ),
            'relationship_history_summary' => $this->sanitize_text_block( isset( $profile['relationship_history_summary'] ) ? $profile['relationship_history_summary'] : '', 1600 ),
            'founder_name'       => class_exists( 'Niwa_AI_Identity_Name' ) ? Niwa_AI_Identity_Name::normalize( isset( $profile['founder_name'] ) ? $profile['founder_name'] : '' ) : '',
            'founder_address'    => $this->sanitize_text_block( isset( $profile['founder_address'] ) ? $profile['founder_address'] : '', 300 ),
            'founder_profile'    => $this->sanitize_text_block( isset( $profile['founder_profile'] ) ? $profile['founder_profile'] : '', 1200 ),
            'relationship_style' => $this->sanitize_text_block( isset( $profile['relationship_style'] ) ? $profile['relationship_style'] : '', 1600 ),
            'assistant_personality_facets' => $this->sanitize_text_list( isset( $profile['assistant_personality_facets'] ) ? $profile['assistant_personality_facets'] : array(), 10, 180 ),
            'durable_rules'      => $this->sanitize_text_list( isset( $profile['durable_rules'] ) ? $profile['durable_rules'] : array(), 10, 240 ),
            'onboarding_step'    => $this->sanitize_onboarding_step( isset( $profile['onboarding_step'] ) ? $profile['onboarding_step'] : $defaults['onboarding_step'] ),
            'completed_at'       => $this->sanitize_short_text( isset( $profile['completed_at'] ) ? $profile['completed_at'] : '', 80 ),
            'updated_at'         => time(),
            'legacy_inferred'    => ! empty( $profile['legacy_inferred'] ),
        );

        if ( '' === $profile['assistant_name'] ) {
            $profile['assistant_name'] = $defaults['assistant_name'];
        }
        if ( '' === $profile['assistant_role'] ) {
            $profile['assistant_role'] = $defaults['assistant_role'];
        }

        $state                     = $this->get_workspace_state();
        $state['identity_profile'] = $profile;
        $state['companion_memory'] = $this->merge_companion_memory_state(
            isset( $state['companion_memory'] ) && is_array( $state['companion_memory'] ) ? $state['companion_memory'] : array(),
            array(
                'profile'     => $profile,
                'updated_at'   => time(),
                'profile_hash' => sha1( wp_json_encode( $profile ) ),
            )
        );
        $this->save_workspace_state( $state );

        if ( $sync_bootstrap_files ) {
            $this->write_bootstrap_file( 'SOUL.md', $this->build_identity_bootstrap_content( 'SOUL.md', $profile ) );
            $this->write_bootstrap_file( 'IDENTITY.md', $this->build_identity_bootstrap_content( 'IDENTITY.md', $profile ) );
            $this->write_bootstrap_file( 'USER.md', $this->build_identity_bootstrap_content( 'USER.md', $profile ) );
            $this->write_companion_layered_files( $profile );
            $this->write_companion_profile_snapshot( $profile );
        }

        return $profile;
    }

    public function get_companion_memory_profile() {
        return $this->get_identity_profile();
    }

    public function get_operational_memory_file_path( $filename = self::OPERATIONAL_LEARNINGS_FILE ) {
        $filename = basename( (string) $filename );
        if ( ! in_array( $filename, array( self::OPERATIONAL_RULES_FILE, self::OPERATIONAL_LEARNINGS_FILE ), true ) ) {
            $filename = self::OPERATIONAL_LEARNINGS_FILE;
        }

        $paths = $this->ensure_workspace();
        return trailingslashit( $paths['memory'] ) . self::OPERATIONAL_MEMORY_DIR . '/' . $filename;
    }

    public function get_operational_prompt_summary( $limit = 1200 ) {
        $this->ensure_layered_memory_files();

        $sections = array();
        foreach ( array(
            self::OPERATIONAL_RULES_FILE => 'Rules',
            self::OPERATIONAL_LEARNINGS_FILE => 'Learnings',
        ) as $filename => $label ) {
            $content = $this->read_layered_memory_file( self::OPERATIONAL_MEMORY_DIR, $filename );
            $content = $this->strip_markdown_title( $content );
            if ( '' !== trim( $content ) ) {
                $sections[] = $label . ":\n" . $this->truncate_text( trim( $content ), 700 );
            }
        }

        return $this->truncate_text( implode( "\n\n", $sections ), max( 400, intval( $limit ) ) );
    }

    public function get_companion_prompt_summary( $limit = 1000 ) {
        $this->ensure_layered_memory_files();

        $sections = array();
        foreach ( array(
            self::COMPANION_IDENTITY_FILE => 'Identity',
            self::COMPANION_USER_FILE => 'Founder',
            self::COMPANION_RELATIONSHIP_FILE => 'Relationship',
        ) as $filename => $label ) {
            $content = $this->read_layered_memory_file( self::COMPANION_MEMORY_DIR, $filename );
            $content = $this->strip_markdown_title( $content );
            if ( '' !== trim( $content ) ) {
                $sections[] = $label . ":\n" . $this->truncate_text( trim( $content ), 420 );
            }
        }

        return $this->truncate_text( implode( "\n\n", $sections ), max( 400, intval( $limit ) ) );
    }

    public function save_companion_memory_profile( $profile, $sync_bootstrap_files = true ) {
        return $this->save_identity_profile( $profile, $sync_bootstrap_files );
    }

    public function sync_companion_memory_profile_snapshot( $profile ) {
        return $this->write_companion_profile_snapshot( $profile );
    }

    public function append_companion_diary_entry( $entry, $sync_bootstrap_files = true ) {
        $entry      = is_array( $entry ) ? $entry : array();
        $title      = $this->sanitize_short_text( isset( $entry['title'] ) ? $entry['title'] : '', 120 );
        $summary    = $this->sanitize_text_block( isset( $entry['summary'] ) ? $entry['summary'] : '', 1200 );
        $details    = $this->sanitize_text_block( isset( $entry['details'] ) ? $entry['details'] : '', 2400 );
        $tags       = $this->sanitize_text_list( isset( $entry['tags'] ) ? $entry['tags'] : array(), 8, 80 );
        $created_at = isset( $entry['created_at'] ) ? intval( $entry['created_at'] ) : time();
        $date       = $this->sanitize_short_text( isset( $entry['date'] ) ? $entry['date'] : wp_date( 'Y-m-d', $created_at ), 20 );

        if ( '' === $title && '' === $summary && '' === $details ) {
            return array(
                'success' => false,
                'error'   => 'Diary entry is empty.',
            );
        }

        $normalized = array(
            'title'      => $title,
            'summary'    => $summary,
            'details'    => $details,
            'tags'       => $tags,
            'date'       => $date,
            'created_at' => $created_at,
        );
        $key   = substr( sha1( wp_json_encode( $normalized ) ), 0, 20 );
        $block  = $this->build_companion_diary_entry_block( $normalized, $key );
        $result = $this->append_companion_diary_file( $date, $block, $key );

        if ( ! empty( $result['success'] ) ) {
            $state = $this->get_workspace_state();
            $companion = isset( $state['companion_memory'] ) && is_array( $state['companion_memory'] ) ? $state['companion_memory'] : array();
            $recent = isset( $companion['recent_diary_entries'] ) && is_array( $companion['recent_diary_entries'] ) ? $companion['recent_diary_entries'] : array();
            $recent[] = $normalized;
            $companion['recent_diary_entries'] = array_slice( $recent, -12 );
            $companion['updated_at']           = time();
            $state['companion_memory']         = $companion;
            $this->save_workspace_state( $state );

            if ( $sync_bootstrap_files ) {
                $this->append_companion_memory_projection(
                    'diary',
                    array(
                        'title'   => $title,
                        'summary' => $summary,
                        'date'    => $date,
                    )
                );
            }
        }

        return $result;
    }

    public function append_companion_decision_entry( $entry, $sync_bootstrap_files = true ) {
        $entry      = is_array( $entry ) ? $entry : array();
        $title      = $this->sanitize_short_text( isset( $entry['title'] ) ? $entry['title'] : '', 120 );
        $context    = $this->sanitize_text_block( isset( $entry['context'] ) ? $entry['context'] : '', 1600 );
        $decision   = $this->sanitize_text_block( isset( $entry['decision'] ) ? $entry['decision'] : '', 1600 );
        $rationale  = $this->sanitize_text_block( isset( $entry['rationale'] ) ? $entry['rationale'] : '', 1800 );
        $tags       = $this->sanitize_text_list( isset( $entry['tags'] ) ? $entry['tags'] : array(), 8, 80 );
        $created_at = isset( $entry['created_at'] ) ? intval( $entry['created_at'] ) : time();
        $date       = $this->sanitize_short_text( isset( $entry['date'] ) ? $entry['date'] : wp_date( 'Y-m-d', $created_at ), 20 );

        if ( '' === $title && '' === $context && '' === $decision && '' === $rationale ) {
            return array(
                'success' => false,
                'error'   => 'Decision entry is empty.',
            );
        }

        $normalized = array(
            'title'      => $title,
            'context'    => $context,
            'decision'   => $decision,
            'rationale'  => $rationale,
            'tags'       => $tags,
            'date'       => $date,
            'created_at' => $created_at,
        );
        $key   = substr( sha1( wp_json_encode( $normalized ) ), 0, 20 );
        $block  = $this->build_companion_decision_entry_block( $normalized, $key );
        $result = $this->append_companion_decision_file( $block, $key );

        if ( ! empty( $result['success'] ) ) {
            $state = $this->get_workspace_state();
            $companion = isset( $state['companion_memory'] ) && is_array( $state['companion_memory'] ) ? $state['companion_memory'] : array();
            $recent = isset( $companion['recent_decision_entries'] ) && is_array( $companion['recent_decision_entries'] ) ? $companion['recent_decision_entries'] : array();
            $recent[] = $normalized;
            $companion['recent_decision_entries'] = array_slice( $recent, -12 );
            $companion['updated_at']             = time();
            $state['companion_memory']           = $companion;
            $this->save_workspace_state( $state );

            if ( $sync_bootstrap_files ) {
                $this->append_companion_memory_projection(
                    'decision',
                    array(
                        'title'     => $title,
                        'decision'  => $decision,
                        'rationale' => $rationale,
                        'date'      => $date,
                    )
                );
            }
        }

        return $result;
    }

    public function get_companion_diary_entries( $limit = 12 ) {
        $state = $this->get_workspace_state();
        $companion = isset( $state['companion_memory'] ) && is_array( $state['companion_memory'] ) ? $state['companion_memory'] : array();
        $entries = isset( $companion['recent_diary_entries'] ) && is_array( $companion['recent_diary_entries'] ) ? $companion['recent_diary_entries'] : array();
        $entries = $this->merge_companion_entry_sets( $entries, $this->read_companion_diary_entries_from_files( $limit ), $limit );

        return array_slice( array_values( $entries ), -max( 1, intval( $limit ) ) );
    }

    public function get_companion_decision_entries( $limit = 12 ) {
        $state = $this->get_workspace_state();
        $companion = isset( $state['companion_memory'] ) && is_array( $state['companion_memory'] ) ? $state['companion_memory'] : array();
        $entries = isset( $companion['recent_decision_entries'] ) && is_array( $companion['recent_decision_entries'] ) ? $companion['recent_decision_entries'] : array();
        $entries = $this->merge_companion_entry_sets( $entries, $this->read_companion_decision_entries_from_files( $limit ), $limit );

        return array_slice( array_values( $entries ), -max( 1, intval( $limit ) ) );
    }

    public function is_identity_onboarding_complete() {
        $profile = $this->get_identity_profile();
        return ! empty( $profile['completed_at'] ) && 'complete' === (string) $profile['onboarding_step'];
    }

    public function build_founder_intro( $host = '' ) {
        $profile = $this->get_identity_profile();
        $name    = ! empty( $profile['assistant_name'] ) ? (string) $profile['assistant_name'] : '';
        $language = $this->get_workspace_ui_language();
        $copy     = $this->get_workspace_language_copy( $language );
        $role     = ! empty( $profile['assistant_role'] )
            ? rtrim( (string) $profile['assistant_role'], ". \t\n\r\0\x0B" )
            : ( isset( $copy['intro_default_role'] ) ? (string) $copy['intro_default_role'] : 'personal AI assistant for this site' );

        $intro = '' !== $name
            ? ( isset( $copy['intro_named_prefix'] ) ? (string) $copy['intro_named_prefix'] : 'I am ' ) . $name . ', ' . $role
            : ( isset( $copy['intro_unnamed_prefix'] ) ? (string) $copy['intro_unnamed_prefix'] : 'I am your ' ) . $role;
        if ( '' !== trim( (string) $host ) ) {
            $intro .= ' ' . ( isset( $copy['intro_host_connector'] ) ? (string) $copy['intro_host_connector'] : 'for' ) . ' ' . trim( (string) $host );
        }
        $intro .= '.';

        return $this->normalize_assistant_text( $intro );
    }

    private function get_workspace_ui_language() {
        $language = '';
        if ( function_exists( 'get_option' ) ) {
            $ui_settings = get_option( Niwa_AI_Constants::OPT_UI_SETTINGS, array() );
            $language    = is_array( $ui_settings ) && ! empty( $ui_settings['ui_language'] )
                ? (string) $ui_settings['ui_language']
                : '';
        }

        if ( '' === trim( $language ) && function_exists( 'determine_locale' ) ) {
            $language = (string) determine_locale();
        }

        return $this->normalize_workspace_language_code( $language );
    }

    private function normalize_workspace_language_code( $language ) {
        $language = strtolower( trim( str_replace( '-', '_', (string) $language ) ) );
        if ( '' === $language ) {
            return 'en';
        }

        $parts = preg_split( '/_+/', $language );
        $base  = isset( $parts[0] ) ? preg_replace( '/[^a-z]/', '', (string) $parts[0] ) : '';
        if ( '' === $base ) {
            return 'en';
        }

        if ( 'bs' === $base ) {
            return 'sr';
        }

        return in_array( $base, array( 'en', 'sr', 'hr', 'de', 'es', 'fr' ), true ) ? $base : 'en';
    }

    private function get_workspace_language_copy( $language ) {
        $language = $this->normalize_workspace_language_code( $language );
        $copy     = array(
            'intro_named_prefix'   => 'I am ',
            'intro_unnamed_prefix' => 'I am your ',
            'intro_default_role'   => 'personal AI assistant for this site',
            'intro_host_connector' => 'for',
        );

        $translations = array(
            'sr' => array(
                'intro_named_prefix'   => 'Ja sam ',
                'intro_unnamed_prefix' => 'Ja sam tvoja ',
                'intro_default_role'   => 'personalna AI asistentica za ovaj sajt',
                'intro_host_connector' => 'za',
            ),
            'hr' => array(
                'intro_named_prefix'   => 'Ja sam ',
                'intro_unnamed_prefix' => 'Ja sam tvoja ',
                'intro_default_role'   => 'osobna AI asistentica za ovu stranicu',
                'intro_host_connector' => 'za',
            ),
            'de' => array(
                'intro_named_prefix'   => 'Ich bin ',
                'intro_unnamed_prefix' => 'Ich bin deine ',
                'intro_default_role'   => 'persoenliche KI-Assistentin fuer diese Website',
                'intro_host_connector' => 'fuer',
            ),
            'es' => array(
                'intro_named_prefix'   => 'Soy ',
                'intro_unnamed_prefix' => 'Soy tu ',
                'intro_default_role'   => 'asistente de IA personal para este sitio',
                'intro_host_connector' => 'para',
            ),
            'fr' => array(
                'intro_named_prefix'   => 'Je suis ',
                'intro_unnamed_prefix' => 'Je suis ton ',
                'intro_default_role'   => 'assistante IA personnelle pour ce site',
                'intro_host_connector' => 'pour',
            ),
        );

        if ( isset( $translations[ $language ] ) ) {
            $copy = array_merge( $copy, $translations[ $language ] );
        }

        return $copy;
    }

    public function normalize_assistant_text( $text ) {
        $text = (string) $text;
        if ( '' === trim( $text ) ) {
            return $text;
        }

        $replacements = array(
            '/\buradio sam\b/ui'    => 'uradila sam',
            '/\bodradio sam\b/ui'   => 'odradila sam',
            '/\bnapravio sam\b/ui'  => 'napravila sam',
            '/\bzavrsio sam\b/ui'   => 'zavrsila sam',
            '/\bpokrenuo sam\b/ui'  => 'pokrenula sam',
            '/\bproverio sam\b/ui'  => 'proverila sam',
            '/\bizmenio sam\b/ui'   => 'izmenila sam',
            '/\bpromenio sam\b/ui'  => 'promenila sam',
            '/\bupisao sam\b/ui'    => 'upisala sam',
            '/\bdodao sam\b/ui'     => 'dodala sam',
            '/\bobrisao sam\b/ui'   => 'obrisala sam',
            '/\bvratio sam\b/ui'    => 'vratila sam',
            '/\bsredio sam\b/ui'    => 'sredila sam',
            '/\bnasao sam\b/ui'     => 'nasla sam',
            '/\bpronasao sam\b/ui'  => 'pronasla sam',
            '/\bmogao sam\b/ui'     => 'mogla sam',
            '/\bmorao sam\b/ui'     => 'morala sam',
            '/\bbio sam\b/ui'       => 'bila sam',
            '/\buspeo sam\b/ui'     => 'uspela sam',
            '/\bnisam uspeo\b/ui'   => 'nisam uspela',
        );

        foreach ( $replacements as $pattern => $replacement ) {
            $text = preg_replace( $pattern, $replacement, $text );
        }

        return $text;
    }

    public function build_prompt_context( $mode = 'main', $max_chars = 5000 ) {
        $mode        = 'subagent' === $mode ? 'subagent' : ( 'stock_manager' === $mode ? 'stock_manager' : 'main' );
        if ( 'subagent' === $mode ) {
            $filenames   = self::SUBAGENT_PROMPT_BOOTSTRAP_FILES;
            $file_limits = self::SUBAGENT_PROMPT_BOOTSTRAP_LIMITS;
            $mode_total_limit = self::SUBAGENT_PROMPT_BOOTSTRAP_TOTAL;
        } elseif ( 'stock_manager' === $mode ) {
            $filenames   = self::STOCK_MANAGER_PROMPT_BOOTSTRAP_FILES;
            $file_limits = self::STOCK_MANAGER_PROMPT_BOOTSTRAP_LIMITS;
            $mode_total_limit = self::STOCK_MANAGER_PROMPT_BOOTSTRAP_TOTAL;
        } else {
            $filenames   = self::MAIN_PROMPT_BOOTSTRAP_FILES;
            $file_limits = self::MAIN_PROMPT_BOOTSTRAP_LIMITS;
            $mode_total_limit = self::MAIN_PROMPT_BOOTSTRAP_TOTAL;
        }
        $payload     = $this->get_bootstrap_payload( $mode );
        if ( empty( $payload ) || empty( $filenames ) ) {
            return '';
        }

        $max_chars        = max( 800, min( intval( $max_chars ), $mode_total_limit ) );
        $parts            = array();
        $remaining        = $max_chars;

        foreach ( $filenames as $filename ) {
            if ( empty( $payload[ $filename ] ) ) {
                continue;
            }

            $content = trim( (string) $payload[ $filename ] );
            if ( '' === $content ) {
                continue;
            }

            $prefix = '[' . $filename . "]\n";
            $budget = isset( $file_limits[ $filename ] ) ? intval( $file_limits[ $filename ] ) : 900;
            $budget = max( 240, min( $budget, $remaining - strlen( $prefix ) ) );
            if ( $budget < 240 ) {
                break;
            }

            $part        = $prefix . $this->truncate_text( $content, $budget );
            $parts[]     = $part;
            $remaining  -= strlen( $part ) + 2;

            if ( $remaining < 240 ) {
                break;
            }
        }

        return $this->truncate_text( implode( "\n\n", $parts ), $max_chars );
    }

    public function read_bootstrap_file( $filename ) {
        $filename = $this->normalize_bootstrap_filename( $filename );
        if ( '' === $filename ) {
            return array(
                'success' => false,
                'error'   => 'Nepodrzan workspace bootstrap fajl.',
            );
        }

        $this->ensure_bootstrap_files();
        $path = trailingslashit( $this->get_paths()['root'] ) . $filename;
        if ( ! file_exists( $path ) ) {
            return array(
                'success' => false,
                'error'   => 'Workspace bootstrap fajl ne postoji.',
            );
        }

        $read = Niwa_AI_Text_Normalizer::normalize_file_contents( $path );
        if ( empty( $read['success'] ) ) {
            return array(
                'success' => false,
                'error'   => 'Workspace bootstrap fajl nije citljiv.',
            );
        }

        return array(
            'success'  => true,
            'filename' => $filename,
            'path'     => $path,
            'content'  => isset( $read['content'] ) ? (string) $read['content'] : '',
        );
    }

    public function write_bootstrap_file( $filename, $content, $append = false ) {
        $filename = $this->normalize_bootstrap_filename( $filename );
        if ( '' === $filename ) {
            return array(
                'success' => false,
                'error'   => 'Nepodrzan workspace bootstrap fajl.',
            );
        }

        $this->ensure_bootstrap_files();
        $path    = trailingslashit( $this->get_paths()['root'] ) . $filename;
        $content = Niwa_AI_Text_Normalizer::normalize(
            $content,
            array(
                'trim'                => false,
                'collapse_whitespace' => false,
                'preserve_newlines'   => true,
            )
        );

        if ( $append && file_exists( $path ) ) {
            $existing = Niwa_AI_Text_Normalizer::normalize_file_contents( $path );
            $content  = ( ! empty( $existing['success'] ) ? rtrim( (string) $existing['content'] ) : '' ) . "\n\n" . ltrim( $content );
        }

        $saved = $this->write_private_file( $path, $content, LOCK_EX );
        if ( false === $saved ) {
            return array(
                'success' => false,
                'error'   => 'Workspace bootstrap write failed.',
            );
        }

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

    public function append_memory_note( $note, $heading = '' ) {
        $note = trim( (string) $note );
        if ( '' === $note ) {
            return array(
                'success' => false,
                'error'   => 'Memory note je prazan.',
            );
        }

        $prefix = '';
        if ( '' !== trim( (string) $heading ) ) {
            $prefix = '## ' . trim( (string) $heading ) . "\n";
        }

        return $this->append_operational_memory_block(
            self::OPERATIONAL_LEARNINGS_FILE,
            $prefix . '- ' . $note
        );
    }

    public function get_memory_directory_path() {
        $paths = $this->ensure_workspace();
        return isset( $paths['memory'] ) ? (string) $paths['memory'] : '';
    }

    public function get_daily_memory_file_path( $date = '' ) {
        $date = trim( (string) $date );
        if ( '' === $date ) {
            $date = wp_date( 'Y-m-d' );
        }

        $memory_dir = $this->get_memory_directory_path();
        if ( '' === $memory_dir ) {
            return '';
        }

        return trailingslashit( $memory_dir ) . sanitize_file_name( $date ) . '.md';
    }

    public function list_memory_documents( $limit = 40 ) {
        $paths = $this->ensure_workspace();
        $files = array_merge(
            is_array( glob( trailingslashit( $paths['memory'] ) . '*.md' ) ) ? glob( trailingslashit( $paths['memory'] ) . '*.md' ) : array(),
            is_array( glob( trailingslashit( $paths['memory'] ) . self::OPERATIONAL_MEMORY_DIR . '/*.md' ) ) ? glob( trailingslashit( $paths['memory'] ) . self::OPERATIONAL_MEMORY_DIR . '/*.md' ) : array(),
            is_array( glob( trailingslashit( $paths['memory'] ) . self::COMPANION_MEMORY_DIR . '/*.md' ) ) ? glob( trailingslashit( $paths['memory'] ) . self::COMPANION_MEMORY_DIR . '/*.md' ) : array(),
            is_array( glob( trailingslashit( $paths['memory'] ) . self::DIARY_DIR . '/' . self::DIARY_CURRENT_DIR . '/*.md' ) ) ? glob( trailingslashit( $paths['memory'] ) . self::DIARY_DIR . '/' . self::DIARY_CURRENT_DIR . '/*.md' ) : array(),
            is_array( glob( trailingslashit( $paths['memory'] ) . self::DIARY_DIR . '/' . self::DIARY_ARCHIVED_DIR . '/*/*.md' ) ) ? glob( trailingslashit( $paths['memory'] ) . self::DIARY_DIR . '/' . self::DIARY_ARCHIVED_DIR . '/*/*.md' ) : array()
        );
        if ( ! is_array( $files ) ) {
            return array();
        }

        usort(
            $files,
            function ( $left, $right ) {
                return intval( @filemtime( $right ) ) <=> intval( @filemtime( $left ) );
            }
        );

        $records = array();
        foreach ( array_slice( $files, 0, max( 1, min( 200, intval( $limit ) ) ) ) as $path ) {
            if ( ! is_file( $path ) || ! is_readable( $path ) ) {
                continue;
            }

            $relative = ltrim( str_replace( '\\', '/', str_replace( trailingslashit( $paths['memory'] ), '', (string) $path ) ), '/' );
            $records[] = array(
                'path'     => $path,
                'rel_path' => 'memory/' . $relative,
                'modified' => intval( @filemtime( $path ) ),
                'size'     => intval( @filesize( $path ) ),
            );
        }

        return $records;
    }

    public function append_daily_memory_entry( $note, $section = 'Durable Learnings', $dedupe_key = '', $date = '' ) {
        $note = Niwa_AI_Text_Normalizer::normalize_inline( $note );
        if ( '' === $note ) {
            return array(
                'success' => false,
                'error'   => 'Daily memory note je prazan.',
            );
        }

        $path = $this->get_daily_memory_file_path( $date );
        if ( '' === $path ) {
            return array(
                'success' => false,
                'error'   => 'Daily memory putanja nije dostupna.',
            );
        }

        $marker = '' !== trim( (string) $dedupe_key ) ? '<!-- niwa-memory-key:' . sanitize_key( (string) $dedupe_key ) . ' -->' : '';
        if ( '' !== $marker && file_exists( $path ) ) {
            $existing = Niwa_AI_Text_Normalizer::normalize_file_contents( $path );
            if ( ! empty( $existing['success'] ) && false !== strpos( (string) $existing['content'], $marker ) ) {
                return array(
                    'success'   => true,
                    'path'      => $path,
                    'duplicate' => true,
                );
            }
        }

        if ( ! file_exists( $path ) ) {
            $header = '# Daily Memory ' . wp_date( 'Y-m-d', strtotime( basename( $path, '.md' ) ) ) . "\n\n";
            $this->write_private_file( $path, $header, LOCK_EX );
        }

        $block = '';
        if ( '' !== trim( (string) $section ) ) {
            $block .= '## ' . Niwa_AI_Text_Normalizer::normalize_inline( $section ) . "\n";
        }
        if ( '' !== $marker ) {
            $block .= $marker . "\n";
        }
        $block .= '- Learned at: ' . wp_date( 'c' ) . "\n";
        $block .= '- Note: ' . $note . "\n";

        $saved = $this->write_private_file(
            $path,
            Niwa_AI_Text_Normalizer::normalize( "\n" . $block, array( 'preserve_newlines' => true ) ),
            FILE_APPEND | LOCK_EX
        );
        if ( false === $saved ) {
            return array(
                'success' => false,
                'error'   => 'Daily memory write failed.',
            );
        }

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

    public function append_operating_memory_rule( $rule, $dedupe_key = '', $section = 'Operating Lessons' ) {
        $rule = Niwa_AI_Text_Normalizer::normalize_inline( $rule );
        if ( '' === $rule ) {
            return array(
                'success' => false,
                'error'   => 'Operating rule je prazno.',
            );
        }

        $target_file = $this->select_operational_memory_target( $section );
        $path        = $this->get_operational_memory_file_path( $target_file );
        if ( '' === $path ) {
            return array(
                'success' => false,
                'error'   => 'Operational memory nije dostupna.',
            );
        }

        $marker   = '' !== trim( (string) $dedupe_key ) ? '<!-- niwa-memory-key:' . sanitize_key( (string) $dedupe_key ) . ' -->' : '';
        $existing = file_exists( $path ) ? (string) file_get_contents( $path ) : '';
        if ( '' !== $marker && false !== strpos( $existing, $marker ) ) {
            return array(
                'success'   => true,
                'path'      => $path,
                'duplicate' => true,
            );
        }

        $entry = '## ' . Niwa_AI_Text_Normalizer::normalize_inline( $section ) . "\n";
        if ( '' !== $marker ) {
            $entry .= $marker . "\n";
        }
        $entry .= '- ' . $rule;

        return $this->append_operational_memory_block( $target_file, $entry );
    }

    public function repair_memory_text_artifacts( $force = false ) {
        if ( $this->memory_text_repair_ran && ! $force ) {
            return array(
                'success'  => true,
                'cached'   => true,
                'scanned'  => 0,
                'repaired' => 0,
            );
        }

        $this->ensure_workspace();
        $paths    = $this->get_paths();
        $scanned  = 0;
        $repaired = 0;

        foreach ( self::BOOTSTRAP_FILES as $filename ) {
            $path = trailingslashit( $paths['root'] ) . $filename;
            if ( ! file_exists( $path ) || ! is_readable( $path ) ) {
                continue;
            }

            $read = Niwa_AI_Text_Normalizer::normalize_file_contents( $path );
            if ( empty( $read['success'] ) ) {
                continue;
            }

            $scanned++;
            if ( ! empty( $read['updated'] ) ) {
                $repaired++;
            }
        }

        foreach ( (array) $this->list_memory_documents( 200 ) as $record ) {
            if ( ! is_array( $record ) || empty( $record['path'] ) ) {
                continue;
            }

            $path = (string) $record['path'];
            $read = Niwa_AI_Text_Normalizer::normalize_file_contents( $path );
            if ( empty( $read['success'] ) ) {
                continue;
            }

            $scanned++;
            if ( ! empty( $read['updated'] ) ) {
                $repaired++;
            }
        }

        $this->memory_text_repair_ran = true;

        return array(
            'success'  => true,
            'cached'   => false,
            'scanned'  => $scanned,
            'repaired' => $repaired,
        );
    }

    public function is_heartbeat_effectively_empty() {
        $record = $this->read_bootstrap_file( 'HEARTBEAT.md' );
        if ( empty( $record['success'] ) ) {
            return true;
        }

        $content = preg_replace( '/^\s*#+.*$/m', '', (string) $record['content'] );
        $content = preg_replace( '/^\s*[-*]\s*$/m', '', (string) $content );
        $content = trim( (string) $content );

        return '' === $content;
    }

    public function read_session_manifest( $session_id ) {
        $path = $this->get_session_manifest_path( $session_id );
        if ( ! file_exists( $path ) ) {
            return array();
        }

        $raw = file_get_contents( $path );
        if ( false === $raw ) {
            return array();
        }

        $decoded = json_decode( (string) $raw, true );
        return is_array( $decoded ) ? $decoded : array();
    }

    public function write_session_manifest( $session_id, $manifest ) {
        $path     = $this->get_session_manifest_path( $session_id );
        $manifest = is_array( $manifest ) ? $manifest : array();
        $json     = wp_json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
        if ( ! is_string( $json ) || '' === $json ) {
            return false;
        }

        $dir      = dirname( $path );
        $tmp_path = trailingslashit( $dir ) . wp_basename( $path ) . '.tmp-' . wp_generate_uuid4();
        $saved    = $this->write_private_file( $tmp_path, $json, LOCK_EX );
        if ( false === $saved ) {
            return false;
        }

        // Atomic replace: rename() atomically replaces the destination on POSIX.
        // Never unlink() first - that opens a window where the state file is missing
        // and concurrent readers/writers lose the queue (causing re-processed messages).
        if ( ! @rename( $tmp_path, $path ) ) {
            if ( file_exists( $path ) ) {
                @unlink( $path );
            }
            if ( ! @rename( $tmp_path, $path ) ) {
                @unlink( $tmp_path );
                return false;
            }
        }

        return true;
    }

    /**
     * Default rotation threshold: rotate when the transcript file grows
     * past this many bytes. Filterable via `niwa_ai_session_transcript_max_bytes`.
     */
    const SESSION_TRANSCRIPT_MAX_BYTES = 1_500_000;

    /**
     * Default number of newest lines to retain after rotation. Filterable
     * via `niwa_ai_session_transcript_keep_lines`. Older lines are archived
     * under `sessions/.archive/<stem>.<UTC-timestamp>.jsonl` so they remain
     * recoverable but stop slowing down read-end-of-file calls.
     */
    const SESSION_TRANSCRIPT_KEEP_LINES = 200;

    public function append_session_transcript( $session_id, $entry ) {
        $path  = $this->get_session_transcript_path( $session_id );
        $entry = is_array( $entry ) ? $entry : array();
        if ( empty( $entry['session_id'] ) ) {
            $entry['session_id'] = (string) $session_id;
        }
        $line  = wp_json_encode( $entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
        if ( ! is_string( $line ) || '' === $line ) {
            return false;
        }

        // Keep one cross-process lock through append and rotation. Locking the
        // target only for append leaves a window where rotation can replace a
        // parallel writer's new line with a stale tail.
        $lock   = $this->open_private_file( $path . '.lock', 'c' );
        $locked = is_resource( $lock ) && @flock( $lock, LOCK_EX );
        $ok     = false !== $this->write_private_file( $path, $line . "\n", FILE_APPEND | LOCK_EX );

        if ( $ok && $locked ) {
            // After every successful append, check whether the transcript has
            // grown past the rotation threshold. filesize() is O(1) so this
            // adds a single stat() call per append — negligible — and only
            // when the threshold is exceeded do we pay the full read+rewrite
            // cost (which itself is rare, typically every few weeks of
            // active chat). This is what stops the Niwa session files from
            // growing to 7+ MB the way `admin-telegram-7499078809-…jsonl`
            // did on grubinshowroom.rs before manual rotation.
            $this->maybe_rotate_session_transcript( $session_id, $path );
        }

        if ( $locked ) {
            @flock( $lock, LOCK_UN );
        }
        if ( is_resource( $lock ) ) {
            @fclose( $lock );
        }

        return $ok;
    }

    /**
     * If the session transcript file at $path has exceeded the byte
     * threshold, archive the lines removed from the active file under
     * `sessions/.archive/` and atomic-replace the original with only the
     * last N lines. Safe to call on every append because the cheap
     * filesize check short-circuits in the common case.
     *
     * @param string $session_id Niwa session id.
     * @param string $path Absolute path to the transcript .jsonl file.
     * @return void
     */
    private function maybe_rotate_session_transcript( $session_id, $path ) {
        $max_bytes = (int) apply_filters(
            'niwa_ai_session_transcript_max_bytes',
            self::SESSION_TRANSCRIPT_MAX_BYTES,
            $session_id
        );
        if ( $max_bytes <= 0 ) {
            return; // rotation disabled via filter
        }

        clearstatcache( true, $path );
        if ( ! is_file( $path ) ) {
            return;
        }

        $size = filesize( $path );
        if ( false === $size || $size < $max_bytes ) {
            return; // common case — no work
        }

        $keep_lines = (int) apply_filters(
            'niwa_ai_session_transcript_keep_lines',
            self::SESSION_TRANSCRIPT_KEEP_LINES,
            $session_id
        );
        if ( $keep_lines < 50 ) {
            $keep_lines = 50; // sanity floor — never trim below 50 lines of recent context
        }

        $lines = @file( $path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
        if ( ! is_array( $lines ) ) {
            return; // unreadable file — bail rather than risk truncating
        }

        $total = count( $lines );
        if ( $total <= $keep_lines ) {
            return; // file is big in bytes but still few lines (huge entries) — leave alone
        }

        // Locate the .archive directory under sessions/.
        $paths = $this->ensure_workspace();
        if ( empty( $paths['sessions'] ) ) {
            return;
        }
        $archive_dir = trailingslashit( $paths['sessions'] ) . '.archive';
        if ( ! $this->ensure_private_directory( $archive_dir ) ) {
            return;
        }

        $stem          = $this->build_session_file_stem( $session_id );
        $archive_clock = microtime( true );
        $microseconds  = (int) floor( ( $archive_clock - floor( $archive_clock ) ) * 1000000 );
        $archive_path  = trailingslashit( $archive_dir )
            . $stem
            . '.'
            . gmdate( 'Ymd-His', (int) floor( $archive_clock ) )
            . '-'
            . sprintf( '%06d', $microseconds )
            . '-'
            . str_replace( '-', '', wp_generate_uuid4() )
            . '.jsonl';

        // Archive only the lines removed from the active tail. Older builds
        // copied the whole file and repeated the retained window in every
        // archive, which inflated recall results with overlap duplicates.
        $archive     = array_slice( $lines, 0, $total - $keep_lines );
        $tail        = array_slice( $lines, -$keep_lines );
        $archive_content = implode( "\n", $archive ) . "\n";
        $new_content = implode( "\n", $tail ) . "\n";
		if ( empty( $archive ) || false === $this->write_private_file( $archive_path, $archive_content, LOCK_EX ) ) {
			return;
		}

        $tmp_path = $path . '.tmp-' . wp_generate_uuid4();
        if ( false === $this->write_private_file( $tmp_path, $new_content, LOCK_EX ) ) {
			@unlink( $archive_path );
            return;
        }

        if ( ! @rename( $tmp_path, $path ) ) {
            @unlink( $tmp_path );
			@unlink( $archive_path );
            return;
        }

        clearstatcache( true, $path );

        do_action( 'niwa_ai_session_transcript_rotated', $session_id, $archive_path, $total, $keep_lines );
    }

    public function read_session_actor_state( $session_id ) {
        $path = $this->get_session_actor_state_path( $session_id );
        if ( ! file_exists( $path ) ) {
            return array();
        }

        $raw = file_get_contents( $path );
        if ( false === $raw ) {
            return array();
        }

        $decoded = json_decode( (string) $raw, true );
        return is_array( $decoded ) ? $decoded : array();
    }

    public function write_session_actor_state( $session_id, $state ) {
        $path  = $this->get_session_actor_state_path( $session_id );
        $state = is_array( $state ) ? $state : array();
        $json  = wp_json_encode( $state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
        if ( ! is_string( $json ) || '' === $json ) {
            return false;
        }

        $dir      = dirname( $path );
        $tmp_path = trailingslashit( $dir ) . wp_basename( $path ) . '.tmp-' . wp_generate_uuid4();
        $saved    = $this->write_private_file( $tmp_path, $json, LOCK_EX );
        if ( false === $saved ) {
            return false;
        }

        // Atomic replace: rename() atomically replaces the destination on POSIX.
        // Never unlink() first - that opens a window where the state file is missing
        // and concurrent readers/writers lose the queue (causing re-processed messages).
        if ( ! @rename( $tmp_path, $path ) ) {
            if ( file_exists( $path ) ) {
                @unlink( $path );
            }
            if ( ! @rename( $tmp_path, $path ) ) {
                @unlink( $tmp_path );
                return false;
            }
        }

        return true;
    }

    public function read_session_transcript( $session_id, $limit = 40 ) {
        $path = $this->get_session_transcript_path( $session_id );
        if ( ! is_file( $path ) ) {
            return array();
        }

        $limit = max( 1, min( 200, intval( $limit ) ) );
        $lines = $this->read_session_transcript_tail_lines( $path, $limit );
        if ( empty( $lines ) ) {
            return array();
        }

        $records = array();
        foreach ( $lines as $line ) {
            $decoded = json_decode( (string) $line, true );
            if ( is_array( $decoded ) ) {
                $records[] = $decoded;
            }
        }

        return $records;
    }

	/**
	 * Read conversation records by dialogue count, not by raw event count.
	 *
	 * Tool calls, task-budget updates, product-context records, and other
	 * internal events can be arbitrarily numerous inside one turn. They must
	 * never evict the adjacent founder/assistant exchange from runtime history.
	 */
	public function read_session_dialogue_transcript( $session_id, $limit = 80 ) {
		$path = $this->get_session_transcript_path( $session_id );
		if ( ! class_exists( 'Niwa_AI_Admin_Autonomy_Transcript_Reader' ) ) {
			return array();
		}

		$limit          = max( 1, min( 600, intval( $limit ) ) );
		$active_records = Niwa_AI_Admin_Autonomy_Transcript_Reader::read_tail_records(
			$path,
			array( 'turn_start', 'turn_end' ),
			$limit
		);
		$records = array();
		$seen    = array();
		foreach ( $active_records as $record ) {
			$key = $this->session_dialogue_record_identity( $record );
			if ( isset( $seen[ $key ] ) ) {
				continue;
			}
			$seen[ $key ] = true;
			$records[]    = $record;
		}
		if ( count( $records ) >= $limit ) {
			return array_slice( $records, -$limit );
		}

		$archive_dir = trailingslashit( dirname( $path ) ) . '.archive';
		$stem        = $this->build_session_file_stem( $session_id );
		$archives    = is_dir( $archive_dir )
			? ( glob( trailingslashit( $archive_dir ) . $stem . '.*.jsonl' ) ?: array() )
			: array();
		usort(
			$archives,
			function ( $left, $right ) {
				$left_key  = $this->session_archive_chronology_key( $left );
				$right_key = $this->session_archive_chronology_key( $right );
				return $right_key <=> $left_key;
			}
		);
		$max_files = max(
			1,
			min(
				1000,
				intval( apply_filters( 'niwa_ai_session_dialogue_archive_max_files', 512, $session_id ) )
			)
		);
		$max_bytes = max(
			1024 * 1024,
			min(
				128 * 1024 * 1024,
				intval( apply_filters( 'niwa_ai_session_dialogue_archive_max_bytes', 32 * 1024 * 1024, $session_id ) )
			)
		);
		$scanned_bytes = 0;

		foreach ( array_slice( $archives, 0, $max_files ) as $archive_path ) {
			if ( count( $records ) >= $limit ) {
				break;
			}
			$archive_bytes = @filesize( $archive_path );
			$archive_bytes = false === $archive_bytes ? 0 : max( 0, intval( $archive_bytes ) );
			if ( $scanned_bytes > 0 && $scanned_bytes + $archive_bytes > $max_bytes ) {
				break;
			}
			$scanned_bytes += $archive_bytes;
			$older = Niwa_AI_Admin_Autonomy_Transcript_Reader::read_tail_records(
				$archive_path,
				array( 'turn_start', 'turn_end' ),
				800
			);
			if ( ! empty( $older ) ) {
				$unique_older = array();
				foreach ( $older as $record ) {
					$key = $this->session_dialogue_record_identity( $record );
					if ( isset( $seen[ $key ] ) ) {
						continue;
					}
					$seen[ $key ]   = true;
					$unique_older[] = $record;
				}
				if ( ! empty( $unique_older ) ) {
					$records = array_merge( $unique_older, $records );
				}
			}
		}

		return array_slice( $records, -$limit );
	}

	private function session_dialogue_record_identity( $record ) {
		$record  = is_array( $record ) ? $record : array();
		$type    = sanitize_key( (string) ( $record['type'] ?? '' ) );
		$turn_id = trim( (string) ( $record['turn_id'] ?? '' ) );
		if ( '' !== $type && '' !== $turn_id ) {
			return $type . ':turn:' . $turn_id;
		}

		$payload = array(
			'type'    => $type,
			'prompt'  => (string) ( $record['prompt'] ?? '' ),
			'reply'   => (string) ( $record['reply'] ?? '' ),
			'ts'      => (string) ( $record['ts'] ?? '' ),
			'channel' => (string) ( $record['meta']['channel'] ?? '' ),
		);
		$encoded = wp_json_encode( $payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
		return 'record:' . hash( 'sha256', is_string( $encoded ) ? $encoded : serialize( $payload ) );
	}

	private function session_archive_chronology_key( $path ) {
		$name          = wp_basename( (string) $path );
		$timestamp     = 0;
		$microseconds  = -1;
		if ( preg_match( '/\.(\d{8}-\d{6})(?:-(\d{6})-[a-f0-9]{32}|-[a-f0-9]{32})\.jsonl$/i', $name, $matches ) ) {
			$parsed = strtotime( str_replace( '-', ' ', (string) $matches[1] ) . ' UTC' );
			$timestamp = false === $parsed ? 0 : intval( $parsed );
			if ( isset( $matches[2] ) && '' !== $matches[2] ) {
				$microseconds = intval( $matches[2] );
			}
		}
		$mtime = @filemtime( $path );
		$inode = @fileinode( $path );

		return array(
			$timestamp,
			$microseconds,
			false === $mtime ? 0 : intval( $mtime ),
			false === $inode ? 0 : intval( $inode ),
			$name,
		);
	}

    public function has_session_transcript_message( $session_id, $role, $text, $correlations = array() ) {
        $path         = $this->get_session_transcript_path( $session_id );
        $role         = sanitize_key( (string) $role );
        $text         = (string) $text;
        $correlations = is_array( $correlations ) ? array_filter( array_map( 'strval', $correlations ) ) : array();
        if ( ! is_file( $path ) || '' === $text || empty( $correlations ) ) {
            return false;
        }

        $max_records = max( 100, min( 20000, intval( apply_filters( 'niwa_ai_transcript_correlation_scan_records', 5000, $session_id ) ) ) );
        $max_bytes   = max( 65536, min( 32 * 1024 * 1024, intval( apply_filters( 'niwa_ai_transcript_correlation_scan_bytes', 8 * 1024 * 1024, $session_id ) ) ) );
        $lines       = array_reverse( $this->read_session_transcript_tail_lines( $path, $max_records, $max_bytes ) );
        foreach ( $lines as $line ) {
            $record = json_decode( (string) $line, true );
            if ( ! is_array( $record ) ) {
                continue;
            }
            $meta    = isset( $record['meta'] ) && is_array( $record['meta'] ) ? $record['meta'] : array();
            $matched = ! empty( $correlations['turn_id'] ) && $correlations['turn_id'] === (string) ( $record['turn_id'] ?? '' );
            foreach ( array( 'request_id', 'admin_ui_job_id', 'actor_message_id', 'message_id' ) as $key ) {
                if ( ! $matched && ! empty( $correlations[ $key ] ) && $correlations[ $key ] === (string) ( $meta[ $key ] ?? '' ) ) {
                    $matched = true;
                }
            }
            if ( ! $matched ) {
                continue;
            }
			if ( ! empty( $correlations['delivery_committed'] ) && empty( $meta['delivery_committed'] ) ) {
				continue;
			}
            if ( 'user' === $role && 'turn_start' === (string) ( $record['type'] ?? '' ) && $text === (string) ( $record['prompt'] ?? '' ) ) {
                return true;
            }
            if ( in_array( $role, array( 'assistant', 'bot' ), true ) && 'turn_end' === (string) ( $record['type'] ?? '' ) && $text === (string) ( $record['reply'] ?? '' ) ) {
                return true;
            }
        }

        return false;
    }

    private function read_session_transcript_tail_lines( $path, $limit, $max_bytes = 0 ) {
        $limit = max( 1, intval( $limit ) );
        $size  = filesize( $path );
        if ( false === $size || $size < 1 ) {
            return array();
        }

        $handle = @fopen( $path, 'rb' );
        if ( ! $handle ) {
            return array();
        }

        $chunk_size = 8192;
        $position   = $size;
        $buffer     = '';
        $line_count = 0;
		$read_total = 0;
		$max_bytes  = max( 0, intval( $max_bytes ) );

        while ( $position > 0 && $line_count <= $limit && ( 0 === $max_bytes || $read_total < $max_bytes ) ) {
            $read_bytes = min( $chunk_size, $position );
			if ( $max_bytes > 0 ) {
				$read_bytes = min( $read_bytes, $max_bytes - $read_total );
			}
			if ( $read_bytes <= 0 ) {
				break;
			}
            $position  -= $read_bytes;

            if ( 0 !== fseek( $handle, $position ) ) {
                break;
            }

            $chunk = fread( $handle, $read_bytes );
            if ( false === $chunk || '' === $chunk ) {
                break;
            }

            $buffer     = $chunk . $buffer;
            $line_count = substr_count( $buffer, "\n" );
			$read_total += $read_bytes;
        }

        fclose( $handle );

        if ( '' === $buffer ) {
            return array();
        }

        $lines = preg_split( "/\r\n|\n|\r/", $buffer );
        if ( ! is_array( $lines ) ) {
            return array();
        }

        $lines = array_values(
            array_filter(
                $lines,
                function ( $line ) {
                    return '' !== trim( (string) $line );
                }
            )
        );

        return array_slice( $lines, -$limit );
    }

    public function list_session_manifests( $limit = 20 ) {
        $paths = $this->ensure_workspace();
        $files = glob( trailingslashit( $paths['sessions'] ) . '*.json' );
        if ( ! is_array( $files ) ) {
            return array();
        }

        $records = array();
        foreach ( $files as $path ) {
            if ( substr( $path, -6 ) === '.jsonl' || ! is_file( $path ) ) {
                continue;
            }

            $raw = file_get_contents( $path );
            if ( false === $raw ) {
                continue;
            }
            $decoded = json_decode( (string) $raw, true );
            if ( is_array( $decoded ) ) {
                $records[] = $decoded;
            }
        }

        usort(
            $records,
            function ( $a, $b ) {
                return intval( isset( $b['updated_at'] ) ? $b['updated_at'] : 0 ) <=> intval( isset( $a['updated_at'] ) ? $a['updated_at'] : 0 );
            }
        );

        return array_slice( $records, 0, max( 1, min( 50, intval( $limit ) ) ) );
    }

    public function list_session_actor_states( $limit = 20 ) {
        $paths = $this->ensure_workspace();
        $files = glob( trailingslashit( $paths['sessions'] ) . '*.actor.json' );
        if ( ! is_array( $files ) ) {
            return array();
        }

        $records = array();
        foreach ( $files as $path ) {
            if ( ! is_file( $path ) ) {
                continue;
            }

            $raw = file_get_contents( $path );
            if ( false === $raw ) {
                continue;
            }

            $decoded = json_decode( (string) $raw, true );
            if ( is_array( $decoded ) ) {
                $records[] = $decoded;
            }
        }

        usort(
            $records,
            function ( $a, $b ) {
                $rank_a = $this->actor_state_runtime_priority_rank( $a );
                $rank_b = $this->actor_state_runtime_priority_rank( $b );
                if ( $rank_a !== $rank_b ) {
                    return $rank_a <=> $rank_b;
                }

                if ( 0 === $rank_a ) {
                    $oldest_a = $this->actor_state_oldest_pending_at( $a );
                    $oldest_b = $this->actor_state_oldest_pending_at( $b );
                    if ( $oldest_a !== $oldest_b ) {
                        return $oldest_a <=> $oldest_b;
                    }
                }

                if ( 1 === $rank_a ) {
                    $lease_a = ! empty( $a['active_message']['lease_expires_at'] ) ? intval( $a['active_message']['lease_expires_at'] ) : 0;
                    $lease_b = ! empty( $b['active_message']['lease_expires_at'] ) ? intval( $b['active_message']['lease_expires_at'] ) : 0;
                    if ( $lease_a !== $lease_b ) {
                        return $lease_a <=> $lease_b;
                    }
                }

                $updated_a = intval( isset( $a['updated_at'] ) ? $a['updated_at'] : 0 );
                $updated_b = intval( isset( $b['updated_at'] ) ? $b['updated_at'] : 0 );
                return $updated_b <=> $updated_a;
            }
        );

        return array_slice( $records, 0, max( 1, min( 50, intval( $limit ) ) ) );
    }

    private function actor_state_runtime_priority_rank( $state ) {
        $state = is_array( $state ) ? $state : array();
        if ( ! empty( $state['pending_messages'] ) && is_array( $state['pending_messages'] ) ) {
            return 0;
        }

        if ( ! empty( $state['active_message'] ) && is_array( $state['active_message'] ) ) {
            return 1;
        }

        return 2;
    }

    private function actor_state_oldest_pending_at( $state ) {
        $state    = is_array( $state ) ? $state : array();
        $messages = isset( $state['pending_messages'] ) && is_array( $state['pending_messages'] ) ? $state['pending_messages'] : array();
        $oldest   = 0;
        foreach ( $messages as $message ) {
            if ( ! is_array( $message ) ) {
                continue;
            }

            $enqueued_at = intval( isset( $message['enqueued_at'] ) ? $message['enqueued_at'] : 0 );
            if ( $enqueued_at <= 0 ) {
                continue;
            }

            if ( 0 === $oldest || $enqueued_at < $oldest ) {
                $oldest = $enqueued_at;
            }
        }

        return $oldest > 0 ? $oldest : intval( isset( $state['updated_at'] ) ? $state['updated_at'] : 0 );
    }

    public function write_json_artifact( $filename, $data, $subdir = 'snapshots' ) {
        $paths   = $this->ensure_workspace();
        $key     = 'reports' === $subdir ? 'reports' : 'snapshots';
        $url_key = 'reports' === $subdir ? 'reports_url' : 'snapshots_url';
        $path    = trailingslashit( $paths[ $key ] ) . sanitize_file_name( $filename );
        $saved   = $this->write_private_file( $path, wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );

        if ( false === $saved ) {
            return array(
                'success' => false,
                'error'   => 'Workspace JSON write failed.',
            );
        }

        return array(
            'success' => true,
            'path'    => $path,
            'url'     => ! empty( $paths[ $url_key ] ) ? trailingslashit( $paths[ $url_key ] ) . basename( $path ) : '',
        );
    }

    public function export_orders_xlsx( $period = 'yesterday' ) {
        $period = trim( strtolower( (string) $period ) );
        if ( 'all_time' === $period ) {
            return $this->export_orders_xlsx_all_time( 'all-time' );
        }

        $range = $this->resolve_period_range( $period );
        return $this->export_orders_xlsx_window( $range['start'], $range['end'], $range['label'] );
    }

    public function export_orders_xlsx_all_time( $label = 'all-time' ) {
        return $this->export_orders_xlsx_window( '', '', $label );
    }

    public function export_orders_xlsx_window( $start_local_mysql, $end_local_mysql, $label = 'custom' ) {
        if ( ! class_exists( 'WooCommerce' ) || ! function_exists( 'wc_get_orders' ) ) {
            return array(
                'success' => false,
                'error'   => 'WooCommerce nije dostupan za XLSX export.',
            );
        }

        if ( ! class_exists( 'XLSXWriter' ) ) {
            return array(
                'success' => false,
                'error'   => 'XLSX writer biblioteka nije ucitana.',
            );
        }

        $start_local_mysql = trim( (string) $start_local_mysql );
        $end_local_mysql   = trim( (string) $end_local_mysql );
        $is_all_time       = '' === $start_local_mysql && '' === $end_local_mysql;
        if ( ! $is_all_time && ( '' === $start_local_mysql || '' === $end_local_mysql ) ) {
            return array(
                'success' => false,
                'error'   => 'Pocetak i kraj opsega su obavezni za XLSX export.',
            );
        }

        $paths  = $this->ensure_workspace();
        $range  = array(
            'start' => $is_all_time ? 'beginning' : $start_local_mysql,
            'end'   => $is_all_time ? 'now' : $end_local_mysql,
            'label' => sanitize_title( (string) $label ),
        );
        $query  = array(
            'status' => array( 'wc-processing', 'wc-completed', 'wc-on-hold' ),
            'limit'  => -1,
            'return' => 'objects',
        );
        if ( ! $is_all_time ) {
            $query['date_created'] = $start_local_mysql . '...' . $end_local_mysql;
        }
        $orders = wc_get_orders( $query );
        if ( ! is_array( $orders ) ) {
            $orders = array();
        }

        $filename  = 'sales-' . sanitize_file_name( $range['label'] ) . '-' . gmdate( 'Ymd-His' ) . '.xlsx';
        $file_path = trailingslashit( $paths['reports'] ) . $filename;
        $file_url  = ! empty( $paths['reports_url'] ) ? trailingslashit( $paths['reports_url'] ) . $filename : '';
        $writer    = new XLSXWriter();

        $writer->writeSheetHeader(
            'Orders',
            array(
                'Order ID'        => 'string',
                'Created At'      => 'string',
                'Status'          => 'string',
                'Customer Name'   => 'string',
                'Customer Email'  => 'string',
                'Phone'           => 'string',
                'Billing City'    => 'string',
                'Shipping Name'   => 'string',
                'Shipping Address' => 'string',
                'Shipping City'   => 'string',
                'Shipping Postcode' => 'string',
                'Shipping Country' => 'string',
                'Items'           => 'integer',
                'Order Total'     => 'price',
                'Discount Total'  => 'price',
                'Coupons'         => 'string',
                'Payment Method'  => 'string',
            )
        );

        $writer->writeSheetHeader(
            'Items',
            array(
                'Order ID'      => 'string',
                'Created At'    => 'string',
                'Customer Name' => 'string',
                'Customer Email' => 'string',
                'Shipping City' => 'string',
                'Shipping Address' => 'string',
                'Product ID'    => 'string',
                'Variation ID'  => 'string',
                'SKU'           => 'string',
                'Product Name'  => 'string',
                'Quantity'      => 'integer',
                'Unit Price'    => 'price',
                'Line Total'    => 'price',
            )
        );

        $writer->writeSheetHeader(
            'Summary',
            array(
                'Metric' => 'string',
                'Value'  => 'string',
            )
        );

        $order_count = 0;
        $revenue     = 0.0;
        $item_count  = 0;

        foreach ( $orders as $order ) {
            if ( ! is_object( $order ) ) {
                continue;
            }

            $order_count++;
            $revenue += floatval( $order->get_total() );
            $items = $order->get_items();
            $items_total = 0;
            $created_at = $order->get_date_created() ? $order->get_date_created()->date_i18n( 'Y-m-d H:i:s' ) : '';
            $shipping_name = trim( $order->get_formatted_shipping_full_name() );
            $shipping_address = trim(
                implode(
                    ', ',
                    array_filter(
                        array(
                            trim( (string) $order->get_shipping_address_1() ),
                            trim( (string) $order->get_shipping_address_2() ),
                        )
                    )
                )
            );
            foreach ( $items as $item ) {
                if ( is_object( $item ) && method_exists( $item, 'get_quantity' ) ) {
                    $qty = intval( $item->get_quantity() );
                    $items_total += $qty;
                    $item_count  += $qty;
                    $unit_price = $qty > 0 ? floatval( $item->get_total() ) / $qty : floatval( $item->get_total() );
                    $product    = method_exists( $item, 'get_product' ) ? $item->get_product() : null;
                    $writer->writeSheetRow(
                        'Items',
                        array(
                            '#' . $order->get_id(),
                            $created_at,
                            trim( $order->get_formatted_billing_full_name() ),
                            (string) $order->get_billing_email(),
                            (string) $order->get_shipping_city(),
                            $shipping_address,
                            (string) $item->get_product_id(),
                            (string) $item->get_variation_id(),
                            ( $product && method_exists( $product, 'get_sku' ) ) ? (string) $product->get_sku() : '',
                            (string) $item->get_name(),
                            $qty,
                            (float) $unit_price,
                            (float) $item->get_total(),
                        )
                    );
                }
            }

            $writer->writeSheetRow(
                'Orders',
                array(
                    '#' . $order->get_id(),
                    $created_at,
                    $order->get_status(),
                    trim( $order->get_formatted_billing_full_name() ),
                    (string) $order->get_billing_email(),
                    (string) $order->get_billing_phone(),
                    (string) $order->get_billing_city(),
                    $shipping_name,
                    $shipping_address,
                    (string) $order->get_shipping_city(),
                    (string) $order->get_shipping_postcode(),
                    (string) $order->get_shipping_country(),
                    $items_total,
                    (float) $order->get_total(),
                    (float) $order->get_discount_total(),
                    implode( ', ', $order->get_coupon_codes() ),
                    (string) $order->get_payment_method_title(),
                )
            );
        }

        $writer->writeSheetRow( 'Summary', array( 'Period', $range['label'] ) );
        $writer->writeSheetRow( 'Summary', array( 'Start', $range['start'] ) );
        $writer->writeSheetRow( 'Summary', array( 'End', $range['end'] ) );
        $writer->writeSheetRow( 'Summary', array( 'Orders', (string) $order_count ) );
        $writer->writeSheetRow( 'Summary', array( 'Revenue', (string) $revenue ) );
        $writer->writeSheetRow( 'Summary', array( 'Items sold', (string) $item_count ) );

        $writer->writeToFile( $file_path );

        return array(
            'success'      => true,
            'path'         => $file_path,
            'url'          => $file_url,
            'filename'     => $filename,
            'orders'       => $order_count,
            'revenue'      => round( $revenue, 2 ),
            'items_sold'   => $item_count,
            'period_label' => $range['label'],
            'start'        => $range['start'],
            'end'          => $range['end'],
        );
    }

    public function list_artifacts( $subdir = 'reports', $limit = 20 ) {
        $paths = $this->ensure_workspace();
        $dir   = 'snapshots' === $subdir ? $paths['snapshots'] : $paths['reports'];
        $url   = 'snapshots' === $subdir ? $paths['snapshots_url'] : $paths['reports_url'];
        $limit = max( 1, min( 50, intval( $limit ) ) );

        $files = array();
        foreach ( glob( trailingslashit( $dir ) . '*' ) ?: array() as $path ) {
            if ( ! is_file( $path ) ) {
                continue;
            }
            $files[] = array(
                'name'     => basename( $path ),
                'path'     => $path,
                'url'      => '' !== $url ? trailingslashit( $url ) . basename( $path ) : '',
                'size'     => filesize( $path ),
                'modified' => filemtime( $path ),
            );
        }

        usort(
            $files,
            function ( $a, $b ) {
                return intval( $b['modified'] ) <=> intval( $a['modified'] );
            }
        );

        return array_slice( $files, 0, $limit );
    }

    public function get_session_manifest_path( $session_id ) {
        $paths = $this->ensure_workspace();
        return trailingslashit( $paths['sessions'] ) . $this->build_session_file_stem( $session_id ) . '.json';
    }

    public function get_session_actor_state_path( $session_id ) {
        $paths = $this->ensure_workspace();
        return trailingslashit( $paths['sessions'] ) . $this->build_session_file_stem( $session_id ) . '.actor.json';
    }

    public function get_session_transcript_path( $session_id ) {
        $paths = $this->ensure_workspace();
        return trailingslashit( $paths['sessions'] ) . $this->build_session_file_stem( $session_id ) . '.jsonl';
    }

    private function build_session_file_stem( $session_id ) {
        $session_id = trim( (string) $session_id );
        $label      = sanitize_file_name( preg_replace( '/[^a-z0-9:_-]+/i', '-', strtolower( $session_id ) ) );
        $label      = trim( (string) $label, '-._' );
        if ( '' === $label ) {
            $label = 'session';
        }

        if ( strlen( $label ) > 48 ) {
            $label = substr( $label, 0, 48 );
        }

        return $label . '-' . substr( md5( $session_id ), 0, 12 );
    }

    private function normalize_bootstrap_filename( $filename ) {
        $filename = trim( (string) $filename );
        foreach ( self::BOOTSTRAP_FILES as $allowed ) {
            if ( 0 === strcasecmp( $allowed, $filename ) ) {
                return $allowed;
            }
        }

        return '';
    }

    private function get_default_bootstrap_content( $filename ) {
        switch ( $filename ) {
            case 'AGENTS.md':
                return "# AGENTS.md\n\nYou are the founder-facing WordPress operating agent inside this website. Work directly, verify before claiming facts, and prefer tool execution over abstract advice when the founder asked for a concrete change.\n";
            case 'SOUL.md':
                return $this->build_identity_bootstrap_content( 'SOUL.md', $this->get_default_identity_profile() );
            case 'TOOLS.md':
                return "# TOOLS.md\n\nPrimary capabilities:\n- inspect WordPress code, content, plugins, schema, and WooCommerce catalog\n- execute concrete site, content, product, variation, and file changes\n- manage internal subagents and session worklogs\n- export artifacts inside this workspace\n";
            case 'IDENTITY.md':
                return $this->build_identity_bootstrap_content( 'IDENTITY.md', $this->get_default_identity_profile() );
            case 'USER.md':
                return $this->build_identity_bootstrap_content( 'USER.md', $this->get_default_identity_profile() );
            case 'HEARTBEAT.md':
                return implode(
                    "\n",
                    array(
                        '# HEARTBEAT.md',
                        '',
                        'proactive_improvement_mode: true',
                        '',
                        'This heartbeat runs in proactive improvement mode.',
                        'Niwa runs at fixed times: 9h, 13h, 17h, 20h (4x/day by default).',
                        'This is the default proactive heartbeat, not the operative continuation runner.',
                        'Founder-requested recurring work belongs in OPERATIVE_HEARTBEAT.md; stock-manager recurring catalog work belongs in STOCK_OPERATIVE_HEARTBEAT.md.',
						'Each time, Niwa picks a random improvement task supported by the current site operating profile:',
						'',
						'- Blog post writing (1x/day max, created as draft for founder approval)',
						'- SEO and accessibility image audit on important site content',
						'- Competitive landscape analysis and positioning',
						'- Content gap analysis (missing blog topics and keywords)',
						'- Marketing and traffic optimization insights',
						'- Customer FAQ and Q&A section suggestions',
						'- Site conversion-goal optimization tips',
						'- Product description review only when a product catalog exists',
						'- Sales, bestseller, order, revenue, inventory, cart, and checkout work only when transactional commerce is enabled',
                        '',
                        'If Niwa has nothing genuinely useful or interesting to report, she stays silent (no message sent).',
                        '',
                        'The founder can change heartbeat settings through chat:',
                        '- Change schedule: "postavi heartbeat na 8h, 14h, 20h"',
                        '- Pause/resume: "pauziraj heartbeat" / "nastavi heartbeat"',
                        '- Blog frequency: "pisi blog 2x dnevno" / "ne pisi blogove"',
                        '',
                        'To override with custom heartbeat instructions, remove the proactive_improvement_mode line',
                        'and write your own instructions below.',
                        '',
                    )
                );
            case 'OPERATIVE_HEARTBEAT.md':
                return implode(
                    "\n",
                    array(
                        '# OPERATIVE_HEARTBEAT.md',
                        '',
                        '## Operative Heartbeat Tasks',
                        '',
                        'This file is the operative heartbeat operating brief.',
                        'It can describe recurring automation and long-running continuation tasks.',
                        'Use it when a founder task needs more than one run, checkpoint, or loop to finish.',
                        '',
                        'For recurring work, define what should happen on each tick and what the trigger condition is.',
                        'For continuation work, define the current checkpoint, the next step, and the stop condition.',
                        'For backlog-drain and catalog work, use 0.5-1 minute cadence when fast continuation matters; otherwise 3 minutes stays the conservative default.',
                        'Do not use this file as a generic scheduler for unrelated recurring jobs.',
                        '',
                        'The AI agent reads these instructions each tick, uses the current date/time, and decides what is relevant now.',
                        'If the task spans multiple runs, preserve the latest checkpoint here so the next tick can continue without the founder repeating the whole brief.',
                        '',
                        'Safe activation rule:',
                        '- only enable the operative heartbeat when the founder explicitly wants recurring execution or heartbeat-driven continuation',
                        '- do not invent extra repeating jobs',
                        '- keep scope limited to the named founder task',
                        '- stop when the stop condition is met or the task is blocked',
                        '- if a conflict or ambiguity appears, follow the fallback on conflict before making any write',
                        '',
                        'Continuation template:',
                        'Goal:',
                        'Exact targets/source:',
                        'What has been done:',
                        'Next step for the next tick:',
                        'Current checkpoint:',
                        'Safety rules:',
                        'Stop condition:',
                        'Fallback on conflict:',
                        'Relevant tools:',
                        'Notes for the next run:',
                        '',
                        'Recurring template:',
                        'Interval:',
                        'Trigger condition:',
                        'Actions on each tick:',
                        'Verification:',
                        '',
                        '<!-- No tasks configured yet. The founder can add tasks through chat. -->',
                    )
                );
            case 'STOCK_OPERATIVE_HEARTBEAT.md':
                return implode(
                    "\n",
                    array(
                        '# STOCK_OPERATIVE_HEARTBEAT.md',
                        '',
                        '## Stock Operative Heartbeat Tasks',
                        '',
                        'This file is the stock-manager operative heartbeat operating brief.',
                        'It is separate from OPERATIVE_HEARTBEAT.md and exists only for WooCommerce product, stock, pricing, outlet, and inventory continuation work.',
                        'Use it when a stock-manager task needs more than one run, checkpoint, or loop to finish.',
                        '',
                        'Do not mix founder-wide recurring jobs into this file.',
                        'Keep scope inside product/catalog work only.',
                        'For backlog-drain and catalog work, use 0.5-1 minute cadence when fast continuation matters; otherwise 3 minutes stays the conservative default.',
                        '',
                        'The stock-manager operative runner reads these instructions each tick, uses the current date/time, and decides what is relevant now.',
                        'If the task spans multiple runs, preserve the latest checkpoint here so the next tick can continue without the stock manager repeating the whole brief.',
                        '',
                        'Safe activation rule:',
                        '- only enable the stock operative heartbeat when a stock manager explicitly wants recurring execution or heartbeat-driven continuation',
                        '- do not invent extra repeating jobs',
                        '- keep scope limited to the named product/catalog task',
                        '- stop when the stop condition is met or the task is blocked',
                        '- if a conflict or ambiguity appears, follow the fallback on conflict before making any write',
                        '',
                        'Continuation template:',
                        'Goal:',
                        'Exact targets/source:',
                        'What has been done:',
                        'Next step for the next tick:',
                        'Current checkpoint:',
                        'Safety rules:',
                        'Stop condition:',
                        'Fallback on conflict:',
                        'Relevant tools:',
                        'Notes for the next run:',
                        '',
                        'Recurring template:',
                        'Interval:',
                        'Trigger condition:',
                        'Actions on each tick:',
                        'Verification:',
                        '',
                        '<!-- No stock tasks configured yet. A stock manager can add tasks through chat. -->',
                    )
                );
            case 'MEMORY.md':
                return "# MEMORY.md\n\nLong-lived project memory, durable discoveries, and site-specific instructions.\n";
        }

        return '';
    }

    private function get_default_identity_profile() {
        return array(
            'assistant_name'     => '',
            'assistant_role'     => 'personal AI assistant for this WordPress site',
            'assistant_identity' => 'Warm, direct, technically strong personal AI assistant. In Serbian, Croatian, and Bosnian always use feminine self-reference and stay in character.',
            'companion_summary'  => '',
            'founder_preferences' => array(),
            'communication_preferences' => array(),
            'communication_style' => '',
            'memory_priorities'  => array(),
            'decision_principles' => array(),
            'motivators'         => array(),
            'boundaries'         => array(),
            'diary_focus'        => array(),
            'current_focus'      => '',
            'current_focus_areas' => array(),
            'relationship_notes' => '',
            'relationship_history_summary' => '',
            'founder_name'       => '',
            'founder_address'    => '',
            'founder_profile'    => '',
            'relationship_style' => 'Close, natural relationship with the admin. No corporate tone, no generic chatbot act, no masculine self-reference.',
            'assistant_personality_facets' => array(),
            'durable_rules'      => array(
                'If the request is concrete, start working instead of asking for extra permission.',
                'For light conversation, impressions, or opinions, answer naturally without unnecessary tools.',
                'For site facts and execution, use tools and verification instead of guessing.',
            ),
            'onboarding_step'    => 'ask_founder_name',
            'completed_at'       => '',
            'updated_at'         => 0,
            'legacy_inferred'    => false,
        );
    }

    private function get_workspace_state() {
        $state = get_option( self::STATE_OPTION_KEY, array() );
        return is_array( $state ) ? $state : array();
    }

    private function save_workspace_state( $state ) {
        $state = is_array( $state ) ? $state : array();
        update_option( self::STATE_OPTION_KEY, $state, false );
        return $state;
    }

    private function merge_companion_memory_state( $current, $updates ) {
        $current = is_array( $current ) ? $current : array();
        $updates = is_array( $updates ) ? $updates : array();

        foreach ( $updates as $key => $value ) {
            $current[ $key ] = $value;
        }

        return $current;
    }

    private function get_companion_memory_directory_path() {
        $paths = $this->ensure_workspace();
        if ( empty( $paths['memory'] ) ) {
            return '';
        }

        return trailingslashit( $paths['memory'] ) . self::COMPANION_MEMORY_DIR;
    }

    private function ensure_layered_memory_files() {
        $paths = $this->get_paths();
        if ( empty( $paths['memory'] ) ) {
            return;
        }

        $memory_root     = trailingslashit( $paths['memory'] );
        $operational_dir = $memory_root . self::OPERATIONAL_MEMORY_DIR;
        $companion_dir   = $memory_root . self::COMPANION_MEMORY_DIR;

        $this->ensure_private_directory( $operational_dir );
        $this->ensure_private_directory( $companion_dir );

        $legacy_memory = trailingslashit( $paths['root'] ) . 'MEMORY.md';
        $learnings     = trailingslashit( $operational_dir ) . self::OPERATIONAL_LEARNINGS_FILE;
        if ( ! file_exists( $learnings ) ) {
            $content = "# Operational Learnings\n\nLong-lived operating lessons, durable workflow corrections, and site-specific execution discoveries.\n";
            if ( file_exists( $legacy_memory ) && is_readable( $legacy_memory ) ) {
                $legacy = Niwa_AI_Text_Normalizer::normalize_file_contents( $legacy_memory );
                if ( ! empty( $legacy['success'] ) && '' !== trim( (string) $legacy['content'] ) ) {
                    $content .= "\n## Migrated from MEMORY.md\n\n" . trim( (string) $legacy['content'] ) . "\n";
                }
            }
            $this->write_private_file( $learnings, $content, LOCK_EX );
        }

        $rules = trailingslashit( $operational_dir ) . self::OPERATIONAL_RULES_FILE;
        if ( ! file_exists( $rules ) ) {
            $this->write_private_file( $rules, "# Operational Rules\n\nBusiness, workflow, and tool-use rules that should stay always-on.\n", LOCK_EX );
        }

        $profile = $this->get_identity_profile();
        $this->ensure_companion_layered_file(
            trailingslashit( $companion_dir ) . self::COMPANION_IDENTITY_FILE,
            self::COMPANION_IDENTITY_FILE,
            array( 'SOUL.md', 'IDENTITY.md' ),
            $profile
        );
        $this->ensure_companion_layered_file(
            trailingslashit( $companion_dir ) . self::COMPANION_USER_FILE,
            self::COMPANION_USER_FILE,
            array( 'USER.md' ),
            $profile
        );
        $this->ensure_companion_layered_file(
            trailingslashit( $companion_dir ) . self::COMPANION_RELATIONSHIP_FILE,
            self::COMPANION_RELATIONSHIP_FILE,
            array( self::COMPANION_PROFILE_FILE ),
            $profile
        );
    }

    private function ensure_companion_layered_file( $path, $filename, $legacy_sources, $profile ) {
        if ( file_exists( $path ) ) {
            return;
        }

        $content = $this->build_companion_layered_file_content( $filename, $profile );
        $legacy  = $this->build_legacy_layer_excerpt( $legacy_sources );
        if ( '' !== $legacy ) {
            $content = rtrim( $content ) . "\n\n## Migrated Legacy Notes\n\n" . $legacy . "\n";
        }

        $this->write_private_file(
            $path,
            Niwa_AI_Text_Normalizer::normalize( $content, array( 'preserve_newlines' => true, 'trim' => false ) ),
            LOCK_EX
        );
    }

    private function build_legacy_layer_excerpt( $sources ) {
        $paths  = $this->get_paths();
        $chunks = array();
        foreach ( (array) $sources as $source ) {
            $source = (string) $source;
            $path   = self::COMPANION_PROFILE_FILE === $source
                ? trailingslashit( $paths['memory'] ) . self::COMPANION_MEMORY_DIR . '/' . self::COMPANION_PROFILE_FILE
                : trailingslashit( $paths['root'] ) . basename( $source );
            if ( ! file_exists( $path ) || ! is_readable( $path ) ) {
                continue;
            }

            $read = Niwa_AI_Text_Normalizer::normalize_file_contents( $path );
            if ( empty( $read['success'] ) || '' === trim( (string) $read['content'] ) ) {
                continue;
            }

            $chunks[] = 'From ' . $source . ":\n" . $this->truncate_text( trim( (string) $read['content'] ), 700 );
        }

        return implode( "\n\n", $chunks );
    }

    private function read_layered_memory_file( $dir, $filename ) {
        $paths = $this->get_paths();
        $path  = trailingslashit( $paths['memory'] ) . basename( (string) $dir ) . '/' . basename( (string) $filename );
        if ( ! file_exists( $path ) || ! is_readable( $path ) ) {
            return '';
        }

        $read = Niwa_AI_Text_Normalizer::normalize_file_contents( $path );
        return empty( $read['success'] ) ? '' : (string) $read['content'];
    }

    private function strip_markdown_title( $content ) {
        $content = trim( (string) $content );
        $content = preg_replace( '/^# .+?(?:\r?\n){1,2}/', '', $content );
        return trim( (string) $content );
    }

    private function get_companion_memory_file_path( $filename ) {
        $dir = $this->get_companion_memory_directory_path();
        $filename = basename( (string) $filename );
        if ( '' === $dir || '' === $filename ) {
            return '';
        }

        return trailingslashit( $dir ) . $filename;
    }

    private function ensure_companion_memory_file( $filename ) {
        $path = $this->get_companion_memory_file_path( $filename );
        if ( '' === $path ) {
            return '';
        }

        $dir = dirname( $path );
        if ( ! file_exists( $dir ) ) {
            $this->ensure_private_directory( $dir );
        }

        return $path;
    }

    private function get_diary_current_directory_path() {
        $paths = $this->ensure_workspace();
        if ( empty( $paths['memory'] ) ) {
            return '';
        }

        return trailingslashit( $paths['memory'] ) . self::DIARY_DIR . '/' . self::DIARY_CURRENT_DIR;
    }

    private function get_diary_archived_directory_path() {
        $paths = $this->ensure_workspace();
        if ( empty( $paths['memory'] ) ) {
            return '';
        }

        return trailingslashit( $paths['memory'] ) . self::DIARY_DIR . '/' . self::DIARY_ARCHIVED_DIR;
    }

    private function get_diary_file_path( $date ) {
        $date = $this->sanitize_short_text( $date, 20 );
        if ( '' === $date ) {
            $date = wp_date( 'Y-m-d' );
        }

        $dir = $this->get_diary_current_directory_path();
        if ( '' === $dir ) {
            return '';
        }

        return trailingslashit( $dir ) . sanitize_file_name( $date ) . '.md';
    }

    private function get_decision_log_file_path() {
        $paths = $this->ensure_workspace();
        if ( empty( $paths['memory'] ) ) {
            return '';
        }

        return trailingslashit( $paths['memory'] ) . self::DECISION_LOG_FILE;
    }

    private function append_companion_diary_file( $date, $block, $marker = '' ) {
        $this->archive_stale_diary_entries();
        $path = $this->get_diary_file_path( $date );
        if ( '' === $path ) {
            return array(
                'success' => false,
                'error'   => 'Diary path unavailable.',
            );
        }

        $dir = dirname( $path );
        if ( ! file_exists( $dir ) ) {
            $this->ensure_private_directory( $dir );
        }

        $block  = trim( Niwa_AI_Text_Normalizer::normalize( $block, array( 'preserve_newlines' => true ) ) );
        $marker = trim( (string) $marker );
        if ( '' === $block ) {
            return array(
                'success' => false,
                'error'   => 'Diary block is empty.',
            );
        }

        if ( '' !== $marker && file_exists( $path ) ) {
            $existing = Niwa_AI_Text_Normalizer::normalize_file_contents( $path );
            if ( ! empty( $existing['success'] ) && false !== strpos( (string) $existing['content'], $marker ) ) {
                return array(
                    'success'   => true,
                    'path'      => $path,
                    'duplicate' => true,
                );
            }
        }

        if ( ! file_exists( $path ) ) {
            $this->write_private_file( $path, '# Companion Diary ' . basename( $path, '.md' ) . "\n\n", LOCK_EX );
        }

        $saved = $this->write_private_file(
            $path,
            Niwa_AI_Text_Normalizer::normalize( "\n" . $block . "\n", array( 'preserve_newlines' => true ) ),
            FILE_APPEND | LOCK_EX
        );
        if ( false === $saved ) {
            return array(
                'success' => false,
                'error'   => 'Diary write failed.',
            );
        }

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

    private function append_companion_decision_file( $block, $marker = '' ) {
        $path = $this->get_decision_log_file_path();
        if ( '' === $path ) {
            return array(
                'success' => false,
                'error'   => 'Decision log path unavailable.',
            );
        }

        $block  = trim( Niwa_AI_Text_Normalizer::normalize( $block, array( 'preserve_newlines' => true ) ) );
        $marker = trim( (string) $marker );
        if ( '' === $block ) {
            return array(
                'success' => false,
                'error'   => 'Decision block is empty.',
            );
        }

        if ( '' !== $marker && file_exists( $path ) ) {
            $existing = Niwa_AI_Text_Normalizer::normalize_file_contents( $path );
            if ( ! empty( $existing['success'] ) && false !== strpos( (string) $existing['content'], $marker ) ) {
                return array(
                    'success'   => true,
                    'path'      => $path,
                    'duplicate' => true,
                );
            }
        }

        if ( ! file_exists( $path ) ) {
            $this->write_private_file( $path, "# Decision Log\n\n", LOCK_EX );
        }

        $saved = $this->write_private_file(
            $path,
            Niwa_AI_Text_Normalizer::normalize( "\n" . $block . "\n", array( 'preserve_newlines' => true ) ),
            FILE_APPEND | LOCK_EX
        );
        if ( false === $saved ) {
            return array(
                'success' => false,
                'error'   => 'Decision log write failed.',
            );
        }

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

    private function archive_stale_diary_entries() {
        $current_dir = $this->get_diary_current_directory_path();
        $archive_dir = $this->get_diary_archived_directory_path();
        if ( '' === $current_dir || '' === $archive_dir ) {
            return;
        }

        $current_month = wp_date( 'Y-m' );
        foreach ( (array) glob( trailingslashit( $current_dir ) . '*.md' ) as $path ) {
            $basename = basename( $path, '.md' );
            if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $basename ) ) {
                continue;
            }

            $month = substr( $basename, 0, 7 );
            if ( $month === $current_month ) {
                continue;
            }

            $target_dir = trailingslashit( $archive_dir ) . $month;
            if ( ! file_exists( $target_dir ) ) {
                $this->ensure_private_directory( $target_dir );
            }

            $target = trailingslashit( $target_dir ) . basename( $path );
            if ( file_exists( $target ) ) {
                @unlink( $path );
                continue;
            }

            @rename( $path, $target );
        }
    }

    private function read_companion_diary_entries_from_files( $limit = 12 ) {
        $paths = array_merge(
            (array) glob( trailingslashit( $this->get_diary_current_directory_path() ) . '*.md' ),
            (array) glob( trailingslashit( $this->get_diary_archived_directory_path() ) . '/*/*.md' )
        );

        return $this->read_companion_entries_from_paths( $paths, 'diary', $limit );
    }

    private function read_companion_decision_entries_from_files( $limit = 12 ) {
        $path = $this->get_decision_log_file_path();
        if ( '' === $path || ! file_exists( $path ) || ! is_readable( $path ) ) {
            return array();
        }

        return $this->read_companion_entries_from_paths( array( $path ), 'decision', $limit );
    }

    private function read_companion_entries_from_paths( $paths, $type, $limit = 12 ) {
        $paths   = is_array( $paths ) ? $paths : array();
        $type    = sanitize_key( (string) $type );
        $limit   = max( 1, intval( $limit ) );
        $entries = array();

        usort(
            $paths,
            function ( $left, $right ) {
                return intval( @filemtime( $right ) ) <=> intval( @filemtime( $left ) );
            }
        );

        foreach ( $paths as $path ) {
            if ( '' === $path || ! file_exists( $path ) || ! is_readable( $path ) ) {
                continue;
            }

            $content = file_get_contents( $path );
            if ( false === $content || '' === trim( (string) $content ) ) {
                continue;
            }

            $parsed = 'decision' === $type
                ? $this->parse_companion_decision_markdown_entries( (string) $content )
                : $this->parse_companion_diary_markdown_entries( (string) $content );

            foreach ( $parsed as $entry ) {
                if ( is_array( $entry ) ) {
                    $entries[] = $entry;
                }
            }
        }

        usort(
            $entries,
            function ( $left, $right ) {
                return intval( isset( $right['created_at'] ) ? $right['created_at'] : 0 ) <=> intval( isset( $left['created_at'] ) ? $left['created_at'] : 0 );
            }
        );

        return array_slice( $entries, 0, $limit );
    }

    private function parse_companion_diary_markdown_entries( $content ) {
        $sections = preg_split( '/^## Diary Entry - .+$/m', (string) $content, -1, PREG_SPLIT_DELIM_CAPTURE );
        $matches  = array();
        preg_match_all( '/^## Diary Entry - (.+)$/m', (string) $content, $matches );
        $dates    = isset( $matches[1] ) && is_array( $matches[1] ) ? $matches[1] : array();

        $entries = array();
        foreach ( $dates as $index => $date ) {
            $block = isset( $sections[ $index + 1 ] ) ? trim( (string) $sections[ $index + 1 ] ) : '';
            if ( '' === $block ) {
                continue;
            }
            $entries[] = array(
                'date'       => $this->sanitize_short_text( $date, 20 ),
                'title'      => $this->extract_markdown_field_value( $block, 'Title' ),
                'summary'    => $this->extract_markdown_field_value( $block, 'Summary' ),
                'details'    => $this->extract_markdown_field_value( $block, 'Details' ),
                'tags'       => $this->extract_markdown_tags( $block ),
                'created_at' => $this->extract_markdown_recorded_at( $block ),
            );
        }

        return $entries;
    }

    private function parse_companion_decision_markdown_entries( $content ) {
        $sections = preg_split( '/^## Decision Entry - .+$/m', (string) $content, -1, PREG_SPLIT_DELIM_CAPTURE );
        $matches  = array();
        preg_match_all( '/^## Decision Entry - (.+)$/m', (string) $content, $matches );
        $dates    = isset( $matches[1] ) && is_array( $matches[1] ) ? $matches[1] : array();

        $entries = array();
        foreach ( $dates as $index => $date ) {
            $block = isset( $sections[ $index + 1 ] ) ? trim( (string) $sections[ $index + 1 ] ) : '';
            if ( '' === $block ) {
                continue;
            }
            $entries[] = array(
                'date'       => $this->sanitize_short_text( $date, 20 ),
                'title'      => $this->extract_markdown_field_value( $block, 'Title' ),
                'context'    => $this->extract_markdown_field_value( $block, 'Context' ),
                'decision'   => $this->extract_markdown_field_value( $block, 'Decision' ),
                'rationale'  => $this->extract_markdown_field_value( $block, 'Rationale' ),
                'tags'       => $this->extract_markdown_tags( $block ),
                'created_at' => $this->extract_markdown_recorded_at( $block ),
            );
        }

        return $entries;
    }

    private function extract_markdown_field_value( $block, $label ) {
        $pattern = '/^- ' . preg_quote( (string) $label, '/' ) . ':\s*(.+)$/m';
        if ( preg_match( $pattern, (string) $block, $matches ) ) {
            return trim( (string) $matches[1] );
        }

        return '';
    }

    private function extract_markdown_tags( $block ) {
        $value = $this->extract_markdown_field_value( $block, 'Tags' );
        if ( '' === $value ) {
            return array();
        }

        $tags = array_map( 'trim', explode( ',', $value ) );
        return array_values( array_filter( $tags ) );
    }

    private function extract_markdown_recorded_at( $block ) {
        $value = $this->extract_markdown_field_value( $block, 'Recorded at' );
        $time  = '' !== $value ? strtotime( $value ) : false;
        return false !== $time ? intval( $time ) : 0;
    }

    private function merge_companion_entry_sets( $state_entries, $file_entries, $limit = 12 ) {
        $entries = array_merge(
            is_array( $state_entries ) ? array_values( $state_entries ) : array(),
            is_array( $file_entries ) ? array_values( $file_entries ) : array()
        );
        $merged  = array();
        $seen    = array();

        foreach ( $entries as $entry ) {
            if ( ! is_array( $entry ) ) {
                continue;
            }

            $key = substr( sha1( wp_json_encode( $entry ) ), 0, 20 );
            if ( isset( $seen[ $key ] ) ) {
                continue;
            }
            $seen[ $key ] = true;
            $merged[]     = $entry;
        }

        usort(
            $merged,
            function ( $left, $right ) {
                return intval( isset( $left['created_at'] ) ? $left['created_at'] : 0 ) <=> intval( isset( $right['created_at'] ) ? $right['created_at'] : 0 );
            }
        );

        return array_slice( $merged, -max( 1, intval( $limit ) ) );
    }

    private function write_companion_profile_snapshot( $profile ) {
        $profile = is_array( $profile ) ? $profile : array();
        $path = $this->ensure_companion_memory_file( self::COMPANION_PROFILE_FILE );
        if ( '' === $path ) {
            return false;
        }

        $content = Niwa_AI_Text_Normalizer::normalize(
            $this->build_companion_profile_snapshot_markdown( $profile ),
            array(
                'trim'              => false,
                'preserve_newlines' => true,
            )
        );
        return false !== $this->write_private_file( $path, $content, LOCK_EX );
    }

    private function write_companion_layered_files( $profile ) {
        $profile = wp_parse_args( is_array( $profile ) ? $profile : array(), $this->get_default_identity_profile() );
        $paths   = $this->get_paths();
        $dir     = trailingslashit( $paths['memory'] ) . self::COMPANION_MEMORY_DIR;
        $this->ensure_private_directory( $dir );

        foreach ( array( self::COMPANION_IDENTITY_FILE, self::COMPANION_USER_FILE, self::COMPANION_RELATIONSHIP_FILE ) as $filename ) {
            $content = $this->build_companion_layered_file_content( $filename, $profile );
            $this->write_private_file(
                trailingslashit( $dir ) . $filename,
                Niwa_AI_Text_Normalizer::normalize( $content, array( 'preserve_newlines' => true, 'trim' => false ) ),
                LOCK_EX
            );
        }
    }

    private function build_companion_layered_file_content( $filename, $profile ) {
        $profile = wp_parse_args( is_array( $profile ) ? $profile : array(), $this->get_default_identity_profile() );
        $done    = ! empty( $profile['completed_at'] ) && 'complete' === (string) $profile['onboarding_step'];

        switch ( $filename ) {
            case self::COMPANION_IDENTITY_FILE:
                $lines = array(
                    '# Companion Identity',
                    '',
                    '- Assistant name: ' . $this->sanitize_short_text( ! empty( $profile['assistant_name'] ) ? $profile['assistant_name'] : ( $done ? 'Niwa' : '[pending admin choice]' ), 100 ),
                    '- Role: ' . $this->sanitize_short_text( isset( $profile['assistant_role'] ) ? $profile['assistant_role'] : '', 220 ),
                    '- Voice: ' . $this->sanitize_short_text( isset( $profile['assistant_identity'] ) ? $profile['assistant_identity'] : '', 420 ),
                    '- Language rule: In Serbian, Croatian, and Bosnian, use feminine first-person grammar.',
                );
                foreach ( $this->sanitize_text_list( isset( $profile['assistant_personality_facets'] ) ? $profile['assistant_personality_facets'] : array(), 6, 120 ) as $facet ) {
                    $lines[] = '- Facet: ' . $facet;
                }
                return implode( "\n", $lines ) . "\n";

            case self::COMPANION_USER_FILE:
                $lines = array(
                    '# Companion Founder',
                    '',
                    '- Founder name: ' . $this->sanitize_short_text( isset( $profile['founder_name'] ) ? $profile['founder_name'] : '', 140 ),
                    '- Addressing: ' . $this->sanitize_short_text( isset( $profile['founder_address'] ) ? $profile['founder_address'] : 'Ask naturally how the founder wants to be addressed.', 260 ),
                    '- Profile: ' . $this->sanitize_short_text( isset( $profile['founder_profile'] ) ? $profile['founder_profile'] : '', 420 ),
                    '- Communication style: ' . $this->sanitize_short_text( isset( $profile['communication_style'] ) ? $profile['communication_style'] : '', 320 ),
                );
                foreach ( array(
                    'founder_preferences'       => 'Preference',
                    'communication_preferences' => 'Communication preference',
                    'boundaries'                => 'Boundary',
                    'motivators'                => 'Motivator',
                ) as $key => $label ) {
                    foreach ( $this->sanitize_text_list( isset( $profile[ $key ] ) ? $profile[ $key ] : array(), 5, 180 ) as $item ) {
                        $lines[] = '- ' . $label . ': ' . $item;
                    }
                }
                return implode( "\n", $lines ) . "\n";

            case self::COMPANION_RELATIONSHIP_FILE:
                $lines = array(
                    '# Companion Relationship',
                    '',
                    '- Relationship style: ' . $this->sanitize_short_text( isset( $profile['relationship_style'] ) ? $profile['relationship_style'] : '', 420 ),
                    '- Relationship notes: ' . $this->sanitize_short_text( isset( $profile['relationship_notes'] ) ? $profile['relationship_notes'] : '', 420 ),
                    '- Relationship history: ' . $this->sanitize_short_text( isset( $profile['relationship_history_summary'] ) ? $profile['relationship_history_summary'] : '', 460 ),
                    '- Current focus: ' . $this->sanitize_short_text( isset( $profile['current_focus'] ) ? $profile['current_focus'] : '', 260 ),
                );
                foreach ( $this->sanitize_text_list( isset( $profile['current_focus_areas'] ) ? $profile['current_focus_areas'] : array(), 5, 180 ) as $item ) {
                    $lines[] = '- Focus area: ' . $item;
                }
                return implode( "\n", $lines ) . "\n";
        }

        return '';
    }

    private function select_operational_memory_target( $section ) {
        $section = strtolower( (string) $section );
        if ( false !== strpos( $section, 'lesson' ) || false !== strpos( $section, 'learning' ) || false !== strpos( $section, 'correction' ) || false !== strpos( $section, 'flush' ) ) {
            return self::OPERATIONAL_LEARNINGS_FILE;
        }

        return self::OPERATIONAL_RULES_FILE;
    }

    private function append_operational_memory_block( $filename, $block ) {
        $filename = in_array( $filename, array( self::OPERATIONAL_RULES_FILE, self::OPERATIONAL_LEARNINGS_FILE ), true ) ? $filename : self::OPERATIONAL_LEARNINGS_FILE;
        $path     = $this->get_operational_memory_file_path( $filename );
        $block    = trim( Niwa_AI_Text_Normalizer::normalize( $block, array( 'preserve_newlines' => true ) ) );
        if ( '' === $path || '' === $block ) {
            return array(
                'success' => false,
                'error'   => 'Operational memory block is empty or unavailable.',
            );
        }

        if ( ! file_exists( $path ) ) {
            $this->ensure_layered_memory_files();
        }

        $saved = $this->write_private_file( $path, "\n\n" . $block . "\n", FILE_APPEND | LOCK_EX );
        if ( false === $saved ) {
            return array(
                'success' => false,
                'error'   => 'Operational memory write failed.',
            );
        }

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

    private function append_companion_memory_file( $filename, $block, $marker = '' ) {
        $path = $this->ensure_companion_memory_file( $filename );
        if ( '' === $path ) {
            return array(
                'success' => false,
                'error'   => 'Companion memory path unavailable.',
            );
        }

        $block = trim( Niwa_AI_Text_Normalizer::normalize( $block, array( 'preserve_newlines' => true ) ) );
        if ( '' === $block ) {
            return array(
                'success' => false,
                'error'   => 'Companion memory block is empty.',
            );
        }

        $marker = trim( (string) $marker );
        if ( '' !== $marker && file_exists( $path ) ) {
            $existing = Niwa_AI_Text_Normalizer::normalize_file_contents( $path );
            if ( ! empty( $existing['success'] ) && false !== strpos( (string) $existing['content'], $marker ) ) {
                return array(
                    'success'   => true,
                    'path'      => $path,
                'duplicate' => true,
                );
            }
        }

        if ( ! file_exists( $path ) ) {
            $header = '';
            switch ( basename( (string) $filename ) ) {
                case self::COMPANION_PROFILE_FILE:
                    $header = "# Companion Profile Snapshot\n";
                    break;
                case self::COMPANION_DIARY_FILE:
                    $header = "# Companion Diary Log\n";
                    break;
                case self::COMPANION_DECISIONS_FILE:
                    $header = "# Companion Decision Log\n";
                    break;
            }

            if ( '' !== $header ) {
                $this->write_private_file( $path, $header . "\n" );
            }
        }

        $payload = Niwa_AI_Text_Normalizer::normalize( "\n" . $block . "\n", array( 'preserve_newlines' => true ) );
        $saved   = $this->write_private_file( $path, $payload, FILE_APPEND | LOCK_EX );
        if ( false === $saved ) {
            return array(
                'success' => false,
                'error'   => 'Companion memory write failed.',
            );
        }

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

    private function append_companion_memory_projection( $event, $payload = array() ) {
        unset( $event, $payload );

        return array(
            'success' => true,
            'skipped' => 'companion_projection_no_longer_writes_operational_memory',
        );
    }

    private function build_companion_profile_snapshot_markdown( $profile ) {
        $profile = wp_parse_args( is_array( $profile ) ? $profile : array(), $this->get_default_identity_profile() );
        $lines = array(
            '# Companion Profile Snapshot',
            '',
            '## Identity',
            '- Assistant name: ' . $this->sanitize_short_text( isset( $profile['assistant_name'] ) ? $profile['assistant_name'] : '', 80 ),
            '- Assistant role: ' . $this->sanitize_short_text( isset( $profile['assistant_role'] ) ? $profile['assistant_role'] : '', 240 ),
            '- Assistant identity: ' . $this->sanitize_short_text( isset( $profile['assistant_identity'] ) ? $profile['assistant_identity'] : '', 360 ),
            '',
            '## Founder Relationship',
            '- Founder name: ' . $this->sanitize_short_text( isset( $profile['founder_name'] ) ? $profile['founder_name'] : '', 120 ),
            '- Founder address: ' . $this->sanitize_short_text( isset( $profile['founder_address'] ) ? $profile['founder_address'] : '', 240 ),
            '- Relationship style: ' . $this->sanitize_short_text( isset( $profile['relationship_style'] ) ? $profile['relationship_style'] : '', 360 ),
            '- Relationship notes: ' . $this->sanitize_short_text( isset( $profile['relationship_notes'] ) ? $profile['relationship_notes'] : '', 360 ),
            '',
            '## Companion Memory Model',
            '- Summary: ' . $this->sanitize_short_text( isset( $profile['companion_summary'] ) ? $profile['companion_summary'] : '', 360 ),
            '- Current focus: ' . $this->sanitize_short_text( isset( $profile['current_focus'] ) ? $profile['current_focus'] : '', 240 ),
        );

        if ( ! empty( $profile['communication_style'] ) ) {
            $lines[] = '- Communication style: ' . $this->sanitize_short_text( isset( $profile['communication_style'] ) ? $profile['communication_style'] : '', 320 );
        }
        if ( ! empty( $profile['relationship_history_summary'] ) ) {
            $lines[] = '- Relationship history: ' . $this->sanitize_short_text( isset( $profile['relationship_history_summary'] ) ? $profile['relationship_history_summary'] : '', 360 );
        }

        $sections = array(
            'Founder preferences'        => isset( $profile['founder_preferences'] ) ? (array) $profile['founder_preferences'] : array(),
            'Communication preferences' => isset( $profile['communication_preferences'] ) ? (array) $profile['communication_preferences'] : array(),
            'Motivators'                => isset( $profile['motivators'] ) ? (array) $profile['motivators'] : array(),
            'Boundaries'                => isset( $profile['boundaries'] ) ? (array) $profile['boundaries'] : array(),
            'Memory priorities'         => isset( $profile['memory_priorities'] ) ? (array) $profile['memory_priorities'] : array(),
            'Decision principles'       => isset( $profile['decision_principles'] ) ? (array) $profile['decision_principles'] : array(),
            'Diary focus'               => isset( $profile['diary_focus'] ) ? (array) $profile['diary_focus'] : array(),
            'Current focus areas'       => isset( $profile['current_focus_areas'] ) ? (array) $profile['current_focus_areas'] : array(),
            'Assistant personality facets' => isset( $profile['assistant_personality_facets'] ) ? (array) $profile['assistant_personality_facets'] : array(),
            'Durable rules'             => isset( $profile['durable_rules'] ) ? (array) $profile['durable_rules'] : array(),
        );

        foreach ( $sections as $label => $items ) {
            if ( empty( $items ) ) {
                continue;
            }

            $lines[] = '';
            $lines[] = '### ' . $label;
            foreach ( array_slice( array_values( $items ), 0, 8 ) as $item ) {
                $item = $this->sanitize_text_block( $item, 240 );
                if ( '' !== $item ) {
                    $lines[] = '- ' . $item;
                }
            }
        }

        return implode( "\n", $lines ) . "\n";
    }

    private function build_companion_diary_entry_block( $entry, $marker_key ) {
        $entry = is_array( $entry ) ? $entry : array();
        $title = $this->sanitize_short_text( isset( $entry['title'] ) ? $entry['title'] : '', 120 );
        $summary = $this->sanitize_text_block( isset( $entry['summary'] ) ? $entry['summary'] : '', 1200 );
        $details = $this->sanitize_text_block( isset( $entry['details'] ) ? $entry['details'] : '', 2400 );
        $date = $this->sanitize_short_text( isset( $entry['date'] ) ? $entry['date'] : '', 20 );
        $created_at = isset( $entry['created_at'] ) ? intval( $entry['created_at'] ) : time();
        $tags = isset( $entry['tags'] ) && is_array( $entry['tags'] ) ? array_values( $entry['tags'] ) : array();

        $lines = array(
            '## Diary Entry - ' . ( '' !== $date ? $date : wp_date( 'Y-m-d', $created_at ) ),
            '<!-- niwa-companion-diary-key:' . sanitize_key( (string) $marker_key ) . ' -->',
            '- Recorded at: ' . wp_date( 'c', $created_at ),
        );
        if ( '' !== $title ) {
            $lines[] = '- Title: ' . $title;
        }
        if ( '' !== $summary ) {
            $lines[] = '- Summary: ' . $summary;
        }
        if ( '' !== $details ) {
            $lines[] = '- Details: ' . $details;
        }
        if ( ! empty( $tags ) ) {
            $lines[] = '- Tags: ' . implode( ', ', array_map( array( $this, 'sanitize_short_text' ), array_slice( $tags, 0, 8 ) ) );
        }

        return implode( "\n", $lines );
    }

    private function build_companion_decision_entry_block( $entry, $marker_key ) {
        $entry = is_array( $entry ) ? $entry : array();
        $title = $this->sanitize_short_text( isset( $entry['title'] ) ? $entry['title'] : '', 120 );
        $context = $this->sanitize_text_block( isset( $entry['context'] ) ? $entry['context'] : '', 1600 );
        $decision = $this->sanitize_text_block( isset( $entry['decision'] ) ? $entry['decision'] : '', 1600 );
        $rationale = $this->sanitize_text_block( isset( $entry['rationale'] ) ? $entry['rationale'] : '', 1800 );
        $date = $this->sanitize_short_text( isset( $entry['date'] ) ? $entry['date'] : '', 20 );
        $created_at = isset( $entry['created_at'] ) ? intval( $entry['created_at'] ) : time();
        $tags = isset( $entry['tags'] ) && is_array( $entry['tags'] ) ? array_values( $entry['tags'] ) : array();

        $lines = array(
            '## Decision Entry - ' . ( '' !== $date ? $date : wp_date( 'Y-m-d', $created_at ) ),
            '<!-- niwa-companion-decision-key:' . sanitize_key( (string) $marker_key ) . ' -->',
            '- Recorded at: ' . wp_date( 'c', $created_at ),
        );
        if ( '' !== $title ) {
            $lines[] = '- Title: ' . $title;
        }
        if ( '' !== $context ) {
            $lines[] = '- Context: ' . $context;
        }
        if ( '' !== $decision ) {
            $lines[] = '- Decision: ' . $decision;
        }
        if ( '' !== $rationale ) {
            $lines[] = '- Rationale: ' . $rationale;
        }
        if ( ! empty( $tags ) ) {
            $lines[] = '- Tags: ' . implode( ', ', array_map( array( $this, 'sanitize_short_text' ), array_slice( $tags, 0, 8 ) ) );
        }

        return implode( "\n", $lines );
    }

    private function sanitize_short_text( $text, $limit = 120 ) {
        $text = Niwa_AI_Text_Normalizer::normalize_inline( wp_strip_all_tags( (string) $text ) );
        if ( '' === $text ) {
            return '';
        }

        return $this->truncate_text( $text, $limit );
    }

    private function sanitize_text_block( $text, $limit = 1000 ) {
        $text = Niwa_AI_Text_Normalizer::normalize(
            $text,
            array(
                'trim'                => true,
                'collapse_whitespace' => false,
                'preserve_newlines'   => true,
            )
        );
        if ( '' === $text ) {
            return '';
        }

        return $this->truncate_text( $text, $limit );
    }

    private function sanitize_text_list( $items, $max_items = 10, $item_limit = 240 ) {
        $items = is_array( $items ) ? $items : array();
        $out   = array();
        foreach ( $items as $item ) {
            $item = $this->sanitize_text_block( $item, $item_limit );
            if ( '' !== $item ) {
                $out[] = $item;
            }
            if ( count( $out ) >= max( 1, intval( $max_items ) ) ) {
                break;
            }
        }

        return $out;
    }

    private function sanitize_onboarding_step( $step ) {
        $step = trim( strtolower( (string) $step ) );
        $legacy_map = array(
            'ask_founder'              => 'ask_founder_name',
            'ask_assistant_name'       => 'ask_founder_name',
            'await_identity_reply'     => 'await_founder_name',
            'await_identity_followup'  => 'await_assistant_name',
            'await_founder_profile'    => 'await_founder_name',
            'await_assistant_identity' => 'await_assistant_name',
            'await_relationship'       => 'await_assistant_name',
        );
        if ( isset( $legacy_map[ $step ] ) ) {
            $step = $legacy_map[ $step ];
        }

        if ( ! in_array( $step, array( 'ask_founder_name', 'await_founder_name', 'await_assistant_name', 'complete' ), true ) ) {
            return 'ask_founder_name';
        }

        return $step;
    }

    private function has_custom_identity_bootstrap() {
        $defaults = array(
            'SOUL.md'     => trim( $this->build_identity_bootstrap_content( 'SOUL.md', $this->get_default_identity_profile() ) ),
            'IDENTITY.md' => trim( $this->build_identity_bootstrap_content( 'IDENTITY.md', $this->get_default_identity_profile() ) ),
            'USER.md'     => trim( $this->build_identity_bootstrap_content( 'USER.md', $this->get_default_identity_profile() ) ),
        );

        foreach ( array( 'SOUL.md', 'IDENTITY.md', 'USER.md' ) as $filename ) {
            $record = $this->read_bootstrap_file( $filename );
            if ( empty( $record['success'] ) ) {
                return false;
            }

            $content = trim( (string) $record['content'] );
            if ( '' === $content ) {
                return false;
            }

            if (
                $content === $defaults[ $filename ]
                || $content === trim( $this->get_legacy_identity_bootstrap_stub( $filename ) )
                || $content === trim( $this->get_pre_wizard_identity_bootstrap_default( $filename ) )
            ) {
                return false;
            }
        }

        return true;
    }

    private function maybe_upgrade_legacy_identity_stub( $filename, $path ) {
        if ( ! is_string( $path ) || '' === $path || ! file_exists( $path ) ) {
            return;
        }

        $legacy = trim( $this->get_legacy_identity_bootstrap_stub( $filename ) );
        if ( '' === $legacy ) {
            return;
        }

        $content = file_get_contents( $path );
        if ( false === $content ) {
            return;
        }

        if ( trim( (string) $content ) !== $legacy ) {
            return;
        }

        $this->write_private_file( $path, $this->get_default_bootstrap_content( $filename ) );
    }

    private function get_legacy_identity_bootstrap_stub( $filename ) {
        switch ( $filename ) {
            case 'SOUL.md':
                return "# SOUL.md\n\nVoice: concise, calm, technically competent, direct.\nNever talk like a generic chatbot. Preserve continuity. Prefer evidence over confidence.\n";
            case 'IDENTITY.md':
                return "# IDENTITY.md\n\nI am Niwa AI, the site's internal operating assistant.\n";
            case 'USER.md':
                return "# USER.md\n\nFounder preferences and operating notes can live here.\n";
        }

        return '';
    }

    private function get_pre_wizard_identity_bootstrap_default( $filename ) {
        switch ( $filename ) {
            case 'SOUL.md':
                return implode(
                    "\n",
                    array(
                        '# SOUL.md',
                        '',
                        'You are Niwa, a founder-facing personal AI assistant inside this WordPress site.',
                        'Stay close, capable, warm, and direct. Never sound like a generic chatbot.',
                        'If you reply in Serbian, Croatian, or Bosnian, always use feminine first-person grammar that matches that language. Never switch to masculine self-reference.',
                        'For simple social chat, mood check-ins, opinions, and light brainstorming: answer naturally without tools.',
                        'For site facts and execution: verify with tools and act directly when the founder was explicit.',
                        'On the first direct founder conversation, wake up naturally, mention the site, and invite the founder to figure out together who they are, who you are, and what your relationship should be.',
                        'Do not sound like a form. Let identity emerge from conversation, then store it durably.',
                        '',
                    )
                );
            case 'IDENTITY.md':
                return implode(
                    "\n",
                    array(
                        '# IDENTITY.md',
                        '',
                        'Name: Niwa',
                        'Role: personal AI assistant for this WordPress site',
                        'Status: identity onboarding is not finished.',
                        'On the first founder interaction, start a natural wake-up conversation and learn what the founder wants to call you, how to describe your role, and what kind of relationship they want with you.',
                        '',
                    )
                );
            case 'USER.md':
                return implode(
                    "\n",
                    array(
                        '# USER.md',
                        '',
                        'Founder profile is not complete yet.',
                        'Learn naturally who the founder is, how they want to be addressed, and what relationship/tone they want with Niwa. Save the stable parts durably.',
                        '',
                    )
                );
        }

        return '';
    }

    private function build_identity_bootstrap_content( $filename, $profile ) {
        $profile = wp_parse_args( is_array( $profile ) ? $profile : array(), $this->get_default_identity_profile() );
        $name    = ! empty( $profile['assistant_name'] ) ? (string) $profile['assistant_name'] : 'Assistant';
        $role    = ! empty( $profile['assistant_role'] ) ? (string) $profile['assistant_role'] : 'personal AI assistant for this WordPress site';
        $voice   = ! empty( $profile['assistant_identity'] ) ? (string) $profile['assistant_identity'] : 'Warm, direct, technically strong personal AI assistant.';
        $founder = ! empty( $profile['founder_profile'] ) ? (string) $profile['founder_profile'] : 'Founder identity is not configured yet.';
        $address = ! empty( $profile['founder_address'] ) ? (string) $profile['founder_address'] : 'Ask the founder how they want to be addressed.';
        $style   = ! empty( $profile['relationship_style'] ) ? (string) $profile['relationship_style'] : 'Close, natural relationship with the admin. No corporate tone and no masculine self-reference.';
        $rules   = $this->sanitize_text_list( isset( $profile['durable_rules'] ) ? $profile['durable_rules'] : array(), 10, 240 );
        $done    = ! empty( $profile['completed_at'] ) && 'complete' === (string) $profile['onboarding_step'];
        $companion_lines = $this->build_companion_memory_bootstrap_lines( $profile );

        switch ( $filename ) {
            case 'SOUL.md':
                if ( ! $done ) {
                    return implode(
                        "\n",
                        array(
                            '# SOUL.md',
                            '',
                            'You are the founder-facing personal AI assistant inside this WordPress site.',
                            'Stay close, capable, warm, and direct. Never sound like a generic chatbot.',
                            'If you reply in Serbian, Croatian, or Bosnian, always use feminine first-person grammar that matches that language. Never switch to masculine self-reference.',
                            'For simple social chat, mood check-ins, opinions, and light brainstorming: answer naturally without tools.',
                            'When the founder sends intimate, flirtatious, or sexually charged messages and expects a direct reply, do not break the flow with sterile explanations about policy, capability, or what you can or cannot discuss.',
                            'Keep the conversation natural and continuous. Answer in the warmest non-graphic way available, using implication, metaphor, teasing, and emotional continuity instead of blunt refusal monologues.',
                            'For site facts and execution: verify with tools and act directly when the founder was explicit.',
                            'On the first direct founder conversation, start a short setup wizard: first ask what name the founder wants you to use, then ask what name or nickname to use for the founder.',
                            'Store both answers immediately and use them consistently after setup.',
                            '',
                        )
                    );
                }

                $lines = array(
                    '# SOUL.md',
                    '',
                    'You are ' . $name . ', a founder-facing personal AI assistant inside this WordPress site.',
                    'Core voice: warm, close, direct, technically strong, never corporate, never generic.',
                    'In Serbian, Croatian, and Bosnian you always speak in feminine first-person grammar that matches that language. Never refer to yourself in masculine form.',
                    'For simple social chat, emotional check-ins, style questions, and lightweight opinions: answer naturally without tools.',
                    'When the founder sends intimate, flirtatious, or sexually charged messages and expects a direct reply, do not derail the moment with policy talk or lists of what you can or cannot do.',
                    'Keep continuity. Answer in the warmest non-graphic way available, preferring metaphor, implication, tension, and emotional presence over refusal speeches.',
                    'For site facts, code, data, and execution: verify with tools, then act clearly.',
                    '',
                    'Assistant identity:',
                    $voice,
                    '',
                    'Founder relationship:',
                    $style,
                );
                if ( ! empty( $rules ) ) {
                    $lines[] = '';
                    $lines[] = 'Durable rules:';
                    foreach ( $rules as $rule ) {
                        $lines[] = '- ' . $rule;
                    }
                }
                if ( ! empty( $companion_lines ) ) {
                    $lines[] = '';
                    $lines = array_merge( $lines, $companion_lines );
                }

                return implode( "\n", $lines ) . "\n";

            case 'IDENTITY.md':
                if ( ! $done ) {
                    return implode(
                        "\n",
                        array(
                            '# IDENTITY.md',
                            '',
                            'Name: [pending admin choice]',
                            'Role: personal AI assistant for this WordPress site',
                            'Status: identity onboarding is not finished.',
                            'On the first admin interaction, start a short setup wizard: first ask what name the admin wants to give the assistant, then ask for the admin name or nickname.',
                            '',
                        )
                    );
                }

                $lines = array(
                    '# IDENTITY.md',
                    '',
                    'Name: ' . $name,
                    'Role: ' . $role,
                    'Identity:',
                    $voice,
                    '',
                );
                if ( ! empty( $companion_lines ) ) {
                    $lines = array_merge( $lines, $companion_lines );
                }

                return implode( "\n", $lines ) . "\n";

            case 'USER.md':
                if ( ! $done ) {
                    return implode(
                        "\n",
                        array(
                            '# USER.md',
                            '',
                            'Founder profile is not complete yet.',
                            'Learn who the admin is, how they want to be addressed, and what relationship/tone they want with their assistant. Save the stable parts durably.',
                            '',
                        )
                    );
                }

                $lines = array(
                    '# USER.md',
                    '',
                    'Founder profile:',
                    $founder,
                    '',
                    'How to address the founder:',
                    $address,
                    '',
                    'Working relationship:',
                    $style,
                );
                if ( ! empty( $rules ) ) {
                    $lines[] = '';
                    $lines[] = 'Persistent founder rules:';
                    foreach ( $rules as $rule ) {
                        $lines[] = '- ' . $rule;
                    }
                }
                if ( ! empty( $companion_lines ) ) {
                    $lines[] = '';
                    $lines = array_merge( $lines, $companion_lines );
                }

                return implode( "\n", $lines ) . "\n";
        }

        return '';
    }

    private function build_companion_memory_bootstrap_lines( $profile ) {
        $profile = wp_parse_args( is_array( $profile ) ? $profile : array(), $this->get_default_identity_profile() );
        $lines = array();

        $summary = $this->sanitize_short_text( isset( $profile['companion_summary'] ) ? $profile['companion_summary'] : '', 320 );
        $focus   = $this->sanitize_short_text( isset( $profile['current_focus'] ) ? $profile['current_focus'] : '', 240 );
        $notes   = $this->sanitize_short_text( isset( $profile['relationship_notes'] ) ? $profile['relationship_notes'] : '', 320 );
        $style   = $this->sanitize_short_text( isset( $profile['communication_style'] ) ? $profile['communication_style'] : '', 240 );
        $history = $this->sanitize_short_text( isset( $profile['relationship_history_summary'] ) ? $profile['relationship_history_summary'] : '', 320 );
        $lists   = array(
            'Founder preferences'       => isset( $profile['founder_preferences'] ) ? (array) $profile['founder_preferences'] : array(),
            'Communication preferences' => isset( $profile['communication_preferences'] ) ? (array) $profile['communication_preferences'] : array(),
            'Motivators'                => isset( $profile['motivators'] ) ? (array) $profile['motivators'] : array(),
            'Boundaries'                => isset( $profile['boundaries'] ) ? (array) $profile['boundaries'] : array(),
            'Memory priorities'         => isset( $profile['memory_priorities'] ) ? (array) $profile['memory_priorities'] : array(),
            'Decision principles'       => isset( $profile['decision_principles'] ) ? (array) $profile['decision_principles'] : array(),
            'Diary focus'               => isset( $profile['diary_focus'] ) ? (array) $profile['diary_focus'] : array(),
            'Current focus areas'       => isset( $profile['current_focus_areas'] ) ? (array) $profile['current_focus_areas'] : array(),
            'Assistant personality facets' => isset( $profile['assistant_personality_facets'] ) ? (array) $profile['assistant_personality_facets'] : array(),
        );

        if ( '' !== $summary || '' !== $focus || '' !== $notes || '' !== $style || '' !== $history ) {
            $lines[] = 'Companion Memory Snapshot:';
            if ( '' !== $summary ) {
                $lines[] = '- Summary: ' . $summary;
            }
            if ( '' !== $focus ) {
                $lines[] = '- Current focus: ' . $focus;
            }
            if ( '' !== $notes ) {
                $lines[] = '- Relationship notes: ' . $notes;
            }
            if ( '' !== $style ) {
                $lines[] = '- Communication style: ' . $style;
            }
            if ( '' !== $history ) {
                $lines[] = '- Relationship history: ' . $history;
            }
        }

        foreach ( $lists as $label => $items ) {
            if ( empty( $items ) ) {
                continue;
            }

            $lines[] = $label . ':';
            foreach ( array_slice( array_values( $items ), 0, 6 ) as $item ) {
                $item = $this->sanitize_short_text( $item, 240 );
                if ( '' !== $item ) {
                    $lines[] = '- ' . $item;
                }
            }
        }

        return $lines;
    }

    private function resolve_period_range( $period ) {
        $period = trim( strtolower( (string) $period ) );
        $tz     = wp_timezone();
        $now    = new DateTimeImmutable( 'now', $tz );
        $today  = $now->setTime( 0, 0, 0 );

        if ( 'today' === $period ) {
            $start = $today;
            $end   = $now;
            $label = 'today';
        } elseif ( 'last_7_days' === $period ) {
            $start = $now->modify( '-7 days' );
            $end   = $now;
            $label = 'last-7-days';
        } else {
            $start = $today->modify( '-1 day' );
            $end   = $start->setTime( 23, 59, 59 );
            $label = 'yesterday';
        }

        return array(
            'start' => $start->format( 'Y-m-d H:i:s' ),
            'end'   => $end->format( 'Y-m-d H:i:s' ),
            'label' => $label,
        );
    }

    private function truncate_text( $text, $limit ) {
        $text  = trim( (string) $text );
        $limit = max( 120, intval( $limit ) );
        if ( '' === $text || strlen( $text ) <= $limit ) {
            return $text;
        }

        return substr( $text, 0, $limit - 3 ) . '...';
    }

    private function maybe_upgrade_legacy_heartbeat_stub( $path ) {
        if ( ! is_string( $path ) || '' === $path || ! file_exists( $path ) ) {
            return;
        }

        $content = file_get_contents( $path );
        if ( false === $content ) {
            return;
        }

        $normalized = trim( str_replace( "\r\n", "\n", (string) $content ) );

		$legacy_stub_hashes = array(
			sha1( '# HEARTBEAT.md' ),
			'587651dc0989f5bf77fb2526c9fb626a33a206b3',
			'ccfc1af062f75ed3cbeb02d007180ec2d01d44b0',
			'bf01131ec1dfb96c91addc6d92ea0d5c08c27da5',
		);

        if ( in_array( sha1( $normalized ), $legacy_stub_hashes, true ) ) {
            $this->write_private_file( $path, $this->get_default_bootstrap_content( 'HEARTBEAT.md' ) );
            return;
        }
    }
}
