<?php
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Static helper methods for NNAX File Manager.
 */
class NNAXFUF_Helpers {
    /**
     * Return the raw stored post title without frontend text filters (e.g. wptexturize).
     */
    public static function get_raw_item_title( $item_id ) {
        $post = get_post( (int) $item_id );
        if ( ! $post ) {
            return '';
        }
        return isset( $post->post_title ) ? (string) $post->post_title : '';
    }

    private static function base64url_encode( $value ) {
        $raw = base64_encode( (string) $value );
        if ( $raw === false ) {
            return '';
        }
        return rtrim( strtr( $raw, '+/', '-_' ), '=' );
    }

    /**
     * Normalize a file extension to a lowercase alphanumeric token.
     */
    public static function normalize_file_extension( $ext ) {
        $ext = ltrim( strtolower( (string) $ext ), '.' );
        $ext = preg_replace( '/[^a-z0-9]/', '', $ext );
        return is_string( $ext ) ? $ext : '';
    }

    /**
     * Extensions that can execute code or serve active content.
     */
    public static function get_restricted_upload_extensions() {
        return array(
            'php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'phar', 'phps', 'pht', 'phtm',
            'js', 'mjs', 'cjs', 'css', 'html', 'htm', 'xhtml', 'shtml', 'svg', 'xml', 'xht',
            'cgi', 'pl', 'py', 'sh', 'bash', 'zsh', 'ksh', 'fish', 'cmd', 'bat', 'com',
            'exe', 'dll', 'scr', 'jar', 'vbs', 'vbe', 'wsf', 'wsh', 'ps1', 'psm1', 'asp', 'aspx',
            'jsp', 'scpt',
        );
    }

    /**
     * Check whether an extension should never be accepted through uploads.
     */
    public static function is_restricted_upload_extension( $ext ) {
        $ext = self::normalize_file_extension( $ext );
        if ( $ext === '' ) {
            return false;
        }

        return in_array( $ext, self::get_restricted_upload_extensions(), true );
    }

    /**
     * Sanitize and filter a comma-separated list of upload extensions.
     */
    public static function normalize_safe_upload_extension_list( $raw_list ) {
        if ( is_array( $raw_list ) ) {
            $raw_list = implode( ',', $raw_list );
        }

        $items = explode( ',', (string) $raw_list );
        $items = array_map( array( __CLASS__, 'normalize_file_extension' ), $items );
        $items = array_filter(
            $items,
            function ( $ext ) {
                return $ext !== '' && ! NNAXFUF_Helpers::is_restricted_upload_extension( $ext );
            }
        );

        return array_values( array_unique( $items ) );
    }
    /**
     * Get the Base64 encoded logo string.
     *
     * @return string
     */
    public static function get_logo_base64(): string {
        return self::get_img_base64( NNAXFUF_PLUGIN_PATH . 'assets/images/logo-base64.txt' );
    }

    /**
     * Get the Base64 encoded file string.
     *
     * @param string $file Absolute file path.
     * @return string
     */
    public static function get_img_base64( $file ): string {
        if ( file_exists( $file ) ) {
            return trim( (string) file_get_contents( $file ) );
        }

        return '';
    }

    /**
     * Default custom file-meta fields shown in admin.
     */
    public static function get_default_custom_meta_fields() {
        return array(
            array(
                'key'   => '_nnaxfuf_desc',
                'label' => 'Beschreibung',
                'type'  => 'textarea',
            ),
            array(
                'key'   => '_nnaxfuf_location',
                'label' => 'Ort',
                'type'  => 'text',
            ),
        );
    }

    /**
     * Normalize custom field definitions.
     */
    public static function normalize_custom_meta_fields( $fields ) {
        $fields = is_array( $fields ) ? $fields : array();

        $allowed_types = array( 'text', 'textarea', 'date', 'number', 'email', 'url' );
        $normalized    = array();
        $seen_keys     = array();

        foreach ( $fields as $field ) {
            if ( ! is_array( $field ) ) {
                continue;
            }

            $raw_key = isset( $field['key'] ) ? sanitize_key( (string) $field['key'] ) : '';
            if ( $raw_key === '' ) {
                continue;
            }

            if ( strpos( $raw_key, '_nnaxfuf_' ) !== 0 ) {
                $raw_key = '_nnaxfuf_' . ltrim( $raw_key, '_' );
            }

            $key = sanitize_key( $raw_key );
            if ( $key === '' || isset( $seen_keys[ $key ] ) ) {
                continue;
            }

            $label = isset( $field['label'] ) ? sanitize_text_field( (string) $field['label'] ) : '';
            if ( $label === '' ) {
                continue;
            }

            $type = isset( $field['type'] ) ? sanitize_key( (string) $field['type'] ) : 'text';
            if ( ! in_array( $type, $allowed_types, true ) ) {
                $type = 'text';
            }

            $normalized[]    = array(
                'key'   => $key,
                'label' => $label,
                'type'  => $type,
            );
            $seen_keys[ $key ] = true;
        }

        return $normalized;
    }

    /**
     * UI color definitions.
     *
     * key => array(label, css_var, default)
     */
    public static function get_ui_color_definitions() {
        return array(
            'primary'      => array( 'label' => 'Primary', 'css_var' => '--nnaxfuf-color-primary', 'default' => '#1e88e5' ),
            'primary_hover'=> array( 'label' => 'Primary (Hover)', 'css_var' => '--nnaxfuf-color-primary-hover', 'default' => '#166fbd' ),
            'on_primary'   => array( 'label' => 'Text auf Primary', 'css_var' => '--nnaxfuf-color-on-primary', 'default' => '#ffffff' ),
            'danger'       => array( 'label' => 'Danger', 'css_var' => '--nnaxfuf-color-danger', 'default' => '#e53935' ),
            'danger_hover' => array( 'label' => 'Danger (Hover)', 'css_var' => '--nnaxfuf-color-danger-hover', 'default' => '#c62828' ),
            'on_danger'    => array( 'label' => 'Text auf Danger', 'css_var' => '--nnaxfuf-color-on-danger', 'default' => '#ffffff' ),
            'button_default_bg'          => array( 'label' => 'Button Default (Hintergrund)', 'css_var' => '--nnaxfuf-button-default-bg', 'default' => '#ffffff' ),
            'button_default_border'      => array( 'label' => 'Button Default (Border)', 'css_var' => '--nnaxfuf-button-default-border', 'default' => '#e5e5e5' ),
            'button_default_text'        => array( 'label' => 'Button Default (Text)', 'css_var' => '--nnaxfuf-button-default-text', 'default' => '#666666' ),
            'button_default_hover_bg'    => array( 'label' => 'Button Default (Hover Hintergrund)', 'css_var' => '--nnaxfuf-button-default-hover-bg', 'default' => '#f8f8f8' ),
            'button_default_hover_border'=> array( 'label' => 'Button Default (Hover Border)', 'css_var' => '--nnaxfuf-button-default-hover-border', 'default' => '#b2b2b2' ),
            'button_default_hover_text'  => array( 'label' => 'Button Default (Hover Text)', 'css_var' => '--nnaxfuf-button-default-hover-text', 'default' => '#666666' ),
            'button_primary_bg'          => array( 'label' => 'Button Primary (Hintergrund)', 'css_var' => '--nnaxfuf-button-primary-bg', 'default' => '#1e87f0' ),
            'button_primary_border'      => array( 'label' => 'Button Primary (Border)', 'css_var' => '--nnaxfuf-button-primary-border', 'default' => '#1e87f0' ),
            'button_primary_text'        => array( 'label' => 'Button Primary (Text)', 'css_var' => '--nnaxfuf-button-primary-text', 'default' => '#ffffff' ),
            'button_primary_hover_bg'    => array( 'label' => 'Button Primary (Hover Hintergrund)', 'css_var' => '--nnaxfuf-button-primary-hover-bg', 'default' => '#0f7ae5' ),
            'button_primary_hover_border'=> array( 'label' => 'Button Primary (Hover Border)', 'css_var' => '--nnaxfuf-button-primary-hover-border', 'default' => '#0f7ae5' ),
            'button_primary_hover_text'  => array( 'label' => 'Button Primary (Hover Text)', 'css_var' => '--nnaxfuf-button-primary-hover-text', 'default' => '#ffffff' ),
            'button_secondary_bg'          => array( 'label' => 'Button Secondary (Hintergrund)', 'css_var' => '--nnaxfuf-button-secondary-bg', 'default' => '#222222' ),
            'button_secondary_border'      => array( 'label' => 'Button Secondary (Border)', 'css_var' => '--nnaxfuf-button-secondary-border', 'default' => '#222222' ),
            'button_secondary_text'        => array( 'label' => 'Button Secondary (Text)', 'css_var' => '--nnaxfuf-button-secondary-text', 'default' => '#ffffff' ),
            'button_secondary_hover_bg'    => array( 'label' => 'Button Secondary (Hover Hintergrund)', 'css_var' => '--nnaxfuf-button-secondary-hover-bg', 'default' => '#151515' ),
            'button_secondary_hover_border'=> array( 'label' => 'Button Secondary (Hover Border)', 'css_var' => '--nnaxfuf-button-secondary-hover-border', 'default' => '#151515' ),
            'button_secondary_hover_text'  => array( 'label' => 'Button Secondary (Hover Text)', 'css_var' => '--nnaxfuf-button-secondary-hover-text', 'default' => '#ffffff' ),
            'button_danger_bg'          => array( 'label' => 'Button Danger (Hintergrund)', 'css_var' => '--nnaxfuf-button-danger-bg', 'default' => '#f0506e' ),
            'button_danger_border'      => array( 'label' => 'Button Danger (Border)', 'css_var' => '--nnaxfuf-button-danger-border', 'default' => '#f0506e' ),
            'button_danger_text'        => array( 'label' => 'Button Danger (Text)', 'css_var' => '--nnaxfuf-button-danger-text', 'default' => '#ffffff' ),
            'button_danger_hover_bg'    => array( 'label' => 'Button Danger (Hover Hintergrund)', 'css_var' => '--nnaxfuf-button-danger-hover-bg', 'default' => '#ee395b' ),
            'button_danger_hover_border'=> array( 'label' => 'Button Danger (Hover Border)', 'css_var' => '--nnaxfuf-button-danger-hover-border', 'default' => '#ee395b' ),
            'button_danger_hover_text'  => array( 'label' => 'Button Danger (Hover Text)', 'css_var' => '--nnaxfuf-button-danger-hover-text', 'default' => '#ffffff' ),
            'surface'      => array( 'label' => 'Surface (Hintergrund)', 'css_var' => '--nnaxfuf-color-surface', 'default' => '#ffffff' ),
            'surface_2'    => array( 'label' => 'Surface 2', 'css_var' => '--nnaxfuf-color-surface-2', 'default' => '#f5f5f5' ),
            'text'         => array( 'label' => 'Text', 'css_var' => '--nnaxfuf-color-text', 'default' => '#111111' ),
            'muted'        => array( 'label' => 'Muted Text', 'css_var' => '--nnaxfuf-color-muted', 'default' => '#666666' ),
            'border'       => array( 'label' => 'Border', 'css_var' => '--nnaxfuf-color-border', 'default' => '#d9d9d9' ),
            'focus'        => array( 'label' => 'Focus Ring', 'css_var' => '--nnaxfuf-color-focus', 'default' => '#1e87f0' ),
            'row_hover'    => array( 'label' => 'Row Hover', 'css_var' => '--nnaxfuf-row-hover', 'default' => '#f5f5f5' ),
            'row_selected' => array( 'label' => 'Row Selected', 'css_var' => '--nnaxfuf-row-selected', 'default' => '#d7ebff' ),
            'notification_primary'    => array( 'label' => 'Notification Primary (Text)', 'css_var' => '--nnaxfuf-notification-primary-text', 'default' => '#0099cc' ),
            'notification_primary_bg' => array( 'label' => 'Notification Primary (Background)', 'css_var' => '--nnaxfuf-notification-primary-bg', 'default' => '#f8f8f8' ),
            'notification_success'    => array( 'label' => 'Notification Success (Text)', 'css_var' => '--nnaxfuf-notification-success-text', 'default' => '#afcc07' ),
            'notification_success_bg' => array( 'label' => 'Notification Success (Background)', 'css_var' => '--nnaxfuf-notification-success-bg', 'default' => '#f8f8f8' ),
            'notification_warning'    => array( 'label' => 'Notification Warning (Text)', 'css_var' => '--nnaxfuf-notification-warning-text', 'default' => '#faa05a' ),
            'notification_warning_bg' => array( 'label' => 'Notification Warning (Background)', 'css_var' => '--nnaxfuf-notification-warning-bg', 'default' => '#f8f8f8' ),
            'notification_danger'     => array( 'label' => 'Notification Danger (Text)', 'css_var' => '--nnaxfuf-notification-danger-text', 'default' => '#e4013a' ),
            'notification_danger_bg'  => array( 'label' => 'Notification Danger (Background)', 'css_var' => '--nnaxfuf-notification-danger-bg', 'default' => '#f8f8f8' ),
            'table-head_bg'  => array( 'label' => 'Table Head (Background)', 'css_var' => '--nnaxfuf-table-head-bg', 'default' => '#fff' ),
        );
    }

    /**
     * Badge color definitions.
     *
     * key => array(label, css_var, default)
     */
    public static function get_badge_color_definitions() {
        return array(
            'generic' => array( 'label' => 'Standard', 'css_var' => '--nnaxfuf-badge-generic', 'default' => '#666666' ),
            'pdf'     => array( 'label' => 'PDF', 'css_var' => '--nnaxfuf-badge-pdf', 'default' => '#e53935' ),
            'word'    => array( 'label' => 'Word', 'css_var' => '--nnaxfuf-badge-word', 'default' => '#1e88e5' ),
            'excel'   => array( 'label' => 'Excel', 'css_var' => '--nnaxfuf-badge-excel', 'default' => '#43a047' ),
            'ppt'     => array( 'label' => 'PowerPoint', 'css_var' => '--nnaxfuf-badge-ppt', 'default' => '#fb8c00' ),
            'image'   => array( 'label' => 'Bilder', 'css_var' => '--nnaxfuf-badge-image', 'default' => '#8e24aa' ),
            'text'    => array( 'label' => 'Text', 'css_var' => '--nnaxfuf-badge-text', 'default' => '#546e7a' ),
            'archive' => array( 'label' => 'Archive', 'css_var' => '--nnaxfuf-badge-archive', 'default' => '#6d4c41' ),
        );
    }

    /**
     * Flat key => default map for UI colors.
     */
    public static function get_default_ui_colors() {
        $defaults = array();
        foreach ( self::get_ui_color_definitions() as $key => $def ) {
            $defaults[ $key ] = isset( $def['default'] ) ? (string) $def['default'] : '';
        }
        return $defaults;
    }

    /**
     * Flat key => default map for badge colors.
     */
    public static function get_default_badge_colors() {
        $defaults = array();
        foreach ( self::get_badge_color_definitions() as $key => $def ) {
            $defaults[ $key ] = isset( $def['default'] ) ? (string) $def['default'] : '';
        }
        return $defaults;
    }

    /**
     * Get default plugin settings.
     */
    public static function get_default_settings() {
        return array(
            'main_headline' => __( 'My Files', 'nnax-frontend-user-files' ),
            'allowed_roles' => array( 'administrator', 'editor', 'author' ),
            'file_size'     => 10, // MB
            'load_plugin_uikit' => 1,
            'enforce_mime_check' => 1,
            'delete_all_on_uninstall' => 0,
            'max_file_size_mb'   => 0,
            'blocked_types'      => 'svg',
            'items_per_page'     => 20,
            'custom_meta_fields' => self::get_default_custom_meta_fields(),
            'ui_colors'     => self::get_default_ui_colors(),
            'badge_colors'  => self::get_default_badge_colors(),
            'quota'         => array(), // role_key => array(max_files, max_mb)
            'share_options' => array(
                'allow_time_limit'   => 0,
                'allow_owner_rights' => 0,
                'allow_shared_folder_file_delete' => 0,
            ),
        );
    }

    /**
     * Get plugin settings.
     */
    public static function get_settings() {
        $defaults = self::get_default_settings();

        $saved = get_option( NNAXFUF_Plugin::OPTION_SETTINGS, array() );
        $settings = wp_parse_args( $saved, $defaults );

        if ( array_key_exists( 'custom_meta_fields', $saved ) ) {
            $settings['custom_meta_fields'] = self::normalize_custom_meta_fields( $saved['custom_meta_fields'] );
        } elseif ( isset( $saved['fields'] ) && is_array( $saved['fields'] ) ) {
            $legacy_fields = array();
            if ( ! empty( $saved['fields']['description'] ) ) {
                $legacy_fields[] = array(
                    'key'   => '_nnaxfuf_desc',
                    'label' => 'Beschreibung',
                    'type'  => 'textarea',
                );
            }
            if ( ! empty( $saved['fields']['extra_date'] ) ) {
                $legacy_fields[] = array(
                    'key'   => '_nnaxfuf_extra_date',
                    'label' => 'Extra-Datum',
                    'type'  => 'date',
                );
            }
            if ( ! empty( $saved['fields']['location'] ) ) {
                $legacy_fields[] = array(
                    'key'   => '_nnaxfuf_location',
                    'label' => 'Ort',
                    'type'  => 'text',
                );
            }
            $settings['custom_meta_fields'] = self::normalize_custom_meta_fields( $legacy_fields );
        } else {
            $settings['custom_meta_fields'] = self::normalize_custom_meta_fields( $defaults['custom_meta_fields'] );
        }

        // wp_parse_args merged nested arrays not deeply enough for partial saves.
        $saved_ui     = ( isset( $saved['ui_colors'] ) && is_array( $saved['ui_colors'] ) ) ? $saved['ui_colors'] : array();
        $saved_badges = ( isset( $saved['badge_colors'] ) && is_array( $saved['badge_colors'] ) ) ? $saved['badge_colors'] : array();
        $saved_share_options = ( isset( $saved['share_options'] ) && is_array( $saved['share_options'] ) ) ? $saved['share_options'] : array();

        $settings['ui_colors']    = wp_parse_args( $saved_ui, $defaults['ui_colors'] );
        $settings['badge_colors'] = wp_parse_args( $saved_badges, $defaults['badge_colors'] );

        // Normalize colors: empty/invalid saved values fall back to defaults.
        foreach ( $settings['ui_colors'] as $key => $value ) {
            $hex = sanitize_hex_color( (string) $value );
            if ( $hex ) {
                $settings['ui_colors'][ $key ] = $hex;
                continue;
            }
            $settings['ui_colors'][ $key ] = isset( $defaults['ui_colors'][ $key ] ) ? (string) $defaults['ui_colors'][ $key ] : '';
        }

        foreach ( $settings['badge_colors'] as $key => $value ) {
            $hex = sanitize_hex_color( (string) $value );
            if ( $hex ) {
                $settings['badge_colors'][ $key ] = $hex;
                continue;
            }
            $settings['badge_colors'][ $key ] = isset( $defaults['badge_colors'][ $key ] ) ? (string) $defaults['badge_colors'][ $key ] : '';
        }

        $settings['share_options'] = wp_parse_args(
            $saved_share_options,
            isset( $defaults['share_options'] ) && is_array( $defaults['share_options'] ) ? $defaults['share_options'] : array()
        );
        $settings['load_plugin_uikit'] = ! empty( $settings['load_plugin_uikit'] ) ? 1 : 0;
        $settings['share_options']['allow_time_limit']   = ! empty( $settings['share_options']['allow_time_limit'] ) ? 1 : 0;
        $settings['share_options']['allow_owner_rights'] = ! empty( $settings['share_options']['allow_owner_rights'] ) ? 1 : 0;
        $settings['share_options']['allow_shared_folder_file_delete'] = ! empty( $settings['share_options']['allow_shared_folder_file_delete'] ) ? 1 : 0;
        $settings['delete_all_on_uninstall'] = ! empty( $settings['delete_all_on_uninstall'] ) ? 1 : 0;

        return $settings;
    }

    /**
     * Detect whether YOOtheme is active.
     *
     * @return bool
     */
    public static function is_yootheme_active() {
        $theme = wp_get_theme();
        if ( $theme ) {
            $template   = strtolower( (string) $theme->get_template() );
            $stylesheet = strtolower( (string) $theme->get_stylesheet() );
            $name       = strtolower( (string) $theme->get( 'Name' ) );
            if ( strpos( $template, 'yootheme' ) !== false || strpos( $stylesheet, 'yootheme' ) !== false || strpos( $name, 'yootheme' ) !== false ) {
                return true;
            }
        }

        return defined( 'YOO_THEME_VERSION' ) || class_exists( '\\YOOtheme\\Application' );
    }

    /**
     * Whether time limits for sharing are currently enabled in settings.
     *
     * @return bool
     */
    public static function is_share_time_limit_enabled() {
        static $enabled = null;

        if ( $enabled !== null ) {
            return (bool) $enabled;
        }

        $settings      = self::get_settings();
        $share_options = isset( $settings['share_options'] ) && is_array( $settings['share_options'] )
            ? $settings['share_options']
            : array();

        $enabled = ! empty( $share_options['allow_time_limit'] );
        return (bool) $enabled;
    }

    /**
     * Whether owner-rights escalation for sharing is currently enabled in settings.
     *
     * @return bool
     */
    public static function is_share_owner_rights_enabled() {
        static $enabled = null;

        if ( $enabled !== null ) {
            return (bool) $enabled;
        }

        $settings      = self::get_settings();
        $share_options = isset( $settings['share_options'] ) && is_array( $settings['share_options'] )
            ? $settings['share_options']
            : array();

        $enabled = ! empty( $share_options['allow_owner_rights'] );
        return (bool) $enabled;
    }

    /**
     * Whether the Sharing Addon is active and providing frontend UI/logic.
     *
     * We keep share metadata in the database regardless of the Addon's state,
     * but when the Addon is inactive the core should behave as if no shares
     * exist on the frontend. This helper centralizes that check.
     *
     * @return bool
     */
    public static function is_sharing_addon_active() {
        static $active = null;

        if ( $active !== null ) {
            return (bool) $active;
        }

        if ( ! defined( 'NNAXFUF_SHARE_ADDON_PATH' ) || ! defined( 'NNAXFUF_SHARE_ADDON_URL' ) ) {
            $active = false;
            return false;
        }

        $addon_share_template = rtrim( NNAXFUF_SHARE_ADDON_PATH, '/' ) . '/templates/partials/modals/share.php';
        $addon_share_script = rtrim( NNAXFUF_SHARE_ADDON_PATH, '/' ) . '/assets/js/modules/share.js';

        $active = ( file_exists( $addon_share_template ) || file_exists( $addon_share_script ) );
        return (bool) $active;
    }

    /**
     * Whether shared users can delete files inside shared folders.
     *
     * @return bool
     */
    public static function is_shared_folder_file_delete_enabled() {
        static $enabled = null;

        if ( $enabled !== null ) {
            return (bool) $enabled;
        }

        $settings      = self::get_settings();
        $share_options = isset( $settings['share_options'] ) && is_array( $settings['share_options'] )
            ? $settings['share_options']
            : array();

        $enabled = ! empty( $share_options['allow_shared_folder_file_delete'] );
        return (bool) $enabled;
    }

    /**
     * Get user role.
     */
    public static function get_current_user_role() {
        $user = wp_get_current_user();
        if ( empty( $user->roles ) || ! is_array( $user->roles ) ) {
            return '';
        }
        return $user->roles[0];
    }

    /**
     * Get user usage (file count and total bytes).
     */
    public static function get_user_usage( $user_id ) {
        $args = array(
            'post_type'      => NNAXFUF_Plugin::CPT,
            // Trash items count toward quota to prevent bypassing limits via trash.
            'post_status'    => array( 'publish', 'trash' ),
            'posts_per_page' => -1,
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Owner filter is required for per-user quota calculation.
            'meta_query'     => array(
                array(
                    'key'   => NNAXFUF_Plugin::META_OWNER,
                    'value' => $user_id,
                ),
            ),
            'fields' => 'ids',
        );

        $posts       = get_posts( $args );
        $file_count  = count( $posts );
        $total_bytes = 0;

        foreach ( $posts as $fid ) {
            $path = get_post_meta( $fid, NNAXFUF_Plugin::META_FILEPATH, true );
            if ( $path && file_exists( $path ) ) {
                $total_bytes += filesize( $path );
            }
        }

        return array(
            'file_count'  => $file_count,
            'total_bytes' => $total_bytes,
        );
    }

    /**
     * Get user quota for a specific user.
     */
    public static function get_user_quota_for( $user_id ) {
        $settings = self::get_settings();
        $quota = array(
            'max_files' => 0,
            'max_mb'    => 0,
        );

        if ( empty( $settings['quota'] ) || ! is_array( $settings['quota'] ) ) {
            return $quota;
        }

        $user = get_userdata( $user_id );
        if ( ! $user || empty( $user->roles ) || ! is_array( $user->roles ) ) {
            return $quota;
        }

        $role_key = $user->roles[0];
        if ( isset( $settings['quota'][ $role_key ] ) ) {
            $role_quota = $settings['quota'][ $role_key ];
            $quota['max_files'] = ! empty( $role_quota['max_files'] ) ? (int) $role_quota['max_files'] : 0;
            $quota['max_mb']    = ! empty( $role_quota['max_mb'] )    ? (float) $role_quota['max_mb']    : 0;
        }

        return $quota;
    }

    /**
     * Get file type info from URL.
     */
    public static function nnaxfuf_get_file_type_from_url( $file_url ) {
        $path = wp_parse_url( $file_url, PHP_URL_PATH );
        $ext  = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) );

        $map = array(
            'jpg'  => array( 'type' => 'image', 'label' => 'IMG' ),
            'jpeg' => array( 'type' => 'image', 'label' => 'IMG' ),
            'png'  => array( 'type' => 'image', 'label' => 'IMG' ),
            'gif'  => array( 'type' => 'image', 'label' => 'IMG' ),
            'webp' => array( 'type' => 'image', 'label' => 'IMG' ),
            'svg'  => array( 'type' => 'image', 'label' => 'SVG' ),
            'pdf'  => array( 'type' => 'pdf',   'label' => 'PDF' ),
            'doc'  => array( 'type' => 'word',  'label' => 'DOC' ),
            'docx' => array( 'type' => 'word',  'label' => 'DOCX' ),
            'xls'  => array( 'type' => 'excel', 'label' => 'XLS' ),
            'xlsx' => array( 'type' => 'excel', 'label' => 'XLSX' ),
            'ppt'  => array( 'type' => 'ppt',   'label' => 'PPT' ),
            'pptx' => array( 'type' => 'ppt',   'label' => 'PPTX' ),
            'txt'  => array( 'type' => 'text',  'label' => 'TXT' ),
            'zip'  => array( 'type' => 'archive', 'label' => 'ZIP' ),
            'rar'  => array( 'type' => 'archive', 'label' => 'RAR' ),
            '7z'   => array( 'type' => 'archive', 'label' => '7Z' ),
        );

        if ( isset( $map[ $ext ] ) ) {
            return $map[ $ext ];
        }

        return array(
            'type'  => 'generic',
            'label' => strtoupper( $ext ?: '?' ),
        );
    }

    /**
     * Dateityp-Filteroptionen für das Frontend-Menü.
     */
    public static function get_file_type_filter_options() {
        return array(
            'all'     => __( 'All types', 'nnax-frontend-user-files' ),
            'pdf'     => __( 'PDF', 'nnax-frontend-user-files' ),
            'image'   => __( 'Images', 'nnax-frontend-user-files' ),
            'doc'     => __( 'Documents', 'nnax-frontend-user-files' ),
            'sheet'   => __( 'Spreadsheets', 'nnax-frontend-user-files' ),
            'archive' => __( 'Archives', 'nnax-frontend-user-files' ),
        );
    }

    /**
     * Erlaubte Typ-Filterkeys auf einen stabilen Zustand normalisieren.
     */
    public static function sanitize_file_type_filter( $raw_type ) {
        $type    = sanitize_key( (string) $raw_type );
        $allowed = array_keys( self::get_file_type_filter_options() );
        if ( ! in_array( $type, $allowed, true ) ) {
            return 'all';
        }
        return $type;
    }

    /**
     * Dateiendung eines Datei-Items ermitteln.
     * Reihenfolge: _nnaxfuf_file_path, _nnaxfuf_file_url, post_title.
     */
    public static function get_item_extension( $item_id ) {
        $item_id = (int) $item_id;
        if ( $item_id <= 0 ) {
            return '';
        }

        $ext = '';

        $file_path = (string) get_post_meta( $item_id, NNAXFUF_Plugin::META_FILEPATH, true );
        if ( $file_path !== '' ) {
            $ext = strtolower( (string) pathinfo( $file_path, PATHINFO_EXTENSION ) );
        }

        if ( $ext === '' ) {
            $file_url = (string) get_post_meta( $item_id, NNAXFUF_Plugin::META_FILEURL, true );
            $url_path = (string) wp_parse_url( $file_url, PHP_URL_PATH );
            $ext      = strtolower( (string) pathinfo( $url_path, PATHINFO_EXTENSION ) );
        }

        if ( $ext === '' ) {
            $title = self::get_raw_item_title( $item_id );
            $ext   = strtolower( (string) pathinfo( $title, PATHINFO_EXTENSION ) );
        }

        return preg_replace( '/[^a-z0-9]/', '', $ext );
    }

    /**
     * Prüft, ob ein Datei-Item zum gewünschten Typfilter gehört.
     */
    public static function item_matches_file_type_filter( $item_id, $type_filter ) {
        $type_filter = self::sanitize_file_type_filter( $type_filter );
        if ( $type_filter === 'all' ) {
            return true;
        }

        $ext = self::get_item_extension( $item_id );
        if ( $ext === '' ) {
            return false;
        }

        $map = array(
            'pdf'     => array( 'pdf' ),
            'image'   => array( 'jpg', 'jpeg', 'png', 'gif', 'webp' ),
            'doc'     => array( 'doc', 'docx', 'txt', 'rtf' ),
            'sheet'   => array( 'xls', 'xlsx', 'csv' ),
            'archive' => array( 'zip', 'rar', '7z' ),
        );

        if ( empty( $map[ $type_filter ] ) ) {
            return true;
        }

        return in_array( $ext, $map[ $type_filter ], true );
    }

    /**
     * Get user-friendly download URL.
     */
    public static function get_download_url( $file_id, $mode = 'download' ) {
        $post = get_post( $file_id );
        if ( ! $post || $post->post_type !== NNAXFUF_Plugin::CPT ) {
            return '';
        }

        $title    = $post->post_title;
        $file_url = get_post_meta( $file_id, NNAXFUF_Plugin::META_FILEURL, true );
        $ext      = strtolower( pathinfo( wp_parse_url( $file_url, PHP_URL_PATH ), PATHINFO_EXTENSION ) );
        
        // Remove extension from title if it exists to avoid double extensions (e.g. test.pdf -> test-pdf.pdf)
        if ( ! empty( $ext ) && strtolower( substr( $title, -strlen( $ext ) - 1 ) ) === '.' . $ext ) {
            $title = substr( $title, 0, -strlen( $ext ) - 1 );
        }

        $title = sanitize_title( $title );
        $filename = $title . ( $ext ? '.' . $ext : '' );
        
        // Build pretty URL: /nnaxfuf-download/ID/filename.ext
        $url = home_url( '/' . NNAXFUF_Plugin::DOWNLOAD_ENDPOINT . '/' . $file_id . '/' . $filename );

        if ( $mode === 'inline' ) {
            $url = trailingslashit( $url ) . 'inline/';
        }

        return wp_nonce_url( $url, 'nnaxfuf_download', 'nnaxfuf_download_nonce' );
    }

    /**
     * Build frontend file-manager URL with pretty path segments when possible.
     *
     * Supported state keys:
     * - NNAXFUF_Plugin::FOLDER_VAR => int folder id (0 = root)
     * - NNAXFUF_Plugin::VIEW_VAR   => default|trash
     * - NNAXFUF_Plugin::FILTER_VAR => all|owned|shared|shared_by_me
     * - NNAXFUF_Plugin::TYPE_VAR   => all|pdf|image|doc|sheet|archive
     */
    public static function get_file_manager_url( $state = array() ) {
        // Read-only query vars used for URL state generation only.
        // phpcs:disable WordPress.Security.NonceVerification.Recommended
        $state = is_array( $state ) ? $state : array();

        $current_view = get_query_var( NNAXFUF_Plugin::VIEW_VAR );
        $current_view = $current_view ? sanitize_key( (string) $current_view ) : 'default';

        $current_filter = get_query_var( NNAXFUF_Plugin::FILTER_VAR );
        $current_filter = $current_filter ? sanitize_key( (string) $current_filter ) : 'all';

        $current_folder = (int) get_query_var( NNAXFUF_Plugin::FOLDER_VAR );
        $current_paged  = (int) get_query_var( NNAXFUF_Plugin::PAGED_VAR );
        $current_s      = get_query_var( NNAXFUF_Plugin::SEARCH_VAR );
        $current_type   = self::sanitize_file_type_filter( get_query_var( NNAXFUF_Plugin::TYPE_VAR ) );
        $current_sort   = get_query_var( NNAXFUF_Plugin::SORT_VAR );
        $current_sort   = $current_sort ? sanitize_key( (string) $current_sort ) : 'name';
        $current_order  = get_query_var( NNAXFUF_Plugin::ORDER_VAR );
        $current_order  = $current_order ? sanitize_key( (string) $current_order ) : 'asc';
        $current_show   = get_query_var( NNAXFUF_Plugin::SHOW_VAR );
        $current_show   = $current_show ? sanitize_key( (string) $current_show ) : '';

        $args = wp_parse_args(
            $state,
            array(
                NNAXFUF_Plugin::FOLDER_VAR => $current_folder,
                NNAXFUF_Plugin::VIEW_VAR   => $current_view,
                NNAXFUF_Plugin::FILTER_VAR => $current_filter,
                NNAXFUF_Plugin::PAGED_VAR  => $current_paged,
                NNAXFUF_Plugin::SEARCH_VAR => $current_s,
                NNAXFUF_Plugin::TYPE_VAR   => $current_type,
                NNAXFUF_Plugin::SORT_VAR   => $current_sort,
                NNAXFUF_Plugin::ORDER_VAR  => $current_order,
                NNAXFUF_Plugin::SHOW_VAR   => $current_show,
            )
        );

        // If caller explicitly changes filter/search but does not provide a page,
        // reset pagination to page 1 to avoid empty result pages.
        $explicit_filter_change = array_key_exists( NNAXFUF_Plugin::FILTER_VAR, $state );
        $explicit_search_change = array_key_exists( NNAXFUF_Plugin::SEARCH_VAR, $state );
        $explicit_type_change   = array_key_exists( NNAXFUF_Plugin::TYPE_VAR, $state );
        $explicit_sort_change   = array_key_exists( NNAXFUF_Plugin::SORT_VAR, $state ) || array_key_exists( NNAXFUF_Plugin::ORDER_VAR, $state );
        $explicit_show_change   = array_key_exists( NNAXFUF_Plugin::SHOW_VAR, $state );
        $explicit_paged_change  = array_key_exists( NNAXFUF_Plugin::PAGED_VAR, $state );
        if ( ( $explicit_filter_change || $explicit_search_change || $explicit_type_change || $explicit_sort_change || $explicit_show_change ) && ! $explicit_paged_change ) {
            $args[ NNAXFUF_Plugin::PAGED_VAR ] = 1;
        }

        $view = isset( $args[ NNAXFUF_Plugin::VIEW_VAR ] ) ? sanitize_key( (string) $args[ NNAXFUF_Plugin::VIEW_VAR ] ) : 'default';
        if ( 'trash' !== $view ) {
            $view = 'default';
        }

        $filter = isset( $args[ NNAXFUF_Plugin::FILTER_VAR ] ) ? sanitize_key( (string) $args[ NNAXFUF_Plugin::FILTER_VAR ] ) : 'all';
        if ( ! in_array( $filter, array( 'all', 'owned', 'shared', 'shared_by_me' ), true ) ) {
            $filter = 'all';
        }

        $folder = isset( $args[ NNAXFUF_Plugin::FOLDER_VAR ] ) ? (int) $args[ NNAXFUF_Plugin::FOLDER_VAR ] : 0;
        if ( $folder < 0 ) {
            $folder = 0;
        }

        $paged = isset( $args[ NNAXFUF_Plugin::PAGED_VAR ] ) ? max( 1, (int) $args[ NNAXFUF_Plugin::PAGED_VAR ] ) : 1;
        $search = isset( $args[ NNAXFUF_Plugin::SEARCH_VAR ] ) ? trim( (string) $args[ NNAXFUF_Plugin::SEARCH_VAR ] ) : '';
        $type   = self::sanitize_file_type_filter( isset( $args[ NNAXFUF_Plugin::TYPE_VAR ] ) ? $args[ NNAXFUF_Plugin::TYPE_VAR ] : 'all' );
        $sort   = isset( $args[ NNAXFUF_Plugin::SORT_VAR ] ) ? sanitize_key( (string) $args[ NNAXFUF_Plugin::SORT_VAR ] ) : 'name';
        if ( ! in_array( $sort, array( 'name', 'size', 'date' ), true ) ) {
            $sort = 'name';
        }
        $order  = isset( $args[ NNAXFUF_Plugin::ORDER_VAR ] ) ? sanitize_key( (string) $args[ NNAXFUF_Plugin::ORDER_VAR ] ) : 'asc';
        if ( ! in_array( $order, array( 'asc', 'desc' ), true ) ) {
            $order = 'asc';
        }
        $show   = isset( $args[ NNAXFUF_Plugin::SHOW_VAR ] ) ? sanitize_key( (string) $args[ NNAXFUF_Plugin::SHOW_VAR ] ) : '';
        if ( 'all' !== $show ) {
            $show = '';
        }

        $base_url = '';
        $page_id  = get_queried_object_id();
        if ( ! empty( get_option( 'permalink_structure' ) ) && $page_id > 0 ) {
            $base_url = get_permalink( $page_id );
        }

        if ( $base_url ) {
            $base_url = trailingslashit( $base_url );
            $segments = array( 'nnaxfuf' );

            if ( 'trash' === $view ) {
                $segments[] = 'view';
                $segments[] = 'trash';
            }

            if ( 'all' !== $filter ) {
                $segments[] = 'filter';
                $segments[] = $filter;
            }

            if ( 'all' !== $type ) {
                $segments[] = 'type';
                $segments[] = $type;
            }

            if ( $folder > 0 ) {
                $segments[] = 'folder';
                $segments[] = (string) $folder;
            }

            if ( $paged > 1 ) {
                $segments[] = 'paged';
                $segments[] = (string) $paged;
            }

            if ( '' !== $search ) {
                $segments[] = 'search';
                $segments[] = urlencode( $search );
            }

            $pretty_url = trailingslashit( $base_url . implode( '/', $segments ) );
            $extra_query = array();
            if ( 'name' !== $sort ) {
                $extra_query[ NNAXFUF_Plugin::SORT_VAR ] = $sort;
            }
            if ( 'asc' !== $order ) {
                $extra_query[ NNAXFUF_Plugin::ORDER_VAR ] = $order;
            }
            if ( 'all' === $show ) {
                $extra_query[ NNAXFUF_Plugin::SHOW_VAR ] = 'all';
            }

            return empty( $extra_query ) ? $pretty_url : add_query_arg( $extra_query, $pretty_url );
        }

        $state_keys = array(
            NNAXFUF_Plugin::FOLDER_VAR,
            NNAXFUF_Plugin::VIEW_VAR,
            NNAXFUF_Plugin::FILTER_VAR,
            NNAXFUF_Plugin::PAGED_VAR,
            NNAXFUF_Plugin::SEARCH_VAR,
            NNAXFUF_Plugin::TYPE_VAR,
            NNAXFUF_Plugin::SORT_VAR,
            NNAXFUF_Plugin::ORDER_VAR,
            NNAXFUF_Plugin::SHOW_VAR,
        );

        $fallback_source = '';
        if ( wp_doing_ajax() ) {
            $fallback_source = (string) wp_get_referer();
        }
        if ( '' === $fallback_source ) {
            $fallback_source = home_url( '/' );
        }

        $fallback_base = remove_query_arg( $state_keys, $fallback_source );

        $query_args = array();
        if ( 'trash' === $view ) {
            $query_args[ NNAXFUF_Plugin::VIEW_VAR ] = 'trash';
        }
        if ( 'all' !== $filter ) {
            $query_args[ NNAXFUF_Plugin::FILTER_VAR ] = $filter;
        }
        if ( $folder > 0 ) {
            $query_args[ NNAXFUF_Plugin::FOLDER_VAR ] = $folder;
        }
        if ( $paged > 1 ) {
            $query_args[ NNAXFUF_Plugin::PAGED_VAR ] = $paged;
        }
        if ( '' !== $search ) {
            $query_args[ NNAXFUF_Plugin::SEARCH_VAR ] = $search;
        }
        if ( 'all' !== $type ) {
            $query_args[ NNAXFUF_Plugin::TYPE_VAR ] = $type;
        }
        if ( 'name' !== $sort ) {
            $query_args[ NNAXFUF_Plugin::SORT_VAR ] = $sort;
        }
        if ( 'asc' !== $order ) {
            $query_args[ NNAXFUF_Plugin::ORDER_VAR ] = $order;
        }
        if ( 'all' === $show ) {
            $query_args[ NNAXFUF_Plugin::SHOW_VAR ] = 'all';
        }

        if ( empty( $query_args ) ) {
            return $fallback_base;
        }

        // phpcs:enable WordPress.Security.NonceVerification.Recommended
        return add_query_arg( $query_args, $fallback_base );
    }

    /**
     * Build ZIP download URL for one or more items.
     *
     * Supported tokens:
     * - file:123 / f:123
     * - folder:456 / d:456
     */
    public static function get_zip_download_url( $items = array(), $download_name = 'download' ) {
        $items = is_array( $items ) ? $items : array( $items );
        $tokens = array();

        foreach ( $items as $item ) {
            $item = sanitize_text_field( (string) $item );
            if ( $item === '' ) {
                continue;
            }

            if ( preg_match( '/^(file|f):(\d+)$/', $item, $matches ) ) {
                $tokens[] = 'file:' . (int) $matches[2];
                continue;
            }

            if ( preg_match( '/^(folder|d):(\d+)$/', $item, $matches ) ) {
                $tokens[] = 'folder:' . (int) $matches[2];
            }
        }

        $tokens = array_values( array_unique( $tokens ) );
        if ( empty( $tokens ) ) {
            return '';
        }

        $download_name = sanitize_file_name( (string) $download_name );
        if ( $download_name === '' ) {
            $download_name = 'download';
        }

        $encoded_items = self::base64url_encode( implode( ',', $tokens ) );
        if ( $encoded_items === '' ) {
            return '';
        }

        $download_name = sanitize_title( $download_name );
        if ( $download_name === '' ) {
            $download_name = 'download';
        }

        return wp_nonce_url(
            home_url( '/' . NNAXFUF_Plugin::DOWNLOAD_ENDPOINT . '/zip/' . $encoded_items . '/' . rawurlencode( $download_name ) . '/' ),
            'nnaxfuf_zip_download',
            'nnaxfuf_zip_nonce'
        );
    }

    /**
     * Base URL for dynamic ZIP links created in JS.
     */
    public static function get_zip_download_base_url() {
        return home_url( '/' . NNAXFUF_Plugin::DOWNLOAD_ENDPOINT . '/zip/' );
    }

    /**
     * Convenience URL for a single folder as ZIP.
     */
    public static function get_folder_zip_download_url( $folder_id ) {
        $folder_id = (int) $folder_id;
        if ( $folder_id <= 0 ) {
            return '';
        }

        $folder_name = self::get_raw_item_title( $folder_id );
        if ( empty( $folder_name ) ) {
            $folder_name = 'ordner-' . $folder_id;
        }
        $slug = sanitize_title( $folder_name );
        if ( $slug === '' ) {
            $slug = 'ordner-' . $folder_id;
        }

        return self::get_zip_download_url( array( 'folder:' . $folder_id ), $slug );
    }

    /**
     * Normalize one share identifier.
     *
     * Supports numeric user ids and role keys in the format "role:{slug}".
     *
     * @param mixed $id Raw identifier.
     * @return int|string|false
     */
    private static function normalize_share_identifier( $id ) {
        if ( is_numeric( $id ) ) {
            $uid = (int) $id;
            return $uid > 0 ? $uid : false;
        }

        $id = (string) $id;
        if ( strpos( $id, 'role:' ) === 0 ) {
            $role_slug = sanitize_key( substr( $id, 5 ) );
            if ( $role_slug !== '' ) {
                return 'role:' . $role_slug;
            }
        }

        return false;
    }

    /**
     * Parse and normalize raw share rules.
     *
     * @param mixed $raw_rules Stored meta value.
     * @return array<int|string,array<string,mixed>>
     */
    public static function normalize_share_rules( $raw_rules ) {
        $raw_rules = is_array( $raw_rules ) ? $raw_rules : array();
        $rules     = array();

        foreach ( $raw_rules as $raw_user_id => $raw_rule ) {
            $share_id = self::normalize_share_identifier( $raw_user_id );
            if ( false === $share_id || ! is_array( $raw_rule ) ) {
                continue;
            }

            $starts_ts  = isset( $raw_rule['starts_ts'] ) ? (int) $raw_rule['starts_ts'] : 0;
            $expires_ts = isset( $raw_rule['expires_ts'] ) ? (int) $raw_rule['expires_ts'] : 0;
            $starts_ts  = max( 0, $starts_ts );
            $expires_ts = max( 0, $expires_ts );

            if ( $starts_ts > 0 && $expires_ts > 0 && $starts_ts > $expires_ts ) {
                $tmp = $starts_ts;
                $starts_ts = $expires_ts;
                $expires_ts = $tmp;
            }

            $rules[ $share_id ] = array(
                'starts_ts'   => $starts_ts,
                'expires_ts'   => max( 0, $expires_ts ),
                'owner_rights' => ! empty( $raw_rule['owner_rights'] ),
                'can_delete'   => ! empty( $raw_rule['can_delete'] ),
            );
        }

        return $rules;
    }

    /**
     * Parse a date string (YYYY-MM-DD) into a site-local start-of-day timestamp.
     *
     * @param string $date_raw Date string from form.
     * @return int Unix timestamp (0 means no limit / invalid date).
     */
    public static function parse_share_start_date( $date_raw ) {
        $date_raw = sanitize_text_field( (string) $date_raw );
        if ( $date_raw === '' || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date_raw ) ) {
            return 0;
        }

        $dt = date_create_immutable_from_format( 'Y-m-d H:i:s', $date_raw . ' 00:00:00', wp_timezone() );
        if ( ! $dt ) {
            return 0;
        }

        return (int) $dt->getTimestamp();
    }

    /**
     * Parse a date string (YYYY-MM-DD) into a site-local end-of-day timestamp.
     *
     * @param string $date_raw Date string from form.
     * @return int Unix timestamp (0 means no limit / invalid date).
     */
    public static function parse_share_expiry_date( $date_raw ) {
        $date_raw = sanitize_text_field( (string) $date_raw );
        if ( $date_raw === '' || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date_raw ) ) {
            return 0;
        }

        $dt = date_create_immutable_from_format( 'Y-m-d H:i:s', $date_raw . ' 23:59:59', wp_timezone() );
        if ( ! $dt ) {
            return 0;
        }

        return (int) $dt->getTimestamp();
    }

    /**
     * Return stored share rules for an item.
     *
     * @param int $item_id Item ID.
     * @return array<int,array<string,mixed>>
     */
    public static function get_item_share_rules( $item_id ) {
        $item_id    = (int) $item_id;
        $raw_rules  = get_post_meta( $item_id, NNAXFUF_Plugin::META_SHARE_RULES, true );
        $rules      = self::normalize_share_rules( $raw_rules );
        $legacy_ids = get_post_meta( $item_id, NNAXFUF_Plugin::META_SHARED, true );
        $legacy_ids = is_array( $legacy_ids ) ? array_map( 'intval', $legacy_ids ) : array();

        foreach ( $legacy_ids as $uid ) {
            $uid = (int) $uid;
            if ( $uid <= 0 || isset( $rules[ $uid ] ) ) {
                continue;
            }
            // Backward compatibility: plain shared list implies read/download with no expiry.
            $rules[ $uid ] = array(
                'starts_ts'   => 0,
                'expires_ts'   => 0,
                'owner_rights' => false,
                'can_delete'   => false,
            );
        }

        return $rules;
    }

    /**
     * Return active share rules only (non-expired).
     *
     * @param int $item_id Item ID.
     * @return array<int,array<string,mixed>>
     */
    public static function get_active_share_rules( $item_id ) {
        $rules = self::get_item_share_rules( $item_id );

        if ( ! self::is_share_time_limit_enabled() ) {
            return $rules;
        }

        $now   = current_time( 'timestamp' );

        return array_filter(
            $rules,
            function ( $rule ) use ( $now ) {
                if ( ! is_array( $rule ) ) {
                    return false;
                }
                $starts_ts  = isset( $rule['starts_ts'] ) ? (int) $rule['starts_ts'] : 0;
                $expires_ts = isset( $rule['expires_ts'] ) ? (int) $rule['expires_ts'] : 0;
                $is_started = ( $starts_ts <= 0 ) || ( $starts_ts <= $now );
                $not_expired = ( $expires_ts <= 0 ) || ( $expires_ts >= $now );
                return $is_started && $not_expired;
            }
        );
    }

    /**
     * Return active shared user IDs for an item.
     *
     * @param int $item_id Item ID.
     * @return array<int,int|string>
     */
    public static function get_active_shared_user_ids( $item_id ) {
        return array_values( array_keys( self::get_active_share_rules( $item_id ) ) );
    }

    /**
     * Apply/replace share rules for an item and keep legacy shared list in sync.
     *
     * @param int   $item_id Item ID.
     * @param array $share_rules Rules keyed by user id or role key.
     */
    public static function set_item_share_rules( $item_id, $share_rules ) {
        $item_id     = (int) $item_id;
        $share_rules = self::normalize_share_rules( $share_rules );

        if ( $item_id <= 0 ) {
            return;
        }

        $legacy_shared = array();
        foreach ( array_keys( $share_rules ) as $key ) {
            if ( is_numeric( $key ) ) {
                $uid = (int) $key;
                if ( $uid > 0 ) {
                    $legacy_shared[] = $uid;
                }
            }
        }

        update_post_meta( $item_id, NNAXFUF_Plugin::META_SHARE_RULES, $share_rules );
        update_post_meta( $item_id, NNAXFUF_Plugin::META_SHARED, array_values( array_unique( $legacy_shared ) ) );
    }

    /**
     * Build share context for a user + item.
     *
     * @param int $current_user_id User ID to test.
     * @param int $item_id Item ID.
     * @return array<string,mixed>
     */
    public static function get_share_context_for_user( $current_user_id, $item_id ) {
        $current_user_id = (int) $current_user_id;
        $item_id         = (int) $item_id;
        $owner_id        = (int) get_post_meta( $item_id, NNAXFUF_Plugin::META_OWNER, true );
        if ( $owner_id <= 0 ) {
            $owner_post = get_post( $item_id );
            $owner_id   = $owner_post ? (int) $owner_post->post_author : 0;
        }

        if ( $owner_id > 0 && $owner_id === $current_user_id ) {
            // Owner shortcut: owners always keep full rights regardless of
            // time limits or "owner_rights" options on shared rules.
            return array(
                'is_owner'      => true,
                'is_shared'     => false,
                'is_active'     => true,
                'can_access'    => true,
                'can_edit'      => true,
                'can_share'     => true,
                'can_delete'    => true,
                'owner_rights'  => true,
                'starts_ts'     => 0,
                'expires_ts'    => 0,
            );
        }

        // If the Sharing Addon is not active, treat all non-owner users as
        // having no share context. This preserves the share metadata but
        // prevents the core plugin from exposing shared items or share
        // controls when the Addon is disabled.
        if ( ! self::is_sharing_addon_active() ) {
            return array(
                'is_owner'      => false,
                'is_shared'     => false,
                'is_active'     => false,
                'can_access'    => false,
                'can_edit'      => false,
                'can_delete'    => false,
                'can_share'     => false,
                'owner_rights'  => false,
                'starts_ts'     => 0,
                'expires_ts'    => 0,
            );
        }

        $time_limit_enabled   = self::is_share_time_limit_enabled();
        $owner_rights_enabled = self::is_share_owner_rights_enabled();
        // At this point rules are already filtered by active time window.
        $active_rules         = self::get_active_share_rules( $item_id );
        
        // 1. Check for individual user rule
        $rule = isset( $active_rules[ $current_user_id ] ) && is_array( $active_rules[ $current_user_id ] )
            ? $active_rules[ $current_user_id ]
            : null;

        // 2. Check for role-based rules if no individual rule or to complement it
        $user = get_userdata( $current_user_id );
        $user_roles = $user ? (array) $user->roles : array();
        
        foreach ( $active_rules as $key => $r ) {
            if ( strpos( (string) $key, 'role:' ) === 0 ) {
                $role_name = substr( (string) $key, 5 );
                if ( in_array( $role_name, $user_roles, true ) ) {
                    // If we have a role match, we use it if we don't have an individual rule
                    // or if the role rule has "more" rights (like owner_rights).
                    if ( ! $rule ) {
                        $rule = $r;
                    } else {
                        // Merge logic: if any rule gives owner_rights, keep it.
                        if ( ! empty( $r['owner_rights'] ) ) {
                            $rule['owner_rights'] = true;
                        }
                        if ( ! empty( $r['can_delete'] ) ) {
                            $rule['can_delete'] = true;
                        }
                    }
                }
            }
        }

        if ( ! $rule ) {
            return array(
                'is_owner'      => false,
                'is_shared'     => false,
                'is_active'     => false,
                'can_access'    => false,
                'can_edit'      => false,
                'can_delete'    => false,
                'can_share'     => false,
                'owner_rights'  => false,
                'starts_ts'     => 0,
                'expires_ts'    => 0,
            );
        }

        $owner_rights = $owner_rights_enabled && ! empty( $rule['owner_rights'] );
        return array(
            'is_owner'      => false,
            'is_shared'     => true,
            'is_active'     => true,
            'can_access'    => true,
            'can_edit'      => $owner_rights,
            'can_delete'    => ( ! empty( $rule['can_delete'] ) ),
            'can_share'     => $owner_rights,
            'owner_rights'  => $owner_rights,
            'starts_ts'     => $time_limit_enabled && isset( $rule['starts_ts'] ) ? (int) $rule['starts_ts'] : 0,
            'expires_ts'    => $time_limit_enabled && isset( $rule['expires_ts'] ) ? (int) $rule['expires_ts'] : 0,
        );
    }

    /**
     * Get the list of role slugs the given user is allowed to share with.
     * Uses explicit admin settings. If empty, falls back to "own and lower roles" by capability count.
     */
    public static function get_allowed_share_roles_for_user( $user_id ) {
        $user = get_user_by( 'id', $user_id );
        if ( ! $user || empty( $user->roles ) ) {
            return array();
        }

        if ( in_array( 'administrator', (array) $user->roles, true ) ) {
            return array_keys( wp_roles()->get_names() );
        }

        $primary_role_slug = $user->roles[0];
        $settings = self::get_settings();
        $share_options = isset( $settings['share_options'] ) && is_array( $settings['share_options'] ) ? $settings['share_options'] : array();

        // 1. Check explicit admin configuration first
        if ( ! empty( $share_options['allowed_target_roles'] ) && is_array( $share_options['allowed_target_roles'] ) ) {
            if ( isset( $share_options['allowed_target_roles'][ $primary_role_slug ] ) ) {
                $allowed = $share_options['allowed_target_roles'][ $primary_role_slug ];
                return is_array( $allowed ) ? $allowed : array();
            }
        }

        // 2. Fallback: "Own and lower roles" based on capability count
        $all_roles = wp_roles()->roles;
        $primary_role_obj = isset( $all_roles[ $primary_role_slug ] ) ? $all_roles[ $primary_role_slug ] : null;
        if ( ! $primary_role_obj ) {
            return array();
        }

        $user_cap_count = count( $primary_role_obj['capabilities'] );
        $allowed_roles = array();

        foreach ( $all_roles as $slug => $data ) {
            $cap_count = isset( $data['capabilities'] ) ? count( $data['capabilities'] ) : 0;
            // Additional basic guard: do not allow sharing with administrator if the user is not an administrator, 
            // even if capability counts are somehow weird.
            if ( 'administrator' === $slug ) {
                continue;
            }
            if ( $cap_count <= $user_cap_count ) {
                $allowed_roles[] = $slug;
            }
        }

        return $allowed_roles;
    }

    /**
     * Can the given user access (read/download) an item?
     */
    public static function user_can_access_item( $current_user_id, $item_id ) {
        return self::can_user_do( $current_user_id, $item_id, 'access' );
    }

    /**
     * Can the given user edit an item (rename, metadata changes)?
     */
    public static function user_can_manage_item( $current_user_id, $item_id ) {
        return self::can_user_do( $current_user_id, $item_id, 'edit' );
    }

    /**
     * Can the given user delete an item?
     */
    public static function user_can_delete_item( $current_user_id, $item_id ) {
        return self::can_user_do( $current_user_id, $item_id, 'delete' );
    }

    /**
     * Can the given user perform a specific action on an item?
     *
     * Supported actions: access, edit, delete, share, restore, permanent_delete
     */
    public static function can_user_do( $current_user_id, $item_id, $action ) {
        $current_user_id = (int) $current_user_id;
        $item_id         = (int) $item_id;
        $action          = sanitize_key( (string) $action );

        if ( $current_user_id <= 0 || $item_id <= 0 ) {
            return false;
        }

        $post = get_post( $item_id );
        if ( ! $post || ( $post->post_type !== NNAXFUF_Plugin::CPT && $post->post_type !== NNAXFUF_Plugin::CPT_FOLDER ) ) {
            return false;
        }

        $ctx = self::get_share_context_for_user( $current_user_id, $item_id );
        if ( empty( $ctx ) || ! is_array( $ctx ) ) {
            return false;
        }

        $is_owner = ! empty( $ctx['is_owner'] );

        switch ( $action ) {
            case 'access':
                return ! empty( $ctx['can_access'] );

            case 'edit':
                return ! empty( $ctx['can_edit'] );

            case 'share':
                return ! empty( $ctx['can_share'] );

            case 'restore':
            case 'permanent_delete':
                return $is_owner;

            case 'delete':
                if ( ! empty( $ctx['can_delete'] ) ) {
                    return true;
                }

                // Optional extension: shared users may delete files when the
                // file is inside a shared folder they can manage.
                if ( ! $is_owner && $post->post_type === NNAXFUF_Plugin::CPT && self::is_shared_folder_file_delete_enabled() ) {
                    $parent_id = (int) $post->post_parent;
                    if ( $parent_id > 0 ) {
                        $parent_ctx = self::get_share_context_for_user( $current_user_id, $parent_id );
                        $parent_allows_delete = ! empty( $parent_ctx['can_delete'] );

                        // Shared folder delete is intentionally stricter than edit rights:
                        // only explicit folder-level delete permission may delete files.
                        if ( $parent_allows_delete && ! empty( $ctx['can_access'] ) ) {
                            return true;
                        }
                    }
                }

                return false;
        }

        return false;
    }

    /**
     * Recursive trash.
     */
    public static function trash_folder_contents_recursive( $folder_id ) {
        $files = get_posts( array(
            'post_type'      => NNAXFUF_Plugin::CPT,
            'post_status'    => 'publish',
            'posts_per_page' => -1,
            'post_parent'    => $folder_id,
            'fields'         => 'ids',
        ) );
        foreach ( $files as $fid ) {
            wp_trash_post( $fid );
        }

        $subfolders = get_posts( array(
            'post_type'      => NNAXFUF_Plugin::CPT_FOLDER,
            'post_status'    => 'publish',
            'posts_per_page' => -1,
            'post_parent'    => $folder_id,
            'fields'         => 'ids',
        ) );
        foreach ( $subfolders as $sfid ) {
            self::trash_folder_contents_recursive( $sfid );
            wp_trash_post( $sfid );
        }
    }

    /**
     * Recursive share.
     *
     * When $replace_existing is true, descendants are forced to exactly match
     * the provided rule set. When false, we merge user ids/rules into existing
     * shares to preserve local additions.
     */
    public static function share_folder_contents_recursive( $folder_id, $share_ids, $replace_existing = false, $share_rules = array() ) {
        $share_ids = is_array( $share_ids ) ? array_values( array_filter( array_map( array( __CLASS__, 'normalize_share_identifier' ), $share_ids ), function ( $id ) {
            return false !== $id;
        } ) ) : array();
        $share_rules  = self::normalize_share_rules( $share_rules );
        $rules_to_apply = $share_rules;

        if ( empty( $rules_to_apply ) && ! empty( $share_ids ) ) {
            foreach ( $share_ids as $share_user_id ) {
                $rules_to_apply[ $share_user_id ] = array(
                    'starts_ts'   => 0,
                    'expires_ts'   => 0,
                    'owner_rights' => false,
                    'can_delete'   => false,
                );
            }
        }

        $files = get_posts( array(
            'post_type'      => NNAXFUF_Plugin::CPT,
            'post_status'    => 'publish',
            'posts_per_page' => -1,
            'post_parent'    => $folder_id,
            'fields'         => 'ids',
        ) );
        foreach ( $files as $fid ) {
            if ( $replace_existing ) {
                self::set_item_share_rules( $fid, $rules_to_apply );
            } else {
                $shared = get_post_meta( $fid, NNAXFUF_Plugin::META_SHARED, true );
                $shared = is_array( $shared ) ? $shared : array();
                $numeric_share_ids = array_values( array_filter( $share_ids, 'is_numeric' ) );
                $numeric_share_ids = array_map( 'intval', $numeric_share_ids );
                $merged = array_unique( array_merge( $shared, $numeric_share_ids ) );
                $merged = array_values( array_filter( array_map( 'intval', $merged ) ) );
                update_post_meta( $fid, NNAXFUF_Plugin::META_SHARED, $merged );

                if ( ! empty( $share_rules ) ) {
                    $existing_rules = self::get_item_share_rules( $fid );
                    // Incoming rules win on key conflicts by design.
                    $merged_rules   = array_replace( $existing_rules, $share_rules );
                    self::set_item_share_rules( $fid, $merged_rules );
                }
            }
        }

        $subfolders = get_posts( array(
            'post_type'      => NNAXFUF_Plugin::CPT_FOLDER,
            'post_status'    => 'publish',
            'posts_per_page' => -1,
            'post_parent'    => $folder_id,
            'fields'         => 'ids',
        ) );
        foreach ( $subfolders as $sfid ) {
            if ( $replace_existing ) {
                self::set_item_share_rules( $sfid, $rules_to_apply );
            } else {
                $shared = get_post_meta( $sfid, NNAXFUF_Plugin::META_SHARED, true );
                $shared = is_array( $shared ) ? $shared : array();
                $numeric_share_ids = array_values( array_filter( $share_ids, 'is_numeric' ) );
                $numeric_share_ids = array_map( 'intval', $numeric_share_ids );
                $merged = array_unique( array_merge( $shared, $numeric_share_ids ) );
                $merged = array_values( array_filter( array_map( 'intval', $merged ) ) );
                update_post_meta( $sfid, NNAXFUF_Plugin::META_SHARED, $merged );

                if ( ! empty( $share_rules ) ) {
                    $existing_rules = self::get_item_share_rules( $sfid );
                    // Keep behavior consistent with file handling above.
                    $merged_rules   = array_replace( $existing_rules, $share_rules );
                    self::set_item_share_rules( $sfid, $merged_rules );
                }
            }
            // Pass original inputs down the tree so each level applies the same strategy.
            self::share_folder_contents_recursive( $sfid, $share_ids, $replace_existing, $share_rules );
        }
    }

    /**
     * Recursive restore.
     */
    public static function restore_folder_contents_recursive( $current_user_id, $folder_id ) {
        $files = get_posts( array(
            'post_type'      => NNAXFUF_Plugin::CPT,
            'post_status'    => 'trash',
            'posts_per_page' => -1,
            'post_parent'    => $folder_id,
            'fields'         => 'ids',
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Restore recursion must scope items to current owner.
            'meta_query'     => array(
                array(
                    'key'   => NNAXFUF_Plugin::META_OWNER,
                    'value' => $current_user_id,
                ),
            ),
        ) );
        foreach ( $files as $fid ) {
            self::restore_single_item( $fid );
        }

        $subfolders = get_posts( array(
            'post_type'      => NNAXFUF_Plugin::CPT_FOLDER,
            'post_status'    => 'trash',
            'posts_per_page' => -1,
            'post_parent'    => $folder_id,
            'fields'         => 'ids',
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Restore recursion must scope folders to current owner.
            'meta_query'     => array(
                array(
                    'key'   => NNAXFUF_Plugin::META_OWNER,
                    'value' => $current_user_id,
                ),
            ),
        ) );
        foreach ( $subfolders as $sfid ) {
            self::restore_single_item( $sfid );
            self::restore_folder_contents_recursive( $current_user_id, $sfid );
        }
    }

    /**
     * Restore single item.
     */
    public static function restore_single_item( $item_id ) {
        $item_id = (int) $item_id;
        if ( $item_id <= 0 ) {
            return;
        }

        $post = get_post( $item_id );
        if ( ! $post ) {
            return;
        }

        $restore_status = get_post_meta( $item_id, '_wp_trash_meta_status', true );
        if ( empty( $restore_status ) || $restore_status === 'trash' ) {
            $restore_status = 'publish';
        }

        $target_parent_id = (int) $post->post_parent;
        if ( $target_parent_id > 0 ) {
            $parent = get_post( $target_parent_id );
            // If the original parent folder is missing/unavailable, restore into root.
            if ( ! $parent || $parent->post_type !== NNAXFUF_Plugin::CPT_FOLDER || $parent->post_status !== 'publish' ) {
                $target_parent_id = 0;
            }
        }

        wp_untrash_post( $item_id );
        wp_update_post( array(
            'ID'          => $item_id,
            'post_status' => $restore_status,
            'post_parent' => $target_parent_id,
        ) );
    }

    /**
     * Sanitize color.
     */
    public static function nnaxfuf_sanitize_color( $value ) {
        if ( empty( $value ) ) return '';
        if ( preg_match( '|^#([A-Fa-f0-9]{3}){1,2}$|', $value ) ) {
            return $value;
        }
        return '';
    }

    /**
     * Folder Size - Rekursiv
     */
    public static function sum_folder_size_recursive( $folder_id ) {
        $folder_id = (int) $folder_id;
        if ( $folder_id <= 0 ) {
            return 0;
        }

        $bytes = 0;
        $files = get_posts( array(
            'post_type'      => NNAXFUF_Plugin::CPT,
            'post_status'    => 'publish',
            'posts_per_page' => -1,
            'post_parent'    => $folder_id,
            'fields'         => 'ids',
        ) );

        foreach ( $files as $file_id ) {
            $file_path = get_post_meta( (int) $file_id, NNAXFUF_Plugin::META_FILEPATH, true );
            if ( ! empty( $file_path ) && file_exists( $file_path ) ) {
                $bytes += (int) filesize( $file_path );
            }
        }

        $subfolders = get_posts( array(
            'post_type'      => NNAXFUF_Plugin::CPT_FOLDER,
            'post_status'    => 'publish',
            'posts_per_page' => -1,
            'post_parent'    => $folder_id,
            'fields'         => 'ids',
        ) );

        foreach ( $subfolders as $subfolder_id ) {
            $bytes += NNAXFUF_Helpers::sum_folder_size_recursive( (int) $subfolder_id );
        }

        return (int) $bytes;
    }
    /**
     * Set up activity table
     */
    public static function setup_activity_table() {
        global $wpdb;
        $table_name = $wpdb->prefix . 'nnaxfuf_activities';
        $charset_collate = $wpdb->get_charset_collate();

        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is derived from fixed WP prefix + static suffix for dbDelta schema creation.
        $sql = "CREATE TABLE $table_name (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            item_id bigint(20) NOT NULL,
            user_id bigint(20) NOT NULL,
            action varchar(50) NOT NULL,
            details text DEFAULT '' NOT NULL,
            created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
            PRIMARY KEY  (id),
            KEY item_id (item_id),
            KEY user_id (user_id),
            KEY action (action)
        ) $charset_collate;";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.SchemaChange -- Required schema setup during plugin activation/upgrade.
        dbDelta( $sql );
    }

    /**
     * Log an activity
     */
    public static function log_activity( $item_id, $action, $details = array() ) {
        global $wpdb;
        $table_name = $wpdb->prefix . 'nnaxfuf_activities';
        $payload    = wp_json_encode( $details );
        if ( false === $payload || null === $payload ) {
            $payload = '{}';
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Intended write to plugin activity log table.
        $inserted = $wpdb->insert(
            $table_name,
            array(
                'item_id'    => (int) $item_id,
                'user_id'    => get_current_user_id(),
                'action'     => sanitize_key( $action ),
                'details'    => $payload,
                'created_at' => current_time( 'mysql' ),
            )
        );

        if ( false !== $inserted ) {
            return;
        }

        // If the activities table is missing or the schema drifted, try a one-time self-heal.
        self::setup_activity_table();

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Intended retry write to plugin activity log table after self-heal.
        $wpdb->insert(
            $table_name,
            array(
                'item_id'    => (int) $item_id,
                'user_id'    => get_current_user_id(),
                'action'     => sanitize_key( $action ),
                'details'    => $payload,
                'created_at' => current_time( 'mysql' ),
            )
        );
    }

    /**
     * Send email notifications to new shared users
     */
    public static function send_share_notifications( $item_id, $new_uids, $custom_message = '' ) {
        if ( empty( $new_uids ) ) return;

        $item = get_post( $item_id );
        if ( ! $item ) return;

        $owner_id   = (int) get_post_meta( $item_id, NNAXFUF_Plugin::META_OWNER, true );
        $owner_user = get_user_by( 'id', $owner_id );
        $owner_name = $owner_user ? $owner_user->display_name : __( 'A user', 'nnax-frontend-user-files' );
        $item_title = self::get_raw_item_title( $item_id );
        
        /* translators: %s: shared item title. */
        $subject = sprintf( __( 'New share: %s', 'nnax-frontend-user-files' ), $item_title );
        
        // Use custom message if provided and not purely whitespace; otherwise fall back to admin settings or hardcoded string
        $message_template = trim( $custom_message );
        if ( empty( $message_template ) ) {
            $settings = self::get_settings();
            $share_options = isset($settings['share_options']) && is_array($settings['share_options']) ? $settings['share_options'] : array();
            $default_msg = __( "Hello,\n\nI have shared \"{item_title}\" with you.\n\nYou can now find it in the file manager on the website: {site_url}\n\n{owner_name}", 'nnax-frontend-user-files' );
            $message_template = isset( $share_options['default_notification_msg'] ) ? $share_options['default_notification_msg'] : $default_msg;
        }

        // Replace placeholders
        $message_template = str_replace( 
            array( '{owner_name}', '{item_title}', '{site_url}' ), 
            array( $owner_name, $item_title, site_url() ), 
            $message_template 
        );
        
        // Backwards compatibility for old %1$s syntax if it was preserved in old settings
        if ( strpos( $message_template, '%1$s' ) !== false || strpos( $message_template, '%2$s' ) !== false ) {
            $message_template = sprintf( $message_template, $owner_name, $item_title );
        }

        // Ensure line breaks are preserved if no paragraphs are input manually
        if ( strpos( $message_template, '<p>' ) === false ) {
            $message_template = wpautop( $message_template );
        }
        $headers = array('Content-Type: text/html; charset=UTF-8');

        foreach ( $new_uids as $sid ) {
            if ( is_numeric( $sid ) ) {
                $user = get_user_by( 'id', (int) $sid );
                if ( $user && $user->user_email ) {
                    wp_mail( $user->user_email, $subject, $message_template, $headers );
                }
            }
            // For roles, we currently do NOT send mass emails to avoid spam.
            // This can be extended later if requested.
        }
    }
}
