import
{
    Tracker,
    TrackerPart,
    PartGui,
    Student,
    MarkData,
    FilterHierarchy,
    RuleDetail,
    Part,
    CriterionState,
    Category,
    GradeDto,
    GradeSchemeDto,
} from '../models/ProgressTrackerModels';
import { ProgressTrackerAPI } from './ProgressTrackerAPI';
import { isShowAllOption, ucFirst } from '../utils/ProgressTrackerUtils'; // Import the utility function

// todo pt Make sure "partId" and "trackerPartId" is fully distinguished. Rename any occasions where partId is incorrectly used for "trackerPartId"

// Extend TrackerPart with createShowAllOption static method
// This allows creation of "Show All" options for categories
TrackerPart.createShowAllOption = function (category: string, categoryName: string): TrackerPart
{
    return {
        trackerPartId: -1, // Special ID for "Show All" option
        partId: -1,
        reference: `SHOW_ALL_${category}`,
        // todo pt Fix distinguishing category name & reference. Remove need for ucFirst.
        name: `Show all ${ucFirst(categoryName)}(s)`,
        category: category,
        parent: -1,
        calculationRulePreset: "",
    } as TrackerPart;
};

// Interface for snackbar notifications
export interface SnackbarState
{
    open: boolean;
    message: string;
    severity: 'success' | 'error' | 'info' | 'warning';
}

// Interface for sort configuration
export interface SortConfig
{
    sortBy: keyof Student | 'score' | 'grade' | null;
    sortDirection: 'asc' | 'desc';
}

// Progress tracker state manager class
export class ProgressTrackerStateManager
{
    // Core API service
    private api: ProgressTrackerAPI;

    // IDs for the tracker and module
    private trackerId: number;
    private moduleId: number;

    // Core data states
    private _tracker: Tracker | null = null;
    private _parts: TrackerPart[] = [];
    private _partGuis: PartGui[] = [];
    private _students: Student[] = [];
    private _partsById: { [id: number]: TrackerPart } = {};
    private _partsHierarchy: { [category: string]: FilterHierarchy } = {};
    private _levelToCategory: { [level: number]: string[] } = {}; // Mapping of level to categories for tab ordering
    // Store grade schemes with grades:
    private _gradeSchemes: { [schemeRef: string]: GradeSchemeDto } = {}
    private _grades: { [schemeRef: string]: GradeDto[] } = {}

    // Mark data and loading states
    private _markDataByPart: { [partId: number]: { [studentId: number]: MarkData } } = {};
    private _markScreenLoading: { [partId: number]: boolean } = {};
    private _loading: boolean = true;
    private _error: string | null = null;

    // Filter and selection states
    private _selectedPartType: string | null = null;
    private _hierarchySelections: { [category: string]: TrackerPart | null } = {};
    private _visiblePartIds: Set<number> = new Set();
    private _activeTabIndex: number = 0;

    // Tab-specific state storage to prevent state loss when switching tabs
    private _tabState: {
        [tabIndex: number]: {
            showSummaryView: boolean;
            summaryParts: Part[];
            summaryPartMarks: { [partId: number]: { [studentId: number]: MarkData } };
            visiblePartIds: Set<number>;
            hierarchySelections: { [category: string]: TrackerPart | null };
        }
    } = {};

    // Summary view states
    private _showSummaryView: boolean = false;
    private _summaryParts: Part[] = [];
    private _summaryPartMarks: { [partId: number]: { [studentId: number]: MarkData } } = {};
    private _summaryViewLoading: boolean = false;

    // Rules dialog states
    private _rulePresetDialogOpen: boolean = false;
    private _ruleDetails: RuleDetail | null = null;
    private _ruleLoading: boolean = false;

    // Editing and sorting states
    private _hasChangesByPart: { [partId: number]: boolean } = {};
    private _manuallyModifiedCriteriaByPart: {
        [partId: number]: { [studentId: number]: { [criterionName: string]: boolean } }
    } = {};
    private _sortConfigByPart: {
        [partId: number]: SortConfig
    } = {};
    private _saveStatusByPart: { [partId: number]: 'idle' | 'saving' | 'saved' | 'error' } = {};

    // UI notification state
    private _snackbar: SnackbarState = {
        open: false,
        message: '',
        severity: 'success'
    };

    // State change callbacks
    private stateChangeCallbacks: (() => void)[] = [];

    constructor(trackerId: number, moduleId: number, api: ProgressTrackerAPI)
    {
        this.trackerId = trackerId;
        this.moduleId = moduleId;
        this.api = api;
    }

    // Register a callback for state changes
    public registerStateChangeCallback(callback: () => void): void
    {
        this.stateChangeCallbacks.push(callback);
    }

    // Notify all registered callbacks
    private notifyStateChange(): void
    {
        this.stateChangeCallbacks.forEach(callback => callback());
    }

    // Initialize the state manager
    public async initialize(): Promise<void>
    {
        this._loading = true;
        this._error = null;
        this.notifyStateChange();

        try
        {
            await this.loadStudents();
            await this.loadTrackerData();
            await this.loadGradeData();
            this._loading = false;
        } catch (error)
        {
            this._loading = false;
            this._error = error instanceof Error ? error.message : 'An unknown error occurred';
        }

        this.notifyStateChange();
    }

    // Load students from module
    private async loadStudents(): Promise<void>
    {
        try
        {
            const students = await this.api.fetchModuleStudents(this.moduleId);
            this._students = students;
        } catch (error)
        {
            console.error('Error loading students:', error);
            throw new Error('Failed to load students for this module');
        }
    }

    private async loadGradeData(): Promise<void>
    {
        const gradeData = await this.api.fetchGradeData();

        // Clear existing data
        this._gradeSchemes = {};
        this._grades = {};

        // Group grades by scheme reference
        const gradesByScheme: { [schemeRef: string]: GradeDto[] } = {};

        // Process each GradeWithSchemeDto
        gradeData.forEach(gradeWithScheme => {
            const schemeRef = gradeWithScheme.SchemeRef;

            // Add to grade schemes map if not already present
            if (!this._gradeSchemes[schemeRef]) {
                this._gradeSchemes[schemeRef] = {
                    Ref: schemeRef,
                    Name: gradeWithScheme.SchemeName
                };
            }

            // Initialize grades array for this scheme if not present
            if (!gradesByScheme[schemeRef]) {
                gradesByScheme[schemeRef] = [];
            }
              // Add the grade data (extracting the grade-specific properties using destructuring)
            const { SchemeName, SchemeRef, ...gradeData } = gradeWithScheme;
            gradesByScheme[schemeRef].push(gradeData);
        });

        // Assign the grouped grades to the instance property
        this._grades = gradesByScheme;

        console.log('this._grades', this._grades);
        console.log('this._gradeSchemes', this._gradeSchemes);
    }

    // Load tracker data (tracker, parts, and GUIs)
    private async loadTrackerData(): Promise<void>
    {
        try
        {
            // Fetch tracker data, parts, and GUIs in parallel
            const [tracker, parts, guis] = await Promise.all([
                this.api.fetchTracker(this.trackerId),
                this.api.fetchTrackerParts(this.trackerId),
                this.api.fetchPartGuis(this.trackerId)
            ]);

            console.log('============');
            console.log('============');
            console.log('============');
            console.log('============');
            console.log('== PARTS ==', parts);

            this._tracker = tracker;
            this._parts = parts;
            this._partGuis = guis;      // Build map of parts by ID for easy reference
            this._partsById = {};
            parts.forEach(part =>
            {
                this._partsById[part.trackerPartId] = part;
            });            // Build hierarchy information for categories
            this._partsHierarchy = this.buildPartsHierarchy(parts);

            // Initialize hierarchy selections with root parts
            this.initializeHierarchySelections();

            // Set initial selected part type (first tab) based on category level
            this.setSelectedPartTypeByLowestLevel();

        } catch (error)
        {
            console.error('Error loading tracker data:', error);
            throw new Error('Failed to load tracker data');
        }
    }

    // Initialize hierarchy selections with root parts
    private initializeHierarchySelections(): void
    {
        const initialSelections: { [category: string]: TrackerPart | null } = {};

        // Find root categories
        const rootCategories = Object.values(this._partsHierarchy)
            .filter(h => h.isRoot)
            .map(h => h.category);    // Pre-select root parts (qualifications) - typically just one
        rootCategories.forEach(category =>
        {
            const rootPartsOfCategory = this._parts.filter(p =>
                p.category === category && !p.parent
            );

            if (rootPartsOfCategory.length === 1)
            {
                // There's only one root part of this category, select it
                initialSelections[category] = rootPartsOfCategory[0];

                // Load mark data for this root part
                this.loadMarkDataForPart(rootPartsOfCategory[0].trackerPartId);
            }
        });

        this._hierarchySelections = initialSelections;    // Update visible part IDs with the root parts
        const rootPartIds = new Set<number>();
        Object.values(initialSelections).forEach(part =>
        {
            if (part) rootPartIds.add(part.trackerPartId);
        });
        this._visiblePartIds = rootPartIds;
    }    // Build a hierarchy map of parts categories


    private buildPartsHierarchy(parts: TrackerPart[]): { [category: string]: FilterHierarchy }
    {
        // PHASE 1: Build a complete hierarchy of tracker parts

        // Create a map of parts by ID for quick lookup
        const partsById: { [id: number]: TrackerPart } = {};
        parts.forEach(part => {
            partsById[part.trackerPartId] = part;
        });

        // Build parent-child relationships map
        const childrenByPartId: { [id: number]: number[] } = {};
        parts.forEach(part => {
            if (part.parent) {
                if (!childrenByPartId[part.parent]) {
                    childrenByPartId[part.parent] = [];
                }
                childrenByPartId[part.parent].push(part.trackerPartId);
            }
        });

        // Build a map to store the level (depth) of each part in the hierarchy
        const partLevels: { [id: number]: number } = {};

        // Recursive function to determine part levels
        const determinePartLevel = (partId: number, level: number): void => {
            partLevels[partId] = level;

            // Process all children
            const children = childrenByPartId[partId] || [];
            children.forEach(childId => {
                determinePartLevel(childId, level + 1);
            });
        };

        // Start with root parts (no parent) and determine levels
        parts.filter(part => !part.parent).forEach(rootPart => {
            determinePartLevel(rootPart.trackerPartId, 0);
        });

        // PHASE 2: Now build the category hierarchy based on the part hierarchy

        const result: { [category: string]: FilterHierarchy } = {};
        const categoriesByPart: { [id: number]: string } = {};

        // Map each part ID to its category
        parts.forEach(part => {
            categoriesByPart[part.trackerPartId] = part.category;
        });

        // Track highest level part for each category
        const categoryMaxLevelPart: { [category: string]: { level: number, partId: number } } = {};

        // Find the highest level part for each category
        parts.forEach(part => {
            const category = part.category;
            const level = partLevels[part.trackerPartId];

            if (!categoryMaxLevelPart[category] || categoryMaxLevelPart[category].level < level) {
                categoryMaxLevelPart[category] = { level, partId: part.trackerPartId };
            }
        });

        // Process each part to build category hierarchy
        parts.forEach(part => {
            const category = part.category;

            // Skip if we've already processed this category
            if (result[category]) return;

            // New category entry
            let parentCategory: string | null = null;
            let isRoot = false;

            // Determine if this is a root category
            if (!part.parent) {
                isRoot = true;
            } else {
                // Find the parent category
                const parentPart = partsById[part.parent];
                if (parentPart) {
                    parentCategory = parentPart.category;
                }
            }

            // Determine the level for this category based on part hierarchy
            let level = 0;
            if (parentCategory) {
                // If we already know the parent category's level, use that + 1
                if (result[parentCategory]) {
                    level = result[parentCategory].level + 1;
                } else {
                    // Try to infer from part levels
                    const parentCategoryLevel = Math.min(
                        ...parts.filter(p => p.category === parentCategory)
                            .map(p => partLevels[p.trackerPartId])
                    );

                    level = parentCategoryLevel + 1;

                    // Fallback if we couldn't determine level
                    if (!Number.isFinite(level)) {
                        level = isRoot ? 0 : 1;
                    }
                }
            }

            result[category] = {
                category,
                level,
                parentCategory,
                categoryName: category, // Use the category string as the name
                isRoot
            };
        });

        return result;
    }// Get part type groups


    public getPartTypeGroups(): { [key: string]: TrackerPart[] }
    {
        const groups: { [key: string]: TrackerPart[] } = {};

        this._parts.forEach(part =>
        {
            const category = part.category;
            if (!groups[category])
            {
                groups[category] = [];
            }
            groups[category].push(part);
        });

        return groups;
    }

    // Get parent categories for a given category
    public getParentCategories(category: string): string[]
    {
        const result: string[] = [];
        let currentCategory = category;
        let visited = new Set<string>();

        while (currentCategory && !visited.has(currentCategory))
        {
            visited.add(currentCategory);
            const hierarchyInfo = this._partsHierarchy[currentCategory];

            // If we have a parent category, add it to the beginning of the array
            if (hierarchyInfo?.parentCategory)
            {
                result.unshift(hierarchyInfo.parentCategory);
                currentCategory = hierarchyInfo.parentCategory;
            } else
            {
                break;
            }
        }

        return result;
    }

    // Get child categories for a given category
    public getChildCategories(category: string): string[]
    {
        const result: string[] = [];
        const visited = new Set<string>();
        const toProcess = [category]; // Queue of categories to process

        while (toProcess.length > 0)
        {
            const currentCategory = toProcess.shift()!;

            // Skip if we've already processed this category
            if (visited.has(currentCategory)) continue;
            visited.add(currentCategory);

            // Find all categories that have this one as parent
            Object.values(this._partsHierarchy).forEach(hierarchy =>
            {
                if (hierarchy.parentCategory === currentCategory)
                {
                    const childCategory = hierarchy.category;
                    // Add to result if not the original category
                    if (childCategory !== category)
                    {
                        result.push(childCategory);
                    }
                    // Add to processing queue
                    toProcess.push(childCategory);
                }
            });
        }

        return result;
    }

    // Load mark data for a specific part
    public async loadMarkDataForPart(partId: number): Promise<void>
    {
        this._markScreenLoading = { ...this._markScreenLoading, [partId]: true };
        this.notifyStateChange();

        try
        {
            const marksData = await this.api.fetchMarkData(partId);
            this._markDataByPart = { ...this._markDataByPart, [partId]: marksData };
        } catch (error)
        {
            console.error("Error loading mark data for part:", partId, error);
        } finally
        {
            this._markScreenLoading = { ...this._markScreenLoading, [partId]: false };
            this.notifyStateChange();
        }
    }

    // Reload mark data for a specific part (used after successful save)
    public async reloadMarkDataForPart(partId: number): Promise<void>
    {
        // Set loading state
        this._markScreenLoading = { ...this._markScreenLoading, [partId]: true };
        this.notifyStateChange();

        try
        {
            // Fetch fresh mark data from API
            const marksData = await this.api.fetchMarkData(partId);

            // Update the mark data
            this._markDataByPart = { ...this._markDataByPart, [partId]: marksData };

            // Clear the changes flag since we now have fresh data
            this._hasChangesByPart = { ...this._hasChangesByPart, [partId]: false };

            console.log(`Successfully reloaded mark data for part ${partId}`);
        } catch (error)
        {
            console.error("Error reloading mark data for part:", partId, error);

            // Set error snackbar
            this._snackbar = {
                open: true,
                message: 'Failed to reload latest data. Please refresh the page.',
                severity: 'error'
            };
        } finally
        {
            // Clear loading state
            this._markScreenLoading = { ...this._markScreenLoading, [partId]: false };
            this.notifyStateChange();
        }
    }

    // Get all parent part IDs for a given part ID
    private getParentPartIds(partId: number): number[]
    {
        const parentIds: number[] = [];
        const part = this._partsById[partId];

        if (!part) {
            return parentIds;
        }

        // Traverse up the hierarchy
        let currentParentId = part.parent;
        while (currentParentId && this._partsById[currentParentId]) {
            parentIds.push(currentParentId);
            currentParentId = this._partsById[currentParentId].parent;
        }

        return parentIds;
    }

    // Reload mark data for a specific part and all its parents (used after successful save)
    public async reloadMarkDataForPartAndParents(partId: number): Promise<void>
    {
        console.log(`Reloading data for part ${partId} and its parents`);

        // Get all part IDs to reload (current part + all parents)
        const partIdsToReload = [partId, ...this.getParentPartIds(partId)];

        console.log(`Parts to reload: ${partIdsToReload.join(', ')}`);

        // Set loading state for all parts
        const updatedLoadingState = { ...this._markScreenLoading };
        partIdsToReload.forEach(id => {
            updatedLoadingState[id] = true;
        });
        this._markScreenLoading = updatedLoadingState;
        this.notifyStateChange();

        try
        {
            // Reload all parts in parallel for better performance
            await Promise.all(partIdsToReload.map(async (id) => {
                try {
                    console.log(`Fetching fresh data for part ${id}`);
                    const marksData = await this.api.fetchMarkData(id);

                    // Update the mark data for this part
                    this._markDataByPart = { ...this._markDataByPart, [id]: marksData };

                    // Clear the changes flag since we now have fresh data
                    this._hasChangesByPart = { ...this._hasChangesByPart, [id]: false };

                    console.log(`Successfully reloaded mark data for part ${id}`);
                } catch (error) {
                    console.error(`Error reloading mark data for part ${id}:`, error);
                    throw error; // Re-throw to be caught by outer try-catch
                }
            }));

            console.log('Successfully reloaded all part data');
        } catch (error)
        {
            console.error("Error reloading mark data for part and parents:", error);

            // Set error snackbar
            this._snackbar = {
                open: true,
                message: 'Failed to reload latest data. Please refresh the page.',
                severity: 'error'
            };
        } finally
        {
            // Clear loading state for all parts
            const updatedLoadingStateFinal = { ...this._markScreenLoading };
            partIdsToReload.forEach(id => {
                updatedLoadingStateFinal[id] = false;
            });
            this._markScreenLoading = updatedLoadingStateFinal;
            this.notifyStateChange();
        }
    }    // Reload all related data for current view (used after successful save)
    public async reloadCurrentView(): Promise<void>
    {
        const currentCategory = this._selectedPartType;
        const currentSelection = currentCategory ? this._hierarchySelections[currentCategory] : null;

        if (!currentSelection) {
            console.warn("No current selection to reload");
            return;
        }

        // If it's a "Show All" selection, reload summary view and parents
        if (currentSelection.trackerPartId === -1) {
            await this.reloadSummaryViewAndParents(currentCategory);
        } else {
            // Otherwise reload the specific part and all its parents
            await this.reloadMarkDataForPartAndParents(currentSelection.trackerPartId);
        }
    }

    // Load data for summary view
    public async loadSummaryView(category: string): Promise<void>
    {
        this._summaryViewLoading = true;
        this._showSummaryView = true;
        this.notifyStateChange();

        try
        {      // Category should already be a string with updated interface
            const categoryRef = category;

            // Get hierarchy info for this category
            const hierarchyInfo = this._partsHierarchy[categoryRef];
            if (!hierarchyInfo)
            {
                throw new Error(`No hierarchy info for category: ${categoryRef}`);
            }

            // Get parts to include in summary based on selected parent
            let partsToInclude: TrackerPart[] = [];
            const partTypeGroups = this.getPartTypeGroups();

            if (hierarchyInfo.parentCategory)
            {
                // Get the selected parent
                const parentSelection = this._hierarchySelections[hierarchyInfo.parentCategory];

                if (!parentSelection)
                {
                    // Parent category not selected yet
                    console.log(`Parent category ${hierarchyInfo.parentCategory} not selected for ${categoryRef}`);
                    this._showSummaryView = false;
                    this.notifyStateChange();
                    return;
                }        // Get parts that have this parent
                partsToInclude = partTypeGroups[categoryRef]?.filter(part =>
                    part.parent === parentSelection.trackerPartId
                ) || [];
            } else
            {
                // Root category - show all parts in this category
                partsToInclude = partTypeGroups[categoryRef] || [];
            }

            // If no parts to show, exit gracefully
            if (partsToInclude.length === 0)
            {
                console.log(`No parts to include for category: ${categoryRef}`);
                this._showSummaryView = false;
                this.notifyStateChange();
                return;
            }

            // Prepare Part objects for PTMarkScreen
            const summaryPartsResult: Part[] = [];

            for (const part of partsToInclude)
            {        // Get GUI config for this part
                const guiConfig = this._partGuis.find(gui => gui.partId === part.trackerPartId);

                if (!guiConfig)
                {
                    console.warn(`No GUI config found for part: ${part.trackerPartId}`);
                    continue;
                }

                // Convert TrackerPart to Part with updated property names
                summaryPartsResult.push(Part.fromTrackerPart(part, guiConfig));
            }

            // Update summary parts
            this._summaryParts = summaryPartsResult;      // Load mark data for all parts in summary
            console.log('summaryPartsResult', summaryPartsResult);
            if (summaryPartsResult.length > 0)
            {
                const partIds = summaryPartsResult.map(part => part.id);
                const markDataResult: { [partId: number]: { [studentId: number]: MarkData } } = {};

                // Fetch data for all parts in parallel
                await Promise.all(partIds.map(async (partId) =>
                {
                    try
                    {
                        const markData = await this.api.fetchMarkData(partId);
                        markDataResult[partId] = markData;

                        // Also update the regular markDataByPart
                        this._markDataByPart = { ...this._markDataByPart, [partId]: markData };
                    } catch (error)
                    {
                        console.error(`Failed to fetch mark data for part ${partId}:`, error);
                        markDataResult[partId] = {};
                    }
                }));

                // Update summary part marks
                this._summaryPartMarks = markDataResult;

                // Update visible part IDs
                this._visiblePartIds = new Set(summaryPartsResult.map(part => part.id));
            }

        } catch (error)
        {
            console.error("Error loading summary view:", error);
            this._error = "Failed to load summary view. Please try again.";
            this._showSummaryView = false;
        } finally
        {
            this._summaryViewLoading = false;
            this.notifyStateChange();
        }
    }  // Handle filter change
    public async handleFilterChange(category: string, part: TrackerPart | null): Promise<void>
    {
        // Check if this is a "Show All" selection
        const isShowAll = part !== null && part.trackerPartId === -1;    // Ensure category is a string (since it should already be with the updated interface)
        const categoryRef = category;

        // Update the selection for this category
        this._hierarchySelections = { ...this._hierarchySelections, [categoryRef]: part };

        // Clear all child selections that depend on this category
        const childCategories = this.getChildCategories(categoryRef);
        childCategories.forEach(childCat =>
        {
            this._hierarchySelections = { ...this._hierarchySelections, [childCat]: null };
        });

        // Handle "Show All" special case - load data for all parts
        if (isShowAll)
        {
            await this.loadSummaryView(categoryRef);
        } else if (part && !this._markDataByPart[part.trackerPartId])
        {
            // If a specific part is selected, load its data
            await this.loadMarkDataForPart(part.trackerPartId);
            this._showSummaryView = false;
        }

        // Update visible part IDs
        if (part)
        {
            if (isShowAll)
            {
                // We'll populate this in loadSummaryView
            } else
            {
                // Regular part selection
                this._visiblePartIds = new Set([part.trackerPartId]);
            }
        }

        this.notifyStateChange();
    }

    /**
       * Handle tab change
       */
    public handleTabChange(index: number): void
    {
        // Skip if already on this tab
        if (this._activeTabIndex === index) return;

        const previousTabIndex = this._activeTabIndex;

        // Save current tab state
        this._tabState[previousTabIndex] = {
            showSummaryView: this._showSummaryView,
            summaryParts: [...this._summaryParts],
            summaryPartMarks: { ...this._summaryPartMarks },
            visiblePartIds: new Set([...this._visiblePartIds]),
            hierarchySelections: { ...this._hierarchySelections }
        };

        this._activeTabIndex = index;    // Get the category for the new tab
        const categories = this.getUniquePartTypes();
        if (categories.length <= index)
        {
            return; // Invalid tab index
        }

        const newCategory = categories[index].ref;
        this._selectedPartType = newCategory;

        // Check if we've visited this tab before
        if (this._tabState[index])
        {
            console.log(`Restoring saved state for tab ${index} (${newCategory})`);
            const tabState = this._tabState[index];

            // Restore most of the tab's view state
            this._showSummaryView = tabState.showSummaryView;
            this._summaryParts = tabState.summaryParts;
            this._summaryPartMarks = tabState.summaryPartMarks;
            this._visiblePartIds = tabState.visiblePartIds;

            // IMPROVED SELECTION MERGING:
            // Start with the current hierarchy selections to maintain all current filters
            // Then restore tab-specific view state from saved tab state
            const mergedSelections = { ...this._hierarchySelections };

            // Get the tab's saved selection for its own category (if any)
            const savedTabCategorySelection = tabState.hierarchySelections[newCategory];

            // Always prioritize the current selection for this tab's category if available
            // This ensures that if a filter for this tab's category was changed elsewhere, it's preserved
            mergedSelections[newCategory] = mergedSelections[newCategory] || savedTabCategorySelection;

            // Apply the merged selections
            this._hierarchySelections = mergedSelections;
        } else
        {
            // No saved state - initializing this tab for the first time
            console.log(`Initializing new tab ${index} (${newCategory})`);

            // If we don't have a selection for this tab's category yet, set default
            if (!this._hierarchySelections[newCategory])
            {
                // For root categories (like Qualification tab)
                const isRootCategory = this._partsHierarchy[newCategory]?.isRoot;

                // Create a "Show All" option for the new category
                const categoryName = this._partsHierarchy[newCategory]?.categoryName || newCategory;
                const showAllOption = TrackerPart.createShowAllOption(newCategory, categoryName);

                this._hierarchySelections = {
                    ...this._hierarchySelections,
                    [newCategory]: showAllOption
                };
            }
        }

        // Instead of handling the selection manually here, let's delegate to handleFilterChange
        // This will ensure we trigger exactly the same process as a manual selection
        const currentSelection = this._hierarchySelections[newCategory];

        if (currentSelection)
        {
            // Explicitly call handleFilterChange with the current selection to trigger the full rendering cycle
            // We wrap in Promise.resolve to handle the async nature but keep the flow synchronous
            Promise.resolve(this.handleFilterChange(newCategory, currentSelection))
                .catch(err => console.error("Error updating view after tab change:", err));
        } else
        {
            // If no selection, we still need to notify for UI updates
            this.notifyStateChange();
        }
    }

    /**
     * Helper method to check if a selection is valid for a specific tab context
     */
    private isValidSelectionForTab(selection: TrackerPart, tabCategory: string): boolean
    {
        // If it's a Show All option, it's always valid
        if (isShowAllOption(selection)) return true;
        // If it's the same category as the tab, it's valid
        if (selection.category === tabCategory) return true;

        // For other categories, check if there are any parts in the tab's category
        // that are connected to this selection
        const hasRelatedParts = this._parts.some(part =>
        {      // Check if this part belongs to the tab's category
            if (part.category !== tabCategory) return false;

            // Check if this part is directly or indirectly related to the selection
            return this.isPartRelatedToSelection(part, selection);
        });

        return hasRelatedParts;
    }
    /**
   * Check if a part is related to a selected part (directly or through ancestry)
   */
    private isPartRelatedToSelection(part: TrackerPart, selection: TrackerPart): boolean
    {
        // Direct relationship (part's parent is the selection)
        if (part.parent === selection.trackerPartId) return true;

        // Check indirect relationship through ancestry
        let currentPart = part;
        while (currentPart.parent)
        {
            const parentPart = this._partsById[currentPart.parent];
            if (!parentPart) break;
            // If any ancestor is the selection, there's a relationship
            if (parentPart.trackerPartId === selection.trackerPartId) return true;

            currentPart = parentPart;
        }

        return false;
    }
    /**
     * Check if all required parent categories are selected for a category
     */
    private canShowSummaryForCategory(category: string): boolean
    {
        // Get hierarchy info for this category
        const hierarchyInfo = this._partsHierarchy[category];
        if (!hierarchyInfo) return false;

        // If it has a parent category, check if that's selected
        if (hierarchyInfo.parentCategory)
        {
            const parentSelection = this._hierarchySelections[hierarchyInfo.parentCategory];
            if (!parentSelection)
            {
                // Parent category not selected, can't show summary
                return false;
            }
        }

        return true;
    }

    /**
     * Determine if we should load summary view for a category
     */
    private shouldLoadSummaryView(category: string | null): boolean
    {
        if (!category) return false;

        // First check if all required parent categories are selected
        if (!this.canShowSummaryForCategory(category))
        {
            return false;
        }

        // Check if the category has a "Show All" selection
        const currentSelection = this._hierarchySelections[category];

        // Only load summary if we have a "Show All" option selected
        return currentSelection && currentSelection.trackerPartId === -1;
    }
    /**
     * Update visible parts based on current filter selections
     */
    private updateVisibleParts(): void
    {
        // Clear previous visible parts
        this._visiblePartIds = new Set<number>();

        // Handle each filter selection
        for (const category in this._hierarchySelections)
        {
            const selection = this._hierarchySelections[category];
            if (selection)
            {
                // If it's a "Show All" option, add all parts of that category
                if (selection.trackerPartId === -1)
                {
                    this._parts.forEach(part =>
                    {
                        if (part.category === category)
                        {
                            this._visiblePartIds.add(part.trackerPartId);
                        }
                    });
                } else
                {
                    // Otherwise just add the selected part
                    this._visiblePartIds.add(selection.trackerPartId);
                }
            }
        }    // Also include all parts that should be visible based on parent-child relationships
        this._parts.forEach(part =>
        {
            if (this.isPartVisibleBasedOnHierarchy(part.trackerPartId))
            {
                this._visiblePartIds.add(part.trackerPartId);
            }
        });
    }  /**
   * Check if a part should be visible based on hierarchy relationships
   */
    private isPartVisibleBasedOnHierarchy(partId: number): boolean
    {
        const part = this._partsById[partId];
        if (!part) return false;

        // Check if any parent category has a selection that would make this part visible
        if (part.parent)
        {
            const parentPart = this._partsById[part.parent];
            if (!parentPart) return false;

            const parentCategorySelection = this._hierarchySelections[parentPart.category];

            // If parent is explicitly selected, this part might be visible
            if (parentCategorySelection && parentCategorySelection.trackerPartId === parentPart.trackerPartId)
            {
                // If this part's category matches the currently selected tab, it's visible
                return part.category === this._selectedPartType;
            }

            // If parent has "Show All" selected, and this part matches current tab
            if (parentCategorySelection && parentCategorySelection.trackerPartId === -1 &&
                part.category === this._selectedPartType)
            {
                return true;
            }

            // Recursively check parent visibility
            return this.isPartVisibleBasedOnHierarchy(part.parent);
        }

        return false;
    }    // Get unique part types for tabs
    public getUniquePartTypes(): { ref: string; name: string }[]
    {
        const partTypeGroups = this.getPartTypeGroups();

        // Use level-based ordering for categories if available
        if (Object.keys(this._levelToCategory).length > 0) {
            // Get categories in order of increasing level
            const orderedCategories: string[] = [];

            // Get all levels, sort them numerically
            const levels = Object.keys(this._levelToCategory)
                .map(Number)
                .sort((a, b) => a - b);

            // Add categories from each level in order
            levels.forEach(level => {
                this._levelToCategory[level].forEach(category => {
                    if (partTypeGroups[category]) {
                        orderedCategories.push(category);
                    }
                });
            });

            // Return ordered categories with their names
            return orderedCategories.map(key => ({
                ref: key,
                name: this._partsHierarchy[key]?.categoryName || key
            }));
        }

        // Fallback to simple alphabetical order if no level information
        return Object.keys(partTypeGroups).map(key => ({
            ref: key,
            name: this._partsHierarchy[key]?.categoryName || key
        }));
    }
    // Clear all filter selections except root categories
    public async handleClearFilters(): Promise<void>
    {
        // Create new state with only root selections
        const rootSelections: { [category: string]: TrackerPart | null } = {};

        // Only keep root category selections
        Object.entries(this._hierarchySelections).forEach(([category, part]) =>
        {
            if (this._partsHierarchy[category]?.isRoot)
            {
                rootSelections[category] = part;
            }
        });

        this._hierarchySelections = rootSelections;

        // Stay on the current tab and create "Show All" option
        const currentCategory = this._selectedPartType;
        if (currentCategory && !this._partsHierarchy[currentCategory]?.isRoot)
        {
            const categoryName = this._partsHierarchy[currentCategory]?.categoryName || currentCategory;
            const showAllOption = TrackerPart.createShowAllOption(currentCategory, categoryName);

            await this.handleFilterChange(currentCategory, showAllOption);
        }

        // Update visible part IDs with only root parts
        const rootPartIds = new Set<number>();
        Object.values(rootSelections).forEach(part =>
        {
            if (part) rootPartIds.add(part.trackerPartId);
        });
        this._visiblePartIds = rootPartIds;

        this.notifyStateChange();
    }

    // Check if content should be shown based on filter selections
    public shouldShowContent(category: string): boolean
    {
        // Get parent categories excluding root categories
        const parentFilters = this.getParentCategories(category)
            .filter(parentCat => !this._partsHierarchy[parentCat]?.isRoot);

        // Check if all parent categories have a selection
        const allParentsHaveSelection = parentFilters.every(parentCat =>
        {
            const selection = this._hierarchySelections[parentCat];
            return selection !== null;
        });

        // The current category must have some selection
        const currentCategoryHasSelection = this._hierarchySelections[category] !== null;

        return allParentsHaveSelection && currentCategoryHasSelection;
    }

    // Get name of missing filter
    public getMissingFilterName(category: string): string
    {
        // Get parent categories in hierarchical order
        const filterChain = [...this.getParentCategories(category), category];

        // Find the first one that doesn't have a selection
        for (const cat of filterChain)
        {
            if (!this._hierarchySelections[cat] && !this._partsHierarchy[cat]?.isRoot)
            {
                return this._partsHierarchy[cat]?.categoryName || cat;
            }
        }

        return "filter"; // Default fallback
    }

    // Get deepest selected filter
    public getDeepestSelectedFilter(): string | null
    {
        let maxLevel = -1;
        let deepestCategory = null;

        for (const category in this._hierarchySelections)
        {
            if (this._hierarchySelections[category] && this._partsHierarchy[category])
            {
                const level = this._partsHierarchy[category].level;
                if (level > maxLevel)
                {
                    maxLevel = level;
                    deepestCategory = category;
                }
            }
        }

        return deepestCategory;
    }  // Handle part header click in summary view
    public async handlePartHeaderClick(partId: number): Promise<void>
    {
        // Find the part in our parts data
        const part = this._partsById[partId];
        if (!part) return;

        // Get the category of the part
        const category = part.category;

        // Update the selection for this category to the specific part
        await this.handleFilterChange(category, part);
    }

    // Handle mark change (grades, scores)
    public handleMarkChange(partId: number, studentId: number, field: keyof MarkData, value: any): void
    {
        // If "changes" is the field, just update the has changes flag
        if (field === 'changes' as keyof MarkData)
        {
            this._hasChangesByPart = { ...this._hasChangesByPart, [partId]: value };
            this.notifyStateChange();
            return;
        }

        // Update mark data for the specific part and student
        if (!this._markDataByPart[partId])
        {
            this._markDataByPart[partId] = {};
        }

        if (!this._markDataByPart[partId][studentId])
        {
            this._markDataByPart[partId][studentId] = new MarkData();
        }

        // Create updated mark data
        const updatedMarkData = { ...this._markDataByPart[partId][studentId] };

        // Update the specific field
        if (field === 'grades')
        {
            updatedMarkData.grade = value;
        } else if (field === 'score')
        {
            updatedMarkData.score = value;
        } else if (field === 'criteria')
        {
            updatedMarkData.criteria = { ...updatedMarkData.criteria, ...value };
        }

        // Update the state
        this._markDataByPart = {
            ...this._markDataByPart,
            [partId]: {
                ...this._markDataByPart[partId],
                [studentId]: updatedMarkData
            }
        };

        // Set this part as having unsaved changes
        this._hasChangesByPart = { ...this._hasChangesByPart, [partId]: true };

        this.notifyStateChange();
    }

    // Handle criterion change for a specific student
    public handleCriterionChange(partId: number, studentId: number, criterionName: string, value: CriterionState | null): void
    {
        // Mark this criterion as manually modified
        if (!this._manuallyModifiedCriteriaByPart[partId])
        {
            this._manuallyModifiedCriteriaByPart[partId] = {};
        }

        if (!this._manuallyModifiedCriteriaByPart[partId][studentId])
        {
            this._manuallyModifiedCriteriaByPart[partId][studentId] = {};
        }

        this._manuallyModifiedCriteriaByPart = {
            ...this._manuallyModifiedCriteriaByPart,
            [partId]: {
                ...this._manuallyModifiedCriteriaByPart[partId],
                [studentId]: {
                    ...this._manuallyModifiedCriteriaByPart[partId][studentId],
                    [criterionName]: true
                }
            }
        };

        // Update the criterion value in marks
        if (!this._markDataByPart[partId])
        {
            this._markDataByPart[partId] = {};
        }

        if (!this._markDataByPart[partId][studentId])
        {
            this._markDataByPart[partId][studentId] = new MarkData();
        }

        if (!this._markDataByPart[partId][studentId].criteria)
        {
            this._markDataByPart[partId][studentId].criteria = {};
        }

        // Create updated mark data
        const updatedMarkData = {
            ...this._markDataByPart[partId][studentId],
            criteria: {
                ...this._markDataByPart[partId][studentId].criteria,
                [criterionName]: value
            }
        };

        // Update the state
        this._markDataByPart = {
            ...this._markDataByPart,
            [partId]: {
                ...this._markDataByPart[partId],
                [studentId]: updatedMarkData
            }
        };

        // Set this part as having unsaved changes
        this._hasChangesByPart = { ...this._hasChangesByPart, [partId]: true };

        this.notifyStateChange();
    }

    // Check if a criterion is "locked"
    public isCriterionLocked(partId: number, studentId: number, criterionName: string): boolean
    {
        // Check if the criterion was manually modified
        if (this._manuallyModifiedCriteriaByPart[partId]?.[studentId]?.[criterionName])
        {
            return true;
        }

        // Check if the criterion had an initial non-null value
        const initialValue = this._markDataByPart[partId]?.[studentId]?.criteria?.[criterionName];
        return initialValue !== null && initialValue !== undefined;
    }

    // Handle criterion header click - cycles all values for that criterion
    public handleCriterionHeaderClick(partId: number, criterionName: string): void
    {
        const partGuiConfig = this._partGuis.find(gui => gui.partId === partId);

        // Don't do anything if criteria are not editable
        if (!partGuiConfig || partGuiConfig.unitCriteria !== "EDITABLE")
        {
            return;
        }

        // Get student data
        const partMarks = this._markDataByPart[partId] || {};
        const newPartMarks = { ...partMarks };

        // For each student, cycle the criterion to the next state if not locked
        this._students.forEach(student =>
        {
            // Skip if this criterion is locked for this student
            if (this.isCriterionLocked(partId, student.id, criterionName))
            {
                return;
            }

            // Get current value and calculate next
            const studentMarks = newPartMarks[student.id] || new MarkData();
            const currentValue = studentMarks.criteria?.[criterionName] || null;
            const nextValue = CriterionState.getNextState(currentValue);

            // Update student marks
            newPartMarks[student.id] = {
                ...studentMarks,
                criteria: {
                    ...(studentMarks.criteria || {}),
                    [criterionName]: nextValue
                }
            };
        });

        // Update mark data
        this._markDataByPart = {
            ...this._markDataByPart,
            [partId]: newPartMarks
        };

        // Set this part as having unsaved changes
        this._hasChangesByPart = { ...this._hasChangesByPart, [partId]: true };

        this.notifyStateChange();
    }

    // Handle sort request
    public handleSortRequest(partId: number, field: keyof Student | 'score' | 'grade'): void
    {
        const currentConfig = this._sortConfigByPart[partId] || { sortBy: null, sortDirection: 'asc' };

        const newConfig: SortConfig = {
            sortBy: field,
            sortDirection: currentConfig.sortBy === field && currentConfig.sortDirection === 'asc' ? 'desc' : 'asc'
        };

        this._sortConfigByPart = {
            ...this._sortConfigByPart,
            [partId]: newConfig
        };

        this.notifyStateChange();
    }

    // Save mark data for a part
    public async saveMarkData(partId: number): Promise<{ success: boolean; message: string }>
    {
        if (!this._hasChangesByPart[partId])
        {
            return { success: true, message: 'No changes to save' };
        }

        // Update save status
        this._saveStatusByPart = {
            ...this._saveStatusByPart,
            [partId]: 'saving'
        };
        this.notifyStateChange();

        try
        {
            // Create a mapping with string keys for API
            const stringKeyedMarks: { [studentId: string]: MarkData } = {};
            Object.entries(this._markDataByPart[partId] || {}).forEach(([studentId, markData]) =>
            {
                stringKeyedMarks[studentId] = markData;
            });

            // Call API to save
            const result = await this.api.saveMarkData(partId, stringKeyedMarks);

            if (result.success)
            {
                // Update save status to saved
                this._saveStatusByPart = {
                    ...this._saveStatusByPart,
                    [partId]: 'saved'
                };

                // Clear changes flag
                this._hasChangesByPart = {
                    ...this._hasChangesByPart,
                    [partId]: false
                };

                // Show success snackbar
                this._snackbar = {
                    open: true,
                    message: result.message || 'Mark data successfully saved!',
                    severity: 'success'
                };

                // Reset save status after delay
                setTimeout(() =>
                {
                    this._saveStatusByPart = {
                        ...this._saveStatusByPart,
                        [partId]: 'idle'
                    };
                    this.notifyStateChange();
                }, 3000);
            } else
            {
                throw new Error(result.message || 'Failed to save mark data');
            }

            this.notifyStateChange();
            return result;
        } catch (error)
        {
            console.error('Error saving mark data:', error);

            // Update save status to error
            this._saveStatusByPart = {
                ...this._saveStatusByPart,
                [partId]: 'error'
            };

            // Show error snackbar
            this._snackbar = {
                open: true,
                message: error instanceof Error ? error.message : 'Failed to save mark data',
                severity: 'error'
            };

            this.notifyStateChange();
            return {
                success: false,
                message: error instanceof Error ? error.message : 'Failed to save mark data'
            };
        }
    }
    // Show rule details dialog
    public async showRuleDetails(): Promise<void>
    {
        // Get the current selected part for the active category
        const currentCategory = this._selectedPartType || "";
        const currentPart = this._hierarchySelections[currentCategory]; if (!currentPart || !currentPart.calculationRulePreset)
        {
            this._snackbar = {
                open: true,
                message: 'No calculation rules available for this part',
                severity: 'info'
            };
            this.notifyStateChange();
            return;
        }

        this._rulePresetDialogOpen = true;
        this._ruleLoading = true;
        this.notifyStateChange();

        try
        {
            const details = await this.api.fetchRuleDetails(currentPart.calculationRulePreset);
            this._ruleDetails = details;
        } catch (error)
        {
            console.error("Error fetching rule details:", error);
            this._snackbar = {
                open: true,
                message: 'Failed to load calculation rules',
                severity: 'error'
            };
        } finally
        {
            this._ruleLoading = false;
            this.notifyStateChange();
        }
    }

    // Close rule details dialog
    public closeRuleDetails(): void
    {
        this._rulePresetDialogOpen = false;
        this.notifyStateChange();
    }

    // Close snackbar
    public closeSnackbar(): void
    {
        this._snackbar = {
            ...this._snackbar,
            open: false
        };
        this.notifyStateChange();
    }

    // Getters for state properties
    get tracker(): Tracker | null { return this._tracker; }
    get parts(): TrackerPart[] { return this._parts; }
    get partGuis(): PartGui[] { return this._partGuis; }
    get students(): Student[] { return this._students; }
    get partsById(): { [id: number]: TrackerPart } { return this._partsById; }
    get partsHierarchy(): { [category: string]: FilterHierarchy } { return this._partsHierarchy; }
    get markDataByPart(): { [partId: number]: { [studentId: number]: MarkData } } { return this._markDataByPart; }
    get markScreenLoading(): { [partId: number]: boolean } { return this._markScreenLoading; }
    get loading(): boolean { return this._loading; }
    get error(): string | null { return this._error; }
    get selectedPartType(): string | null { return this._selectedPartType; }
    get hierarchySelections(): { [category: string]: TrackerPart | null } { return this._hierarchySelections; }
    get visiblePartIds(): Set<number> { return this._visiblePartIds; }
    get activeTabIndex(): number { return this._activeTabIndex; }
    get showSummaryView(): boolean { return this._showSummaryView; }
    get summaryParts(): Part[] { return this._summaryParts; }
    get summaryPartMarks(): { [partId: number]: { [studentId: number]: MarkData } } { return this._summaryPartMarks; }
    get summaryViewLoading(): boolean { return this._summaryViewLoading; }
    get rulePresetDialogOpen(): boolean { return this._rulePresetDialogOpen; }
    get ruleDetails(): RuleDetail | null { return this._ruleDetails; }
    get ruleLoading(): boolean { return this._ruleLoading; }
    get hasChangesByPart(): { [partId: number]: boolean } { return this._hasChangesByPart; }
    get manuallyModifiedCriteriaByPart(): { [partId: number]: { [studentId: number]: { [criterionName: string]: boolean } } } { return this._manuallyModifiedCriteriaByPart; }
    get sortConfigByPart(): { [partId: number]: SortConfig } { return this._sortConfigByPart; }
    get saveStatusByPart(): { [partId: number]: 'idle' | 'saving' | 'saved' | 'error' } { return this._saveStatusByPart; }
    get snackbar(): SnackbarState { return this._snackbar; }

    // Setters for specific properties that need custom handling
    set manuallyModifiedCriteriaByPart(value: { [partId: number]: { [studentId: number]: { [criterionName: string]: boolean } } })
    {
        this._manuallyModifiedCriteriaByPart = value;
        this.notifyStateChange();
    }

    set sortConfigByPart(value: { [partId: number]: SortConfig })
    {
        this._sortConfigByPart = value;
        this.notifyStateChange();
    }

    set saveStatusByPart(value: { [partId: number]: 'idle' | 'saving' | 'saved' | 'error' })
    {
        this._saveStatusByPart = value;
        this.notifyStateChange();
    }

    set snackbar(value: SnackbarState)
    {
        this._snackbar = value;
        this.notifyStateChange();
    }    /**
     * Sets the selected part type based on the category with the lowest level value.
     * This ensures the tabs are ordered correctly based on hierarchy.
     */
    private setSelectedPartTypeByLowestLevel(): void
    {
        // Create a mapping of level to categories
        this._levelToCategory = {};

        // Populate the mapping using the hierarchy information
        Object.entries(this._partsHierarchy).forEach(([category, hierarchyInfo]) => {
            const level = hierarchyInfo.level;
            if (!this._levelToCategory[level]) {
                this._levelToCategory[level] = [];
            }
            this._levelToCategory[level].push(category);
        });

        // Find the lowest level with categories
        const levels = Object.keys(this._levelToCategory)
            .map(Number)
            .sort((a, b) => a - b);

        // If we have any levels, select the first category in the lowest level
        if (levels.length > 0) {
            const lowestLevel = levels[0];
            const categoriesAtLowestLevel = this._levelToCategory[lowestLevel];

            if (categoriesAtLowestLevel.length > 0) {
                this._selectedPartType = categoriesAtLowestLevel[0];
            }
        } else {
            // Fallback: Use the first category from parts if no hierarchy levels are available
            const categories = [...new Set(this._parts.map(part => part.category))];
            if (categories.length > 0) {
                this._selectedPartType = categories[0];
            }
        }

        console.log(`Selected part type: ${this._selectedPartType} based on lowest hierarchy level`);
    }

    // Reload summary view and all related parent parts
    public async reloadSummaryViewAndParents(category: string): Promise<void>
    {
        console.log(`Reloading summary view for category ${category} and related parents`);

        this._summaryViewLoading = true;
        this.notifyStateChange();

        try
        {
            // First reload the summary view to get fresh part lists
            await this.loadSummaryView(category);

            // Then get all unique parent part IDs from all parts in the summary
            const allParentIds = new Set<number>();

            // Get all parts that were loaded in the summary
            this._summaryParts.forEach(part => {
                const parentIds = this.getParentPartIds(part.id);
                parentIds.forEach(id => allParentIds.add(id));
            });

            // If we have parent parts to reload, do so in parallel
            if (allParentIds.size > 0) {
                console.log(`Reloading parent parts: ${Array.from(allParentIds).join(', ')}`);

                await Promise.all(Array.from(allParentIds).map(async (parentId) => {
                    try {
                        const marksData = await this.api.fetchMarkData(parentId);
                        this._markDataByPart = { ...this._markDataByPart, [parentId]: marksData };
                        this._hasChangesByPart = { ...this._hasChangesByPart, [parentId]: false };
                    } catch (error) {
                        console.error(`Error reloading parent part ${parentId}:`, error);
                        // Don't throw here, continue with other parts
                    }
                }));
            }

            console.log('Successfully reloaded summary view and parent data');
        } catch (error)
        {
            console.error("Error reloading summary view and parents:", error);
            this._error = "Failed to reload summary view and parent data. Please try again.";
        } finally
        {
            this._summaryViewLoading = false;
            this.notifyStateChange();
        }
    }

    // Getter methods for grade schemes and grades
    get gradeSchemes(): { [schemeRef: string]: GradeSchemeDto } { return this._gradeSchemes; }
    get grades(): { [schemeRef: string]: GradeDto[] } { return this._grades; }

    // Helper method to get grades for a specific part
    public getGradesForPart(partId: number): GradeDto[] {
        const part = this._partsById[partId];
        if (!part || !part.gradeScheme) {
            return [];
        }
        return this._grades[part.gradeScheme] || [];
    }

    // Helper method to get grade scheme for a specific part
    public getGradeSchemeForPart(partId: number): GradeSchemeDto | null {
        const part = this._partsById[partId];
        if (!part || !part.gradeScheme) {
            return null;
        }
        return this._gradeSchemes[part.gradeScheme] || null;
    }

    // Helper method to get grade display name by reference
    public getGradeDisplayName(gradeRef: string, partId: number): string {
        const grades = this.getGradesForPart(partId);
        const grade = grades.find(g => g.Ref === gradeRef);
        return grade ? grade.Ref : gradeRef; // Return the reference if display name not found
    }

    // Helper method to get all grades for a tracker part (including default unset option)
    public getGradeOptionsForPart(partId: number): Array<{ value: string; label: string }> {
        const grades = this.getGradesForPart(partId);
        const options = grades.map(grade => ({
            value: grade.Ref,
            label: grade.Ref
        }));
        
        // Add empty option at the beginning
        return [{ value: "", label: "Select..." }, ...options];
    }
}
