#!/usr/bin/env python3
"""
uQCme - Microbial Quality Control Web Application

A Streamlit web application for visualizing and exploring microbial QC results
generated by the uQCme CLI tool. This module focuses on data tables and
interactive data exploration, with plotting functionality handled by plot.py.

Note: This module requires the 'app' or 'all' extras to be installed:
    pip install uqcme[app]
"""

import os
import sys
import streamlit as st
from streamlit.web import cli as stcli
import pandas as pd
import yaml
from pathlib import Path
from typing import Dict, Any, Optional
from uQCme.app.plot import QCPlotter, get_available_metrics
from uQCme.core.loader import load_data_from_config, load_config_from_file
from uQCme.core.engine import QCProcessor
from uQCme.core.config import UQCMeConfig, DataInput
from uQCme.core.exceptions import ConfigError, DataLoadError


class QCDashboard:
    """Main class for the QC dashboard application."""

    def __init__(self, config_path: Optional[str]):
        """Initialize the dashboard with configuration."""
        self.config_path = config_path
        self.config: UQCMeConfig = self._load_config(config_path)
        self.data: pd.DataFrame = pd.DataFrame()
        self.mapping: Dict[str, Any] = {}
        self.qc_rules: pd.DataFrame = pd.DataFrame()
        self.qc_tests: pd.DataFrame = pd.DataFrame()
        self.plotter: QCPlotter = QCPlotter(self.config)
        
    def _load_config(self, config_path: Optional[str]) -> UQCMeConfig:
        """Load configuration from YAML file or use defaults."""
        if config_path:
            try:
                return load_config_from_file(config_path)
            except ConfigError as e:
                st.error(f"Configuration Error: {e}")
                st.stop()
            except Exception as e:
                st.error(f"Unexpected error loading config: {e}")
                st.stop()
        else:
            return self._get_default_config()

    def _get_default_config(self) -> UQCMeConfig:
        """Get default configuration using bundled files."""
        defaults_dir = Path(__file__).parent.parent / 'defaults'
        config_path = defaults_dir / 'config.yaml'
        
        try:
            config = load_config_from_file(str(config_path))
        except ConfigError as e:
            st.error(f"Error loading default config: {e}")
            st.stop()
            
        # Update paths to point to the defaults directory if local files don't exist
        if config.app and config.app.input:
            inp = config.app.input
            # Update mapping, rules, tests to absolute paths in defaults dir
            if not os.path.exists(inp.mapping):
                inp.mapping = str(defaults_dir / Path(inp.mapping).name)
            if not os.path.exists(inp.qc_rules):
                inp.qc_rules = str(defaults_dir / Path(inp.qc_rules).name)
            if not os.path.exists(inp.qc_tests):
                inp.qc_tests = str(defaults_dir / Path(inp.qc_tests).name)
                    
        if config.qc and config.qc.input:
            inp = config.qc.input
            # Update mapping, rules, tests to absolute paths in defaults dir
            if not os.path.exists(inp.mapping):
                inp.mapping = str(defaults_dir / Path(inp.mapping).name)
            if not os.path.exists(inp.qc_rules):
                inp.qc_rules = str(defaults_dir / Path(inp.qc_rules).name)
            if not os.path.exists(inp.qc_tests):
                inp.qc_tests = str(defaults_dir / Path(inp.qc_tests).name)
        
        return config

    def _get_dashboard_config(self, key: str, default_value):
        """Get dashboard configuration value with fallback to default."""
        if self.config.app and self.config.app.dashboard:
            return getattr(self.config.app.dashboard, key, default_value)
        return default_value

    def load_data(self):
        """Load all required data files or from API."""
        if not self.config.app:
            st.error("Configuration error: 'app' section missing.")
            st.stop()

        try:
            # Load mapping configuration first as it's needed for validation
            mapping_path = self.config.app.input.mapping
            with open(mapping_path, 'r', encoding='utf-8') as f:
                self.mapping = yaml.safe_load(f)
            
            # Load processed QC results - check if API or file
            data_config = self.config.app.input.data
            
            # Check if data source is configured (file or api_call)
            has_configured_source = False
            if isinstance(data_config, DataInput):
                if data_config.file or data_config.api_call:
                    has_configured_source = True
            elif isinstance(data_config, dict):
                if data_config.get('file') or data_config.get('api_call'):
                    has_configured_source = True
            elif isinstance(data_config, str) and data_config:
                has_configured_source = True

            # Only attempt to load if a source is configured
            if has_configured_source:
                try:
                    self.data = load_data_from_config(data_config, self.mapping)
                    # Validate the loaded data if not empty
                    if not self.data.empty:
                        self.data = self._validate_api_data(self.data)
                    # Clear any previous API warnings
                    self.api_warning = None
                except DataLoadError as e:
                    # Check for timeout/502 errors and show specific warnings
                    if hasattr(e, 'error_type') and e.error_type in ['timeout', '502']:
                        self.api_warning = {
                            'type': e.error_type,
                            'message': str(e),
                            'status_code': getattr(e, 'status_code', None)
                        }
                        st.warning(
                            f"⚠️ API Error ({e.error_type.upper()}): {e}\n\n"
                            "You can upload a data file manually below."
                        )
                    else:
                        st.error(f"Data loading failed: {e}")
                    # If data loading fails, we'll handle it in run() by asking
                    # for upload
                    self.data = pd.DataFrame()
                except ConfigError as e:
                    st.error(f"Configuration error: {e}")
                    self.data = pd.DataFrame()
                    self.api_warning = None
                except Exception as e:
                    st.error(f"Unexpected error loading data: {e}")
                    self.data = pd.DataFrame()
                    self.api_warning = None
            else:
                # No source configured, start with empty dataframe
                self.data = pd.DataFrame()
                self.api_warning = None
            
            # Load QC rules
            rules_path = self.config.app.input.qc_rules
            self.qc_rules = pd.read_csv(rules_path, sep='\t')
            
            # Load QC tests
            tests_path = self.config.app.input.qc_tests
            self.qc_tests = pd.read_csv(tests_path, sep='\t')
            
            # Load warnings if available
            warnings_path = self.config.app.input.warnings
            if warnings_path and os.path.exists(warnings_path):
                self.warnings = pd.read_csv(warnings_path, sep='\t')
            else:
                self.warnings = None
            
        except Exception as e:
            st.error(f"Error loading configuration files: {e}")
            st.stop()

    def _get_field_by_role(self, role: str) -> Optional[str]:
        """Get field name from mapping by its role (e.g., 'id', 'outcome', 'action').
        
        Searches the mapping configuration for fields with specific report roles.
        """
        sections = self.mapping.get('Sections', {})
        
        for section_data in sections.values():
            for field_name, field_config in section_data.items():
                if not isinstance(field_config, dict):
                    continue
                
                report_config = field_config.get('report', {})
                data_config = field_config.get('data', {})
                qc_config = field_config.get('QC', {})
                
                # Check for specific roles
                if role == 'id' and report_config.get('id'):
                    # Return the QC mapping (output column name) if available
                    return qc_config.get('mapping') or data_config.get('mapping')
                elif role == 'outcome' and data_config.get('mapping') == 'qc_outcome':
                    return 'qc_outcome'
                elif role == 'action' and data_config.get('mapping') == 'qc_action':
                    return 'qc_action'
                elif role == 'failed_rules' and data_config.get('mapping') == 'failed_rules':
                    return 'failed_rules'
                elif role == 'passed_rules' and data_config.get('mapping') == 'passed_rules':
                    return 'passed_rules'
                elif role == 'species':
                    qc_mapping = qc_config.get('mapping')
                    if qc_mapping:
                        if isinstance(qc_mapping, list) and 'species' in qc_mapping:
                            return 'species'
                        elif qc_mapping == 'species':
                            return 'species'
        
        return None

    def _get_id_field(self) -> Optional[str]:
        """Get the ID field name from mapping configuration."""
        return self._get_field_by_role('id')

    def _get_outcome_field(self) -> Optional[str]:
        """Get the QC outcome field name from mapping configuration."""
        return self._get_field_by_role('outcome') or 'qc_outcome'

    def _get_action_field(self) -> Optional[str]:
        """Get the QC action field name from mapping configuration."""
        return self._get_field_by_role('action') or 'qc_action'

    def _get_species_field(self) -> Optional[str]:
        """Get the species field name from mapping configuration."""
        return self._get_field_by_role('species') or 'species'

    def _get_required_fields(self) -> dict:
        """Get required fields from mapping configuration."""
        required_fields = {
            'critical': [],  # Fields that will break core functionality
            'important': []  # Fields that will break specific features
        }
        
        # Critical fields determined from mapping roles
        id_field = self._get_id_field()
        outcome_field = self._get_outcome_field()
        
        critical_fields = []
        if id_field:
            critical_fields.append(id_field)
        if outcome_field:
            critical_fields.append(outcome_field)
        
        # Important fields determined from mapping roles
        species_field = self._get_species_field()
        action_field = self._get_action_field()
        
        important_fields = []
        if species_field:
            important_fields.append(species_field)
        if action_field:
            important_fields.append(action_field)
        
        # Get field mappings from configuration
        sections = self.mapping.get('Sections', {})
        
        for section_data in sections.values():
            for field_config in section_data.values():
                if not isinstance(field_config, dict):
                    continue
                    
                mapping_key = field_config.get('data', {}).get('mapping')
                if mapping_key:
                    if mapping_key in critical_fields:
                        required_fields['critical'].append(mapping_key)
                    elif mapping_key in important_fields:
                        required_fields['important'].append(mapping_key)
        
        # Add fallback for critical fields if not found in mapping
        for field in critical_fields:
            if field not in required_fields['critical']:
                required_fields['critical'].append(field)
        
        return required_fields

    def _validate_api_data(self, data: pd.DataFrame) -> pd.DataFrame:
        """Validate that API data contains required fields."""
        if data.empty:
            raise ValueError("Data source returned no data")
        
        required_fields = self._get_required_fields()
        missing_critical = []
        missing_important = []
        
        # Check for missing critical fields
        for field in required_fields['critical']:
            if field not in data.columns:
                missing_critical.append(field)
        
        # Check for missing important fields
        for field in required_fields['important']:
            if field not in data.columns:
                missing_important.append(field)
        
        # Handle missing critical fields (fatal errors)
        if missing_critical:
            raise ValueError(
                "Missing required fields: " + ", ".join(missing_critical)
            )
        
        # Handle missing important fields (warnings)
        if missing_important:
            st.warning(
                "Optional fields missing: " + ", ".join(missing_important)
            )
        
        return data


    def setup_page(self):
        """Set up the Streamlit page configuration."""
        st.set_page_config(
            page_title=self.config.title,
            page_icon="🔬",
            layout="wide",
            initial_sidebar_state="expanded"
        )

    def render_header(self):
        """Render the application header."""
        st.title("🔬 uQCme - Microbial Quality Control Dashboard")
    
    def render_sidebar_metrics(self, filtered_data: pd.DataFrame):
        """Render summary metrics in the sidebar."""
        st.sidebar.subheader("📊 Summary")
        
        # Version info
        version = self.config.version
        st.sidebar.markdown(f"**Version:** {version}")
        
        # Create columns for horizontal layout
        col1, col2, col3 = st.sidebar.columns(3)
        
        # Sample count metrics (filtered vs total)
        total_samples = len(filtered_data)
        total_all = len(self.data)
        with col1:
            # Show filtered count vs total
            if total_samples == total_all:
                st.metric("Samples", total_samples)
            else:
                delta_text = f"of {total_all}"
                st.metric("Samples", total_samples, delta=delta_text)
        
        st.sidebar.markdown("---")

        # QC outcome metrics (based on filtered data)
        outcome_field = self._get_outcome_field()
        if outcome_field and outcome_field in filtered_data.columns:
            # Count PASS outcomes (check if any outcome contains 'PASS' prefix)
            def contains_only_pass(val):
                if pd.isna(val):
                    return False
                outcomes = str(val).split(',')
                return all(o.strip().upper().startswith('PASS') for o in outcomes)
            
            pass_filter = filtered_data[outcome_field].apply(contains_only_pass)
            pass_count = len(filtered_data[pass_filter])
            
            # Calculate total PASS from unfiltered data
            total_pass_filter = self.data[outcome_field].apply(contains_only_pass)
            total_pass_count = len(self.data[total_pass_filter])
            
            with col2:
                if pass_count == total_pass_count:
                    st.metric("PASS", pass_count)
                else:
                    delta_text = f"of {total_pass_count}"
                    st.metric("PASS", pass_count, delta=delta_text)
            
            fail_filter = ~filtered_data[outcome_field].apply(contains_only_pass)
            fail_count = len(filtered_data[fail_filter])
            
            # Calculate total Issues from unfiltered data
            total_fail_filter = ~self.data[outcome_field].apply(contains_only_pass)
            total_fail_count = len(self.data[total_fail_filter])
            
            with col3:
                if fail_count == total_fail_count:
                    st.metric("Issues", fail_count)
                else:
                    delta_text = f"of {total_fail_count}"
                    st.metric("Issues", fail_count, delta=delta_text)
        else:
            # No outcome field available
            with col2:
                st.metric("PASS", "N/A")
            with col3:
                st.metric("Issues", "N/A")

    def _get_filterable_fields(self, data: pd.DataFrame) -> list:
        """Get all fields that should have filters based on mapping config."""
        filterable_fields = []
        sections_columns = self._get_columns_by_section(data)
        
        for section_name, section_cols in sections_columns.items():
            for col_info in section_cols:
                if col_info['filter'] and col_info['column'] in data.columns:
                    filterable_fields.append({
                        'column': col_info['column'],
                        'field_name': col_info['field_name'],
                        'section': section_name
                    })
        
        return filterable_fields

    def _create_numerical_filter(self, filtered_data: pd.DataFrame,
                                 column: str, field_name: str) -> pd.DataFrame:
        """Create and apply numerical range filter."""
        unique_values = filtered_data[column].dropna()
        
        if len(unique_values) == 0:
            return filtered_data
            
        min_val = float(unique_values.min())
        max_val = float(unique_values.max())
        
        # Only show slider if there's a range
        if min_val == max_val:
            return filtered_data
        
        # Check if filters should be reset
        reset_filters = st.session_state.get('filters_reset', False)
        default_value = (min_val, max_val)
        
        # Clear the reset flag after using it
        if reset_filters:
            key = f"range_{column}"
            if key in st.session_state:
                del st.session_state[key]
            
        selected_range = st.sidebar.slider(
            f"{field_name} Range",
            min_value=min_val,
            max_value=max_val,
            value=default_value,
            key=f"range_{column}"
        )
        
        # Apply range filter
        # Include NaN values when slider is at full range
        full_range = (selected_range[0] == min_val and
                      selected_range[1] == max_val)
        if full_range:
            # Full range selected - don't filter anything
            return filtered_data
        else:
            # Partial range - apply filter but preserve NaN
            col_data = filtered_data[column]
            min_check = col_data >= selected_range[0]
            max_check = col_data <= selected_range[1]
            in_range = min_check & max_check
            range_condition = (in_range | col_data.isna())
            return filtered_data[range_condition]

    def _create_categorical_filter(self, filtered_data: pd.DataFrame,
                                   column: str,
                                   field_name: str) -> pd.DataFrame:
        """Create and apply categorical dropdown filter."""
        unique_values = filtered_data[column].dropna()
        
        if len(unique_values) == 0:
            return filtered_data
            
        unique_sorted = sorted(unique_values.unique())
        
        # Only create filter if we have reasonable number of options
        threshold = self._get_dashboard_config(
            'categorical_filter_threshold', 20
        )
        if len(unique_sorted) > threshold:
            return filtered_data
            
        options = ['All'] + list(unique_sorted)
        
        # Check if filters should be reset
        reset_filters = st.session_state.get('filters_reset', False)
        default_index = 0  # 'All'
        
        # Clear the reset flag after using it
        if reset_filters:
            key = f"filter_{column}"
            if key in st.session_state:
                del st.session_state[key]
        
        selected_value = st.sidebar.selectbox(
            f"Filter by {field_name}",
            options,
            index=default_index,
            key=f"filter_{column}"
        )
        
        if selected_value != 'All':
            filter_condition = (filtered_data[column] == selected_value)
            return filtered_data[filter_condition]
        
        return filtered_data

    def _create_text_search_filter(self, filtered_data: pd.DataFrame,
                                   column: str,
                                   field_name: str) -> pd.DataFrame:
        """Create and apply text search filter."""
        # Check if filters should be reset
        reset_filters = st.session_state.get('filters_reset', False)
        default_value = ""
        
        # Clear the reset flag after using it
        if reset_filters:
            key = f"search_{column}"
            if key in st.session_state:
                del st.session_state[key]
        
        search_value = st.sidebar.text_input(
            f"Search {field_name}",
            placeholder=f"Enter {field_name.lower()}...",
            value=default_value,
            key=f"search_{column}"
        )
        
        if search_value:
            column_str = filtered_data[column].astype(str)
            contains_filter = column_str.str.contains(
                search_value, case=False, na=False
            )
            return filtered_data[contains_filter]
        
        return filtered_data

    def _clear_all_filters(self):
        """Clear all filter-related session state values and selections."""
        # Set a reset flag instead of trying to modify widget values directly
        st.session_state['filters_reset'] = True
        
        # Clear sample selections
        if 'selected_samples' in st.session_state:
            st.session_state.selected_samples.clear()
        
        # Force a rerun to refresh the interface
        st.rerun()

    def render_sidebar_filters(self):
        """Render sidebar filters for data exploration."""
        # Get filterable fields from mapping configuration
        filterable_fields = self._get_filterable_fields(self.data)
        
        # Apply filters
        filtered_data = self.data.copy()
        
        # Generate dynamic filters based on mapping configuration
        for field_info in filterable_fields:
            column = field_info['column']
            field_name = field_info['field_name']
            
            if column in filtered_data.columns:
                # Get unique values for this column
                unique_values = filtered_data[column].dropna()
                
                if len(unique_values) > 0:
                    # Check if column is numerical
                    is_numeric = pd.api.types.is_numeric_dtype(unique_values)
                    
                    if is_numeric:
                        # Use extracted numerical filter method
                        filtered_data = self._create_numerical_filter(
                            filtered_data, column, field_name
                        )
                    else:
                        # Categorical or text filters for non-numerical columns
                        unique_sorted = sorted(unique_values.unique())
                        
                        # Determine filter type based on unique count
                        threshold = self._get_dashboard_config(
                            'categorical_filter_threshold', 20
                        )
                        if len(unique_sorted) <= threshold:
                            # Use extracted categorical filter method
                            filtered_data = self._create_categorical_filter(
                                filtered_data, column, field_name
                            )
                        else:
                            # Use extracted text search filter method
                            filtered_data = self._create_text_search_filter(
                                filtered_data, column, field_name
                            )
        
        # Add sample name search (always available)
        # Get the ID field from mapping
        id_field = self._get_id_field()
        search_field = id_field if id_field and id_field in filtered_data.columns else None
        
        # Check if filters should be reset
        reset_filters = st.session_state.get('filters_reset', False)
        default_sample_value = ""
        
        # Clear the reset flag for sample search
        if reset_filters:
            key = "search_sample_name"
            if key in st.session_state:
                del st.session_state[key]
        
        if search_field:
            sample_filter = st.sidebar.text_input(
                f"Search {search_field}",
                placeholder=f"Enter {search_field}...",
                value=default_sample_value,
                key="search_sample_name"
            )
            
            if sample_filter:
                contains_filter = filtered_data[search_field].astype(str).str.contains(
                    sample_filter, case=False, na=False
                )
                filtered_data = filtered_data[contains_filter]
        
        # Clear the reset flag after all filters have been processed
        if st.session_state.get('filters_reset', False):
            st.session_state['filters_reset'] = False
        
        # Render summary metrics at the top with filtered data
        self.render_sidebar_metrics(filtered_data)
        
        st.sidebar.header("🔍 Filters")
        
        # Add Clear All Filters button
        if st.sidebar.button("🗑️ Clear All Filters", type="secondary"):
            self._clear_all_filters()
        
        return filtered_data

    def _get_columns_by_section(self, data: pd.DataFrame) -> Dict[str, list]:
        """Get columns organized by section from mapping.yaml."""
        sections_columns = {}
        
        # Get sections from mapping
        sections = self.mapping.get('Sections', {})
        
        for section_name, section_data in sections.items():
            section_cols = []
            
            for field_name, field_config in section_data.items():
                # Skip if field_config is not a dict (e.g., boolean values)
                if not isinstance(field_config, dict):
                    continue
                
                # Try data.mapping first, then QC.mapping as fallback
                mapping_key = field_config.get('data', {}).get('mapping')
                qc_mapping = field_config.get('QC', {}).get('mapping')
                
                # Determine which column to use based on what exists in the data
                actual_column = None
                if mapping_key and mapping_key in data.columns:
                    actual_column = mapping_key
                elif qc_mapping:
                    # QC mapping can be a string or list
                    if isinstance(qc_mapping, str) and qc_mapping in data.columns:
                        actual_column = qc_mapping
                    elif isinstance(qc_mapping, list):
                        for qc_col in qc_mapping:
                            if qc_col in data.columns:
                                actual_column = qc_col
                                break
                
                if actual_column:
                    # Get report configuration
                    report_config = field_config.get('report', {})
                    
                    # Include hidden fields in the section but mark them
                    is_hidden = (field_config.get('hidden', False) or
                                 report_config.get('hidden', False))
                    
                    section_cols.append({
                        'column': actual_column,
                        'field_name': field_name,
                        'hidden': is_hidden,
                        'filter': report_config.get('filter', False),
                        'id': report_config.get('id', False)
                    })
            
            if section_cols:  # Only add sections that have columns
                sections_columns[section_name] = section_cols
        
        # Add unmapped columns to "Other" section
        all_mapped_cols = []
        for section_cols in sections_columns.values():
            all_mapped_cols.extend([col['column'] for col in section_cols])
        
        unmapped_cols = [
            col for col in data.columns
            if col not in all_mapped_cols
        ]
        
        if unmapped_cols:
            sections_columns['Other'] = [
                {
                    'column': col,
                    'field_name': col.replace('_', ' ').title(),
                    'hidden': False,
                    'filter': False,
                    'id': False
                }
                for col in unmapped_cols
            ]
        
        return sections_columns

    def _get_id_column(self, data: pd.DataFrame) -> Optional[str]:
        """Get the column marked as ID field in mapping configuration."""
        sections_columns = self._get_columns_by_section(data)
        
        for section_cols in sections_columns.values():
            for col_info in section_cols:
                if col_info.get('id', False):
                    return col_info['column']
        
        return None

    def _get_column_description(self, column_name: str) -> Optional[str]:
        """Get description for a column from mapping configuration."""
        sections = self.mapping.get('Sections', {})
        
        for section_data in sections.values():
            for field_name, field_config in section_data.items():
                # Skip if field_config is not a dict
                if not isinstance(field_config, dict):
                    continue
                    
                mapping_key = field_config.get('data', {}).get('mapping')
                if mapping_key == column_name:
                    # Check for description in report config first
                    report_config = field_config.get('report', {})
                    description = report_config.get('description')
                    if description:
                        return description
                    
                    # Fallback: check QC config for backward compatibility
                    qc_config = field_config.get('QC', {})
                    description = qc_config.get('description')
                    if description:
                        return description
                    
                    # Fallback to field name if no description
                    return field_name
        
        # Return None if no mapping found
        return None

    def _get_qc_action_color(self, action: str) -> str:
        """Get color for QC action based on action type."""
        # Map actions to colors
        action_colors = {
            'none': "#00AA00",      # Green for no action needed
            'review': "#FF8C00",    # Orange for review needed
            'reject': "#DC143C",    # Red for reject
            'return_to_lab': "#8B0000"  # Dark red for return to lab
        }
        
        action_lower = str(action).lower()
        return action_colors.get(action_lower, "#000000")

    def _get_ordered_columns_with_sections(
        self,
        data: pd.DataFrame,
        visible_sections: Dict[str, bool]
    ) -> list:
        """Get ordered columns based on visible sections."""
        ordered_cols = []
        sections_columns = self._get_columns_by_section(data)
        
        # Define section order
        section_order = ['Basic', 'QC_metrics', 'Experimental', 'Other']
        
        # Add columns from visible sections in order
        for section_name in section_order:
            section_visible = visible_sections.get(section_name, True)
            if section_name in sections_columns and section_visible:
                for col_info in sections_columns[section_name]:
                    # Skip hidden fields
                    if col_info['hidden']:
                        continue
                    ordered_cols.append(col_info['column'])
        
        # Add remaining sections not in the predefined order
        for section_name, section_cols in sections_columns.items():
            section_visible = visible_sections.get(section_name, True)
            section_not_ordered = section_name not in section_order
            if section_not_ordered and section_visible:
                for col_info in section_cols:
                    if col_info['column'] not in ordered_cols:
                        ordered_cols.append(col_info['column'])
        
        return ordered_cols

    def _render_plotly_chart(self, fig, key: str, title: Optional[str] = None):
        """Helper method to render plotly charts with consistent styling."""
        if title:
            st.subheader(title)
        if fig:
            st.plotly_chart(
                fig,
                width='content',
                key=key
            )

    def _render_styled_dataframe(self, filtered_data: pd.DataFrame,
                                 column_order: list, key: str):
        """Helper method to render dataframe with selection checkboxes."""
        # Initialize session state for selected samples
        if 'selected_samples' not in st.session_state:
            st.session_state.selected_samples = set()
        
        # Get the ID column for sample selection
        id_column = self._get_id_column(filtered_data)
        
        # Create a working copy of the data
        display_data = filtered_data.copy()
        
        # Add selection checkbox column if ID column exists
        if id_column and id_column in filtered_data.columns:
            # Add a checkbox column for selection
            display_data['Select'] = display_data[id_column].apply(
                lambda x: x in st.session_state.selected_samples
            )
            # Put the select column first
            column_order = ['Select'] + column_order
        
        # Configure columns
        column_config = {}
        
        # Configure float columns to show compact numbers
        for col in display_data.columns:
            if display_data[col].dtype in ['float64', 'float32']:
                column_config[col] = st.column_config.NumberColumn(
                    col,
                    format="compact"
                )
        
        # Configure the select column if present
        if 'Select' in display_data.columns:
            column_config['Select'] = st.column_config.CheckboxColumn(
                "Select",
                help="Select this sample"
            )
        
        # Apply QC action styling if the column exists
        action_field = self._get_action_field()
        if action_field and action_field in display_data.columns:
            def highlight_action_values(val):
                if pd.isna(val):
                    return ''
                color = self._get_qc_action_color(str(val))
                return (f'color: {color}; font-weight: bold; '
                        f'text-shadow: 0 0 3px {color};')
            
            styled_data = display_data.style.map(
                highlight_action_values,
                subset=[action_field]
            )
        else:
            styled_data = display_data
        
        # Disable all columns except Select to prevent accidental editing
        disabled_columns = [col for col in display_data.columns
                            if col != 'Select']
        
        # Use data_editor to enable checkbox interaction
        edited_data = st.data_editor(
            styled_data,
            width='stretch',
            key=key,
            column_order=column_order,
            column_config=column_config,
            hide_index=True,
            disabled=disabled_columns
        )
        
        # Update selected samples based on checkbox changes
        if 'Select' in edited_data.columns and id_column:
            # Get current selections from the edited data
            current_selections = set()
            for idx, row in edited_data.iterrows():
                if row['Select']:
                    sample_id = str(row[id_column])
                    current_selections.add(sample_id)
            
            # Update session state if there are changes
            if current_selections != st.session_state.selected_samples:
                st.session_state.selected_samples = current_selections
                st.rerun()
        
        # Display selected samples summary
        if st.session_state.selected_samples:
            selected_samples = sorted(st.session_state.selected_samples)
            selected_text = ', '.join(selected_samples)
            st.info(f"**Selected samples:** {selected_text}")
            
            if st.button("Clear Selection", key=f"clear_{key}"):
                st.session_state.selected_samples.clear()
                st.rerun()

    def render_data_tab(self, filtered_data: pd.DataFrame):
        """Render the data tab with section toggles."""
        st.header("📊 Data")
        
        # Section toggles
        st.subheader("Section Visibility")
        sections_columns = self._get_columns_by_section(filtered_data)
        
        # Create columns for section toggles
        num_columns = self._get_dashboard_config('section_toggle_columns', 3)
        toggle_columns = st.columns(num_columns)
        
        visible_sections = {}
        section_names = list(sections_columns.keys())
        
        # Distribute toggles across columns
        for i, section_name in enumerate(section_names):
            col_idx = i % num_columns
            
            with toggle_columns[col_idx]:
                # Count visible columns in section
                section_cols = sections_columns[section_name]
                visible_col_count = len([
                    col for col in section_cols
                    if not col['hidden']
                ])
                
                # Default visibility based on content
                # Hide sections that are mostly hidden fields or experimental
                default_visible = True
                if visible_col_count == 0:
                    default_visible = False
                
                visible_sections[section_name] = st.checkbox(
                    f"{section_name} ({visible_col_count} columns)",
                    value=default_visible,
                    key=f"data_preview_toggle_{section_name}"
                )
        
        # Get ordered columns based on visible sections for reference
        ordered_columns = self._get_ordered_columns_with_sections(
            filtered_data,
            visible_sections
        )
        
        # Show active sections info
        active_sections = [
            name for name, visible in visible_sections.items()
            if visible
        ]
        
        # Display helpful info about column organization
        if ordered_columns:
            tip_msg = (
                f"💡 **Tip:** Use the column visibility controls (👁️) in the "
                f"table to show/hide specific columns. Currently showing "
                f"{len(ordered_columns)} columns from selected sections: "
                f"{', '.join(active_sections)}"
            )
            st.info(tip_msg)
        
        # Reorder dataframe columns to put important ones first
        # Only show columns from visible sections
        priority_columns = []
        
        # Add columns in section order priority (only from visible sections)
        for col in ordered_columns:
            if col in filtered_data.columns:
                priority_columns.append(col)
        
        # Create column order for Streamlit - only visible section columns
        column_order = priority_columns
        
        # Filter the dataframe to only show columns from visible sections
        display_data = filtered_data[
            [col for col in priority_columns if col in filtered_data.columns]
        ] if priority_columns else filtered_data
        
        # Display the dataframe with built-in controls and QC action styling
        # Only show columns from visible sections
        self._render_styled_dataframe(
            display_data, column_order, "data_preview_table"
        )
        
        # Show column information organized by visible sections
        with st.expander("📋 Column Information"):
            st.write("**Column mapping from configuration:**")
            
            for section_name in active_sections:
                if section_name in sections_columns:
                    st.subheader(f"{section_name} Section")
                    section_cols = sections_columns[section_name]
                    
                    for col_info in section_cols:
                        mapping_key = col_info['column']
                        field_name = col_info['field_name']
                        hidden = col_info['hidden']
                        has_filter = col_info['filter']
                        is_id = col_info['id']
                        
                        if mapping_key in filtered_data.columns:
                            # Skip hidden fields from display
                            if hidden:
                                continue
                            
                            # Get description from mapping config
                            description = self._get_column_description(
                                mapping_key
                            )
                            
                            # Build display string with additional info
                            extras = []
                            if has_filter:
                                extras.append("Filterable")
                            if is_id:
                                extras.append("ID")
                            
                            extra_info = ""
                            if extras:
                                extra_info = f" ({', '.join(extras)})"
                            
                            # Create field display with description
                            if description and description != field_name:
                                field_display = (
                                    f"- **{field_name}**: `{mapping_key}` - "
                                    f"{description}{extra_info}"
                                )
                            else:
                                field_display = (
                                    f"- **{field_name}**: `{mapping_key}`"
                                    f"{extra_info}"
                                )
                            st.write(field_display)

    def render_overview_tab(self, filtered_data: pd.DataFrame):
        """Render the overview tab with summary statistics."""
        st.header("📈 Overview")
        
        # Use the plotter to create overview charts
        overview_plots = self.plotter.create_quality_overview_dashboard(
            filtered_data
        )
        
        # Display charts in columns
        col1, col2 = st.columns(2)
        
        with col1:
            self._render_plotly_chart(
                overview_plots.get('outcome_pie'),
                "overview_outcome_pie",
                "QC Outcomes Distribution"
            )
        
        with col2:
            self._render_plotly_chart(
                overview_plots.get('species_bar'),
                "overview_species_bar",
                "Species Distribution"
            )
        
        # Failed Rules Analysis
        self._render_plotly_chart(
            overview_plots.get('failed_rules'),
            "overview_failed_rules",
            "Most Common Failed Rules"
        )
        
        # Quality metrics if available
        self._render_plotly_chart(
            overview_plots.get('metric_dist'),
            "overview_metric_dist",
            "Quality Metrics Distribution"
        )
        
        self._render_plotly_chart(
            overview_plots.get('correlation'),
            "overview_correlation",
            "Metrics Correlation"
        )

    def render_quality_metrics_tab(self, filtered_data: pd.DataFrame):
        """Render quality metrics visualizations."""
        st.header("🔍 Quality Metrics")
        
        # Get available numeric columns
        available_cols = get_available_metrics(filtered_data)
        
        if not available_cols:
            warning_msg = (
                "No numeric quality metrics available for visualization."
            )
            st.warning(warning_msg)
            return
        
        # Metrics selection
        col1, col2 = st.columns(2)
        
        with col1:
            selected_metric = st.selectbox(
                "Select Quality Metric",
                available_cols,
                format_func=lambda x: self.plotter._format_column_name(x),
                index=0
            )
        
        with col2:
            chart_type = st.selectbox(
                "Chart Type",
                ["Distribution", "Box Plot", "Scatter Plot"],
                index=0
            )
        
        # Create visualizations using plotter
        if selected_metric and chart_type:
            if chart_type == "Distribution":
                fig = self.plotter.create_distribution_plot(
                    filtered_data, selected_metric
                )
                self._render_plotly_chart(fig, "metrics_distribution")
                
            elif chart_type == "Box Plot":
                fig = self.plotter.create_box_plot(
                    filtered_data, selected_metric
                )
                self._render_plotly_chart(fig, "metrics_box_plot")
                
            elif chart_type == "Scatter Plot":
                # Find another metric for comparison
                other_metrics = [
                    col for col in available_cols
                    if col != selected_metric
                ]
                
                if other_metrics:
                    def format_metric_name(x):
                        return self.plotter._format_column_name(x)
                    
                    y_metric = st.selectbox(
                        "Select Y-axis metric",
                        other_metrics,
                        format_func=format_metric_name,
                        index=0
                    )
                    
                    if y_metric:
                        fig = self.plotter.create_scatter_plot(
                            filtered_data, selected_metric, y_metric
                        )
                        self._render_plotly_chart(fig, "metrics_scatter_plot")
                else:
                    warning_msg = (
                        "No additional metrics available for scatter plot."
                    )
                    st.warning(warning_msg)

    def render_sample_details_tab(self, filtered_data: pd.DataFrame):
        """Render detailed sample information."""
        st.header("🔬 Sample Details")
        
        # Get field names from mapping
        id_field = self._get_id_field()
        outcome_field = self._get_outcome_field()
        action_field = self._get_action_field()
        species_field = self._get_species_field()
        
        # Sample selection - use ID field from mapping
        if id_field and id_field in filtered_data.columns:
            sample_options = filtered_data[id_field].tolist()
        else:
            st.warning("No ID field configured in mapping.")
            return
        
        if not sample_options:
            st.warning("No samples match the current filters.")
            return
        
        selected_sample = st.selectbox(
            "Select Sample (based on filtered data)",
            sample_options
        )
        
        # Get sample data
        selected_filter = filtered_data[id_field] == selected_sample
        sample_data = filtered_data[selected_filter].iloc[0]
        
        # Display sample information
        col1, col2 = st.columns(2)
        
        with col1:
            st.subheader("Basic Information")
            st.write(f"**{id_field}:** {sample_data[id_field]}")
            
            if species_field and species_field in sample_data:
                species_val = sample_data.get(species_field, 'N/A')
                st.write(f"**{species_field}:** {species_val}")
            
            # Display QC outcome
            if outcome_field and outcome_field in sample_data:
                outcome = sample_data[outcome_field]
                st.write(f"**{outcome_field}:** {outcome}")
            
            # Display QC action with color highlighting if available
            if action_field and action_field in sample_data and pd.notna(sample_data.get(action_field)):
                action = sample_data[action_field]
                action_color = self._get_qc_action_color(action)
                st.markdown(
                    f"**{action_field}:** <span style='color: {action_color}; "
                    f"font-weight: bold;'>{action}</span>",
                    unsafe_allow_html=True
                )
        
        with col2:
            st.subheader("Quality Metrics")
            # Display available numeric quality metrics dynamically
            # Get system fields to skip from mapping
            system_fields = set()
            if id_field:
                system_fields.add(id_field)
            if outcome_field:
                system_fields.add(outcome_field)
            if action_field:
                system_fields.add(action_field)
            if species_field:
                system_fields.add(species_field)
            system_fields.update(['failed_rules', 'passed_rules', 'error'])
            
            quality_metrics_displayed = 0
            for col in sample_data.index:
                if quality_metrics_displayed >= 3:  # Limit to 3 metrics
                    break
                # Skip system columns
                if col in system_fields:
                    continue
                try:
                    val = sample_data[col]
                    if pd.notna(val) and isinstance(val, (int, float)):
                        # Format with 2 decimal places
                        st.write(f"**{col}:** {val:.2f}")
                        quality_metrics_displayed += 1
                except (ValueError, TypeError):
                    continue
        
        # Failed and passed rules
        st.subheader("QC Rules Analysis")
        
        col1, col2 = st.columns(2)
        
        with col1:
            failed_rules_val = sample_data.get('failed_rules')
            if (failed_rules_val and
                    pd.notna(failed_rules_val) and
                    isinstance(failed_rules_val, str)):
                st.write("**Failed Rules:**")
                failed_rules = failed_rules_val.split(',')
                # Display all failed rules, each on one row
                st.write("❌ " + ", ".join([rule.strip() for rule in failed_rules]))
            else:
                st.write("✅ No failed rules")
        
        with col2:
            passed_rules_val = sample_data.get('passed_rules')
            if (passed_rules_val and
                    pd.notna(passed_rules_val) and
                    isinstance(passed_rules_val, str)):
                st.write("**Passed Rules:**")
                passed_rules = passed_rules_val.split(',')
                # Display all passed rules, all on one row
                st.write("✅ " + ", ".join([rule.strip() for rule in passed_rules]))
            else:
                st.write("No passed rules data available")

    def render_qc_tests_tab(self):
        """Render the QC tests configuration tab."""
        st.header("⚙️ QC Tests Configuration")
        
        if self.qc_tests.empty:
            st.warning("No QC tests data available.")
            return
        
        # Display QC tests overview
        st.subheader("Available QC Tests")
        st.write(f"**Total QC tests configured:** {len(self.qc_tests)}")
        
        # QC tests selection
        st.subheader("Select QC Test")
        
        # Prepare QC tests dataframe for selection
        display_tests = self.qc_tests.copy()
        
        # Add formatted display columns
        display_tests['Priority_Label'] = display_tests['priority'].astype(str)
        display_tests['Test_Name'] = display_tests.apply(
            lambda row: row.get('outcome_name', row['outcome_id']), axis=1
        )
        
        # Select columns for display
        display_columns = [
            'outcome_id', 'Test_Name', 'Priority_Label',
            'action_required', 'description'
        ]
        
        # Ensure all columns exist
        for col in display_columns:
            if col not in display_tests.columns:
                display_tests[col] = 'N/A'
        
        # Sort by priority (highest first)
        display_tests = display_tests.sort_values('priority', ascending=False)
        
        # Create display dataframe
        selection_df = display_tests[display_columns].copy()
        selection_df.columns = [
            'Outcome ID', 'Test Name', 'Priority', 'Action Required',
            'Description'
        ]
        
        # Display selectable dataframe
        selected_rows = st.dataframe(
            selection_df,
            width='stretch',
            hide_index=True,
            on_select="rerun",
            selection_mode="single-row",
            key="qc_tests_selection"
        )
        
        # Get selected test
        if selected_rows['selection']['rows']:
            selected_idx = selected_rows['selection']['rows'][0]
            selected_test = display_tests.iloc[selected_idx]
        else:
            # Default to first test if none selected
            selected_test = display_tests.iloc[0]
            st.info("👆 Select a QC test from the table above to view details.")
        
        # Display test details
        st.subheader("Test Details")
        st.write(f"**Outcome ID:** {selected_test['outcome_id']}")
        outcome_name = selected_test.get('outcome_name', 'N/A')
        st.write(f"**Name:** {outcome_name}")
        st.write(f"**Priority:** {selected_test['priority']}")
        description = selected_test.get('description', 'N/A')
        st.write(f"**Description:** {description}")
        action = selected_test['action_required']
        st.write(f"**Action Required:** {action}")
        
        # Display rule conditions
        passed_conditions = selected_test.get('passed_rule_conditions', '')
        failed_conditions = selected_test.get('failed_rule_conditions', '')
        
        if pd.notna(passed_conditions) and passed_conditions:
            st.write(f"**Passed Rule Conditions:** {passed_conditions}")
        if pd.notna(failed_conditions) and failed_conditions:
            st.write(f"**Failed Rule Conditions:** {failed_conditions}")
        
        # Related QC Rules Table
        st.subheader("Related QC Rules")
        
        if self.qc_rules.empty:
            st.warning("No QC rules data available.")
            return
        
        # Parse rule conditions from the two columns
        passed_conditions = selected_test.get('passed_rule_conditions', '')
        failed_conditions = selected_test.get('failed_rule_conditions', '')
        
        has_passed = pd.notna(passed_conditions) and passed_conditions and passed_conditions.strip()
        has_failed = pd.notna(failed_conditions) and failed_conditions and failed_conditions.strip()
        
        if not has_passed and not has_failed:
            st.info("No rule conditions specified for this test.")
            return
        
        # Build description
        desc_parts = []
        if has_failed:
            failed_rule_ids = [r.strip() for r in failed_conditions.split(',')]
            desc_parts.append(f"ANY of {len(failed_rule_ids)} rules fail (OR logic)")
        if has_passed:
            passed_rule_ids = [r.strip() for r in passed_conditions.split(',')]
            desc_parts.append(f"ALL of {len(passed_rule_ids)} rules pass (AND logic)")
        
        st.info(f"This test triggers when: {' **AND** '.join(desc_parts)}")
        
        # Show failed rules section
        if has_failed:
            failed_rule_ids = [r.strip() for r in failed_conditions.split(',')]
            st.markdown("**Failed Rules (OR - any must fail):**")
            rules_table_data = self._get_rules_table_data(failed_rule_ids)
            if rules_table_data:
                rules_df = pd.DataFrame(rules_table_data)
                st.dataframe(rules_df, width='stretch', hide_index=True)
        
        # Show passed rules section
        if has_passed:
            passed_rule_ids = [r.strip() for r in passed_conditions.split(',')]
            st.markdown("**Passed Rules (AND - all must pass):**")
            rules_table_data = self._get_rules_table_data(passed_rule_ids)
            if rules_table_data:
                rules_df = pd.DataFrame(rules_table_data)
                st.dataframe(rules_df, width='stretch', hide_index=True)

    def _get_rules_table_data(self, rule_ids: list) -> list:
        """Get table data for a list of rule IDs."""
        rules_table_data = []
        for rule_id in rule_ids:
            matching_rules = self.qc_rules[
                self.qc_rules['rule_id'] == rule_id
            ]
            
            if not matching_rules.empty:
                rule = matching_rules.iloc[0]
                rules_table_data.append({
                    'Rule ID': rule_id,
                    'Species': rule.get('species', 'N/A'),
                    'Assembly Type': rule.get('assembly_type', 'N/A'),
                    'Software': rule.get('software', 'N/A'),
                    'Field': rule.get('field', 'N/A'),
                    'Operator': rule.get('operator', 'N/A'),
                    'Value': rule.get('value', 'N/A'),
                    'Special Field': rule.get('special_field', 'N/A')
                })
            else:
                rules_table_data.append({
                    'Rule ID': rule_id,
                    'Species': 'Rule not found',
                    'Assembly Type': '-',
                    'Software': '-',
                    'Field': '-',
                    'Operator': '-',
                    'Value': '-',
                    'Special Field': '-'
                })
        return rules_table_data

    def render_warnings_tab(self):
        """Render the warnings tab showing processing warnings and issues."""
        st.header("⚠️ Processing Warnings")
        
        if self.warnings is None or self.warnings.empty:
            st.info("No warnings file found or no warnings generated "
                    "during processing.")
            
            # Show information about warnings output
            warnings_path = (
                self.config.app.input.warnings if self.config.app else None
            )
            if warnings_path:
                st.write(f"**Expected warnings file:** `{warnings_path}`")
                st.write("Warnings will be saved here during the next "
                         "uQCme processing run.")
            else:
                st.write("No warnings input path configured in config.yaml")
            return
        
        # Display summary statistics
        st.subheader("📊 Warnings Summary")
        
        col1, col2, col3 = st.columns(3)
        
        with col1:
            total_warnings = len(self.warnings)
            st.metric("Total Warnings", total_warnings)
            # Add a divider
            st.markdown("---")

        with col2:
            warning_types = self.warnings['warning_type'].nunique()
            st.metric("Warning Types", warning_types)
        
        with col3:
            # Get most recent warning timestamp if available
            if 'timestamp' in self.warnings.columns:
                latest_warning = self.warnings['timestamp'].max()
                formatted_date = (
                    latest_warning.split('T')[0]
                    if 'T' in str(latest_warning)
                    else str(latest_warning)
                )
                st.metric("Latest Warning", formatted_date)
        
        # Filter by warning type
        st.subheader("🔍 Filter Warnings")
        
        col1, col2 = st.columns(2)
        
        with col1:
            warning_types = (
                ['All'] +
                sorted(self.warnings['warning_type'].unique().tolist())
            )
            selected_type = st.selectbox(
                "Warning Type",
                warning_types,
                key="warnings_type_filter"
            )
        
        with col2:
            # Search in warning messages
            search_term = st.text_input(
                "Search in messages",
                placeholder="Enter search term...",
                key="warnings_search"
            )
        
        # Apply filters
        filtered_warnings = self.warnings.copy()
        
        if selected_type != 'All':
            filtered_warnings = filtered_warnings[
                filtered_warnings['warning_type'] == selected_type
            ]
        
        if search_term:
            filtered_warnings = filtered_warnings[
                filtered_warnings['warning_message'].str.contains(
                    search_term, case=False, na=False
                )
            ]
        
        # Display warnings table
        st.subheader("📋 Warnings Details")
        
        if filtered_warnings.empty:
            st.info("No warnings match the current filters.")
        else:
            st.write(f"Showing {len(filtered_warnings)} of "
                     f"{len(self.warnings)} warnings")
            
            # Format warnings for display
            display_warnings = filtered_warnings.copy()
            
            # Format timestamp if available
            if 'timestamp' in display_warnings.columns:
                display_warnings['timestamp'] = (
                    pd.to_datetime(display_warnings['timestamp'])
                    .dt.strftime('%Y-%m-%d %H:%M:%S')
                )
            
            # Rename columns for better display
            column_mapping = {
                'warning_type': 'Type',
                'warning_message': 'Message',
                'timestamp': 'Timestamp'
            }
            
            display_warnings = display_warnings.rename(columns=column_mapping)
            
            # Display as a dataframe with styling
            st.dataframe(
                display_warnings,
                width='stretch',
                hide_index=True,
                column_config={
                    'Type': st.column_config.TextColumn(
                        'Type', width="small"
                    ),
                    'Message': st.column_config.TextColumn(
                        'Message', width="large"
                    ),
                    'Timestamp': st.column_config.TextColumn(
                        'Timestamp', width="medium"
                    )
                }
            )
            
            # Show warning type breakdown
            if len(filtered_warnings) > 1:
                st.subheader("📈 Warning Type Distribution")
                warning_counts = (
                    filtered_warnings['warning_type'].value_counts()
                )
                
                for warning_type, count in warning_counts.items():
                    percentage = (count / len(filtered_warnings)) * 100
                    st.write(f"**{warning_type}:** {count} warnings "
                             f"({percentage:.1f}%)")

    def _process_uploaded_file(self, uploaded_file) -> bool:
        """Process an uploaded raw data file and update self.data."""
        try:
            with st.spinner("Processing data..."):
                # Try to read as TSV first (standard for this tool)
                uploaded_file.seek(0)
                try:
                    df = pd.read_csv(uploaded_file, sep='\t')
                except Exception:
                    df = pd.DataFrame()

                # If it looks like it failed to parse (only 1 column), try CSV
                if len(df.columns) <= 1:
                    uploaded_file.seek(0)
                    try:
                        df_csv = pd.read_csv(uploaded_file, sep=',')
                        if len(df_csv.columns) > 1:
                            df = df_csv
                    except Exception:
                        # If CSV fails, stick with the TSV result
                        pass
                
                if df.empty:
                    st.error("Uploaded file is empty or could not be read.")
                    return False

                # Check for ID column using mapping configuration
                id_col = self._get_id_column(df)
                if not id_col:
                    id_field = self._get_id_field()
                    st.error(
                        f"⚠️ Could not find the configured ID column "
                        f"('{id_field}') in the uploaded file. Please check "
                        "your file format (TSV/CSV) and headers."
                    )
                    return False

                # Initialize processor
                processor = QCProcessor(self.config_path)
                
                # Load reference data
                processor.load_reference_data()
                
                # Set run data directly
                processor.run_data = df
                
                # Process
                processor.process_samples()
                
                # Get results
                self.data = processor.results
                
                # Convert warnings set to DataFrame for display
                warnings_data = []
                for warning in sorted(processor.warnings):
                    warnings_data.append({
                        'warning_type': 'processing',
                        'warning_message': warning,
                        'timestamp': pd.Timestamp.now().isoformat()
                    })
                for rule in sorted(processor.skipped_rules):
                    warning_msg = (f"Rule {rule} skipped due to "
                                   f"missing fields")
                    warnings_data.append({
                        'warning_type': 'skipped_rule',
                        'warning_message': warning_msg,
                        'timestamp': pd.Timestamp.now().isoformat()
                    })
                
                if warnings_data:
                    self.warnings = pd.DataFrame(warnings_data)
                else:
                    self.warnings = pd.DataFrame()
                
                st.success("Data processed successfully!")
                return True
                
        except Exception as e:
            st.error(f"Data was not processed successfully: {e}")
            return False

    def run(self):
        """Run the Streamlit application."""
        # Setup page
        self.setup_page()
        
        # Load data
        self.load_data()
        
        # Render header
        self.render_header()
        
        # Data Source Info & Override in Sidebar
        with st.sidebar:
            st.subheader("📁 Data Source")
            
            # Get source description
            source_desc = "Unknown"
            if (self.config.app and self.config.app.input and
                    self.config.app.input.data):
                data_config = self.config.app.input.data
                if isinstance(data_config, DataInput):
                    if data_config.api_call:
                        source_desc = f"API: {data_config.api_call}"
                    elif data_config.file:
                        source_desc = f"File: {data_config.file}"
                elif isinstance(data_config, dict):
                    if data_config.get('api_call'):
                        source_desc = f"API: {data_config['api_call']}"
                    elif data_config.get('file'):
                        source_desc = f"File: {data_config['file']}"
                elif isinstance(data_config, str):
                    source_desc = f"File: {data_config}"

            if not self.data.empty:
                source_info_placeholder = st.empty()
                
                with st.expander("Upload New Raw Data"):
                    st.write("Upload a raw run data file to process and "
                             "replace the current data.")
                    uploaded_file = st.file_uploader(
                        "Upload Run Data (TSV)",
                        type=['tsv', 'txt', 'csv'],
                        key="sidebar_uploader"
                    )
                    
                    if uploaded_file:
                        if self._process_uploaded_file(uploaded_file):
                            source_desc = f"User Upload: {uploaded_file.name}"
                        else:
                            # If processing failed, clear the data so we don't
                            # show stale data with a confusing error state
                            self.data = pd.DataFrame()
                
                if not self.data.empty:
                    source_info_placeholder.info(
                        f"**Current Source:**\n{source_desc}"
                    )
            
            st.markdown("---")
        
        # Check if data is loaded (if empty and no upload in sidebar)
        if self.data.empty:
            # Show specific message for API timeout/502 errors
            if hasattr(self, 'api_warning') and self.api_warning:
                error_type = self.api_warning.get('type', 'unknown')
                if error_type == '502':
                    st.warning(
                        "⚠️ **API returned 502 Bad Gateway error**\n\n"
                        "The server may be overloaded or temporarily "
                        "unavailable. **All samples from this API request "
                        "are affected** - no data was retrieved.\n\n"
                        "Please try again later or upload a data file "
                        "manually below."
                    )
                elif error_type == 'timeout':
                    st.warning(
                        "⚠️ **API request timed out**\n\n"
                        "The request took too long to complete (>30 seconds). "
                        "**All samples from this API request are affected** - "
                        "no data was retrieved.\n\n"
                        "This may indicate the server is processing many "
                        "samples or is under heavy load. Please try again "
                        "later or upload a data file manually below."
                    )
            else:
                st.info("No processed QC data found. Please upload a raw run "
                        "data file to process.")
            uploaded_file = st.file_uploader(
                "Upload Run Data (TSV)", type=['tsv', 'txt', 'csv'],
                key="main_uploader"
            )
            
            if uploaded_file:
                if not self._process_uploaded_file(uploaded_file):
                    st.stop()
            else:
                st.stop()

        # Get filtered data (this will also render sidebar metrics)
        filtered_data = self.render_sidebar_filters()
        
        # Main content tabs
        tabs = st.tabs([
            "📊 Data Preview",
            "📈 Overview",
            "🔍 Quality Metrics",
            "🔬 Sample Details",
            "⚙️ QC Tests",
            "⚠️ Warnings"
        ])
        
        with tabs[0]:
            self.render_data_tab(filtered_data)
        
        with tabs[1]:
            self.render_overview_tab(filtered_data)
        
        with tabs[2]:
            self.render_quality_metrics_tab(filtered_data)
        
        with tabs[3]:
            self.render_sample_details_tab(filtered_data)
        
        with tabs[4]:
            self.render_qc_tests_tab()
        
        with tabs[5]:
            self.render_warnings_tab()


def _run_dashboard():
    """Internal function to run the dashboard logic."""
    import argparse
    
    # Parse command line arguments
    parser = argparse.ArgumentParser(description='uQCme Dashboard')
    parser.add_argument(
        '--config',
        default=None,
        help='Path to configuration file'
    )
    
    try:
        args = parser.parse_args()
        config_path = args.config
    except SystemExit:
        # Streamlit might trigger this if --help is passed
        return

    # Logic to find config file
    if config_path:
        # User explicitly provided a path
        if not Path(config_path).exists():
            st.error(f"Configuration file '{config_path}' not found!")
            st.stop()
    else:
        # Try default locations
        possible_paths = ['config.yaml', 'config/config.yaml']
        found = False
        for path in possible_paths:
            if Path(path).exists():
                config_path = path
                found = True
                break
        
        if not found:
            # If no config found, we'll let QCDashboard load defaults
            # instead of stopping execution
            config_path = None
            st.info(
                "No configuration file found. Using default configuration."
            )
    
    # Initialize and run dashboard
    dashboard = QCDashboard(config_path)
    dashboard.run()


def main():
    """Entry point for the application script."""
    if st.runtime.exists():
        _run_dashboard()
    else:
        sys.argv = ["streamlit", "run", __file__] + sys.argv[1:]
        sys.exit(stcli.main())


if __name__ == "__main__":
    main()
