'use client' // ============================================================================= // TOM Generator Context // State management for the TOM Generator Wizard // ============================================================================= import React, { createContext, useContext, useReducer, useCallback, useEffect, useRef, ReactNode, } from 'react' import { TOMGeneratorState, TOMGeneratorStepId, CompanyProfile, DataProfile, ArchitectureProfile, SecurityProfile, RiskProfile, EvidenceDocument, DerivedTOM, GapAnalysisResult, ExportRecord, WizardStep, createInitialTOMGeneratorState, TOM_GENERATOR_STEPS, getStepIndex, calculateProtectionLevel, isDSFARequired, hasSpecialCategories, } from './types' import { TOMRulesEngine } from './rules-engine' // ============================================================================= // ACTION TYPES // ============================================================================= type TOMGeneratorAction = | { type: 'INITIALIZE'; payload: { tenantId: string; state?: TOMGeneratorState } } | { type: 'RESET'; payload: { tenantId: string } } | { type: 'SET_CURRENT_STEP'; payload: TOMGeneratorStepId } | { type: 'SET_COMPANY_PROFILE'; payload: CompanyProfile } | { type: 'UPDATE_COMPANY_PROFILE'; payload: Partial } | { type: 'SET_DATA_PROFILE'; payload: DataProfile } | { type: 'UPDATE_DATA_PROFILE'; payload: Partial } | { type: 'SET_ARCHITECTURE_PROFILE'; payload: ArchitectureProfile } | { type: 'UPDATE_ARCHITECTURE_PROFILE'; payload: Partial } | { type: 'SET_SECURITY_PROFILE'; payload: SecurityProfile } | { type: 'UPDATE_SECURITY_PROFILE'; payload: Partial } | { type: 'SET_RISK_PROFILE'; payload: RiskProfile } | { type: 'UPDATE_RISK_PROFILE'; payload: Partial } | { type: 'COMPLETE_STEP'; payload: { stepId: TOMGeneratorStepId; data: unknown } } | { type: 'UNCOMPLETE_STEP'; payload: TOMGeneratorStepId } | { type: 'ADD_EVIDENCE'; payload: EvidenceDocument } | { type: 'UPDATE_EVIDENCE'; payload: { id: string; data: Partial } } | { type: 'DELETE_EVIDENCE'; payload: string } | { type: 'SET_DERIVED_TOMS'; payload: DerivedTOM[] } | { type: 'UPDATE_DERIVED_TOM'; payload: { id: string; data: Partial } } | { type: 'SET_GAP_ANALYSIS'; payload: GapAnalysisResult } | { type: 'ADD_EXPORT'; payload: ExportRecord } | { type: 'BULK_UPDATE_TOMS'; payload: { updates: Array<{ id: string; data: Partial }> } } | { type: 'LOAD_STATE'; payload: TOMGeneratorState } // ============================================================================= // REDUCER // ============================================================================= function tomGeneratorReducer( state: TOMGeneratorState, action: TOMGeneratorAction ): TOMGeneratorState { const updateState = (updates: Partial): TOMGeneratorState => ({ ...state, ...updates, updatedAt: new Date(), }) switch (action.type) { case 'INITIALIZE': { if (action.payload.state) { return action.payload.state } return createInitialTOMGeneratorState(action.payload.tenantId) } case 'RESET': { return createInitialTOMGeneratorState(action.payload.tenantId) } case 'SET_CURRENT_STEP': { return updateState({ currentStep: action.payload }) } case 'SET_COMPANY_PROFILE': { return updateState({ companyProfile: action.payload }) } case 'UPDATE_COMPANY_PROFILE': { if (!state.companyProfile) return state return updateState({ companyProfile: { ...state.companyProfile, ...action.payload }, }) } case 'SET_DATA_PROFILE': { // Automatically set hasSpecialCategories based on categories const profile: DataProfile = { ...action.payload, hasSpecialCategories: hasSpecialCategories(action.payload.categories), } return updateState({ dataProfile: profile }) } case 'UPDATE_DATA_PROFILE': { if (!state.dataProfile) return state const updatedProfile = { ...state.dataProfile, ...action.payload } // Recalculate hasSpecialCategories if categories changed if (action.payload.categories) { updatedProfile.hasSpecialCategories = hasSpecialCategories( action.payload.categories ) } return updateState({ dataProfile: updatedProfile }) } case 'SET_ARCHITECTURE_PROFILE': { return updateState({ architectureProfile: action.payload }) } case 'UPDATE_ARCHITECTURE_PROFILE': { if (!state.architectureProfile) return state return updateState({ architectureProfile: { ...state.architectureProfile, ...action.payload }, }) } case 'SET_SECURITY_PROFILE': { return updateState({ securityProfile: action.payload }) } case 'UPDATE_SECURITY_PROFILE': { if (!state.securityProfile) return state return updateState({ securityProfile: { ...state.securityProfile, ...action.payload }, }) } case 'SET_RISK_PROFILE': { // Automatically calculate protection level and DSFA requirement const profile: RiskProfile = { ...action.payload, protectionLevel: calculateProtectionLevel(action.payload.ciaAssessment), dsfaRequired: isDSFARequired(state.dataProfile, action.payload), } return updateState({ riskProfile: profile }) } case 'UPDATE_RISK_PROFILE': { if (!state.riskProfile) return state const updatedProfile = { ...state.riskProfile, ...action.payload } // Recalculate protection level if CIA assessment changed if (action.payload.ciaAssessment) { updatedProfile.protectionLevel = calculateProtectionLevel( action.payload.ciaAssessment ) } // Recalculate DSFA requirement updatedProfile.dsfaRequired = isDSFARequired(state.dataProfile, updatedProfile) return updateState({ riskProfile: updatedProfile }) } case 'COMPLETE_STEP': { const updatedSteps = state.steps.map((step) => step.id === action.payload.stepId ? { ...step, completed: true, data: action.payload.data, validatedAt: new Date(), } : step ) return updateState({ steps: updatedSteps }) } case 'UNCOMPLETE_STEP': { const updatedSteps = state.steps.map((step) => step.id === action.payload ? { ...step, completed: false, validatedAt: null } : step ) return updateState({ steps: updatedSteps }) } case 'ADD_EVIDENCE': { return updateState({ documents: [...state.documents, action.payload], }) } case 'UPDATE_EVIDENCE': { const updatedDocuments = state.documents.map((doc) => doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc ) return updateState({ documents: updatedDocuments }) } case 'DELETE_EVIDENCE': { return updateState({ documents: state.documents.filter((doc) => doc.id !== action.payload), }) } case 'SET_DERIVED_TOMS': { return updateState({ derivedTOMs: action.payload }) } case 'UPDATE_DERIVED_TOM': { const updatedTOMs = state.derivedTOMs.map((tom) => tom.id === action.payload.id ? { ...tom, ...action.payload.data } : tom ) return updateState({ derivedTOMs: updatedTOMs }) } case 'SET_GAP_ANALYSIS': { return updateState({ gapAnalysis: action.payload }) } case 'ADD_EXPORT': { return updateState({ exports: [...state.exports, action.payload], }) } case 'BULK_UPDATE_TOMS': { let updatedTOMs = [...state.derivedTOMs] for (const update of action.payload.updates) { updatedTOMs = updatedTOMs.map((tom) => tom.id === update.id ? { ...tom, ...update.data } : tom ) } return updateState({ derivedTOMs: updatedTOMs }) } case 'LOAD_STATE': { return action.payload } default: return state } } // ============================================================================= // CONTEXT VALUE INTERFACE // ============================================================================= interface TOMGeneratorContextValue { state: TOMGeneratorState dispatch: React.Dispatch // Navigation currentStepIndex: number totalSteps: number canGoNext: boolean canGoPrevious: boolean goToStep: (stepId: TOMGeneratorStepId) => void goToNextStep: () => void goToPreviousStep: () => void completeCurrentStep: (data: unknown) => void // Profile setters setCompanyProfile: (profile: CompanyProfile) => void updateCompanyProfile: (data: Partial) => void setDataProfile: (profile: DataProfile) => void updateDataProfile: (data: Partial) => void setArchitectureProfile: (profile: ArchitectureProfile) => void updateArchitectureProfile: (data: Partial) => void setSecurityProfile: (profile: SecurityProfile) => void updateSecurityProfile: (data: Partial) => void setRiskProfile: (profile: RiskProfile) => void updateRiskProfile: (data: Partial) => void // Evidence management addEvidence: (document: EvidenceDocument) => void updateEvidence: (id: string, data: Partial) => void deleteEvidence: (id: string) => void // TOM derivation deriveTOMs: () => void updateDerivedTOM: (id: string, data: Partial) => void bulkUpdateTOMs: (updates: Array<{ id: string; data: Partial }>) => void // Gap analysis runGapAnalysis: () => void // Export addExport: (record: ExportRecord) => void // Persistence saveState: () => Promise loadState: () => Promise resetState: () => void // Status isStepCompleted: (stepId: TOMGeneratorStepId) => boolean getCompletionPercentage: () => number isLoading: boolean error: string | null } // ============================================================================= // CONTEXT // ============================================================================= const TOMGeneratorContext = createContext(null) // ============================================================================= // STORAGE KEYS // ============================================================================= const STORAGE_KEY_PREFIX = 'tom-generator-state-' function getStorageKey(tenantId: string): string { return `${STORAGE_KEY_PREFIX}${tenantId}` } // ============================================================================= // PROVIDER COMPONENT // ============================================================================= interface TOMGeneratorProviderProps { children: ReactNode tenantId: string initialState?: TOMGeneratorState enablePersistence?: boolean } export function TOMGeneratorProvider({ children, tenantId, initialState, enablePersistence = true, }: TOMGeneratorProviderProps) { const [state, dispatch] = useReducer( tomGeneratorReducer, initialState ?? createInitialTOMGeneratorState(tenantId) ) const [isLoading, setIsLoading] = React.useState(false) const [error, setError] = React.useState(null) const rulesEngineRef = useRef(null) // Initialize rules engine useEffect(() => { if (!rulesEngineRef.current) { rulesEngineRef.current = new TOMRulesEngine() } }, []) // Load state from localStorage on mount useEffect(() => { if (enablePersistence && typeof window !== 'undefined') { try { const stored = localStorage.getItem(getStorageKey(tenantId)) if (stored) { const parsed = JSON.parse(stored) // Convert date strings back to Date objects if (parsed.createdAt) parsed.createdAt = new Date(parsed.createdAt) if (parsed.updatedAt) parsed.updatedAt = new Date(parsed.updatedAt) if (parsed.steps) { parsed.steps = parsed.steps.map((step: WizardStep) => ({ ...step, validatedAt: step.validatedAt ? new Date(step.validatedAt) : null, })) } if (parsed.documents) { parsed.documents = parsed.documents.map((doc: EvidenceDocument) => ({ ...doc, uploadedAt: new Date(doc.uploadedAt), validFrom: doc.validFrom ? new Date(doc.validFrom) : null, validUntil: doc.validUntil ? new Date(doc.validUntil) : null, aiAnalysis: doc.aiAnalysis ? { ...doc.aiAnalysis, analyzedAt: new Date(doc.aiAnalysis.analyzedAt), } : null, })) } if (parsed.derivedTOMs) { parsed.derivedTOMs = parsed.derivedTOMs.map((tom: DerivedTOM) => ({ ...tom, implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null, reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null, })) } if (parsed.gapAnalysis?.generatedAt) { parsed.gapAnalysis.generatedAt = new Date(parsed.gapAnalysis.generatedAt) } if (parsed.exports) { parsed.exports = parsed.exports.map((exp: ExportRecord) => ({ ...exp, generatedAt: new Date(exp.generatedAt), })) } dispatch({ type: 'LOAD_STATE', payload: parsed }) } } catch (e) { console.error('Failed to load TOM Generator state from localStorage:', e) } } }, [tenantId, enablePersistence]) // Save state to localStorage on changes useEffect(() => { if (enablePersistence && typeof window !== 'undefined') { try { localStorage.setItem(getStorageKey(tenantId), JSON.stringify(state)) } catch (e) { console.error('Failed to save TOM Generator state to localStorage:', e) } } }, [state, tenantId, enablePersistence]) // Navigation helpers const currentStepIndex = getStepIndex(state.currentStep) const totalSteps = TOM_GENERATOR_STEPS.length const canGoNext = currentStepIndex < totalSteps - 1 const canGoPrevious = currentStepIndex > 0 const goToStep = useCallback((stepId: TOMGeneratorStepId) => { dispatch({ type: 'SET_CURRENT_STEP', payload: stepId }) }, []) const goToNextStep = useCallback(() => { if (canGoNext) { const nextStep = TOM_GENERATOR_STEPS[currentStepIndex + 1] dispatch({ type: 'SET_CURRENT_STEP', payload: nextStep.id }) } }, [canGoNext, currentStepIndex]) const goToPreviousStep = useCallback(() => { if (canGoPrevious) { const prevStep = TOM_GENERATOR_STEPS[currentStepIndex - 1] dispatch({ type: 'SET_CURRENT_STEP', payload: prevStep.id }) } }, [canGoPrevious, currentStepIndex]) const completeCurrentStep = useCallback( (data: unknown) => { dispatch({ type: 'COMPLETE_STEP', payload: { stepId: state.currentStep, data }, }) }, [state.currentStep] ) // Profile setters const setCompanyProfile = useCallback((profile: CompanyProfile) => { dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile }) }, []) const updateCompanyProfile = useCallback((data: Partial) => { dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: data }) }, []) const setDataProfile = useCallback((profile: DataProfile) => { dispatch({ type: 'SET_DATA_PROFILE', payload: profile }) }, []) const updateDataProfile = useCallback((data: Partial) => { dispatch({ type: 'UPDATE_DATA_PROFILE', payload: data }) }, []) const setArchitectureProfile = useCallback((profile: ArchitectureProfile) => { dispatch({ type: 'SET_ARCHITECTURE_PROFILE', payload: profile }) }, []) const updateArchitectureProfile = useCallback( (data: Partial) => { dispatch({ type: 'UPDATE_ARCHITECTURE_PROFILE', payload: data }) }, [] ) const setSecurityProfile = useCallback((profile: SecurityProfile) => { dispatch({ type: 'SET_SECURITY_PROFILE', payload: profile }) }, []) const updateSecurityProfile = useCallback((data: Partial) => { dispatch({ type: 'UPDATE_SECURITY_PROFILE', payload: data }) }, []) const setRiskProfile = useCallback((profile: RiskProfile) => { dispatch({ type: 'SET_RISK_PROFILE', payload: profile }) }, []) const updateRiskProfile = useCallback((data: Partial) => { dispatch({ type: 'UPDATE_RISK_PROFILE', payload: data }) }, []) // Evidence management const addEvidence = useCallback((document: EvidenceDocument) => { dispatch({ type: 'ADD_EVIDENCE', payload: document }) }, []) const updateEvidence = useCallback( (id: string, data: Partial) => { dispatch({ type: 'UPDATE_EVIDENCE', payload: { id, data } }) }, [] ) const deleteEvidence = useCallback((id: string) => { dispatch({ type: 'DELETE_EVIDENCE', payload: id }) }, []) // TOM derivation const deriveTOMs = useCallback(() => { if (!rulesEngineRef.current) return const derivedTOMs = rulesEngineRef.current.deriveAllTOMs({ companyProfile: state.companyProfile, dataProfile: state.dataProfile, architectureProfile: state.architectureProfile, securityProfile: state.securityProfile, riskProfile: state.riskProfile, }) dispatch({ type: 'SET_DERIVED_TOMS', payload: derivedTOMs }) }, [ state.companyProfile, state.dataProfile, state.architectureProfile, state.securityProfile, state.riskProfile, ]) const updateDerivedTOM = useCallback( (id: string, data: Partial) => { dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } }) }, [] ) const bulkUpdateTOMs = useCallback( (updates: Array<{ id: string; data: Partial }>) => { for (const { id, data } of updates) { dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } }) } }, [] ) // Gap analysis const runGapAnalysis = useCallback(() => { if (!rulesEngineRef.current) return const result = rulesEngineRef.current.performGapAnalysis( state.derivedTOMs, state.documents ) dispatch({ type: 'SET_GAP_ANALYSIS', payload: result }) }, [state.derivedTOMs, state.documents]) // Export const addExport = useCallback((record: ExportRecord) => { dispatch({ type: 'ADD_EXPORT', payload: record }) }, []) // Persistence const saveState = useCallback(async () => { setIsLoading(true) setError(null) try { // API call to save state const response = await fetch('/api/sdk/v1/tom-generator/state', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tenantId, state }), }) if (!response.ok) { throw new Error('Failed to save state') } } catch (e) { setError(e instanceof Error ? e.message : 'Unknown error') throw e } finally { setIsLoading(false) } }, [tenantId, state]) const loadState = useCallback(async () => { setIsLoading(true) setError(null) try { const response = await fetch( `/api/sdk/v1/tom-generator/state?tenantId=${tenantId}` ) if (!response.ok) { throw new Error('Failed to load state') } const data = await response.json() if (data.state) { dispatch({ type: 'LOAD_STATE', payload: data.state }) } } catch (e) { setError(e instanceof Error ? e.message : 'Unknown error') throw e } finally { setIsLoading(false) } }, [tenantId]) const resetState = useCallback(() => { dispatch({ type: 'RESET', payload: { tenantId } }) }, [tenantId]) // Status helpers const isStepCompleted = useCallback( (stepId: TOMGeneratorStepId) => { const step = state.steps.find((s) => s.id === stepId) return step?.completed ?? false }, [state.steps] ) const getCompletionPercentage = useCallback(() => { const completedSteps = state.steps.filter((s) => s.completed).length return Math.round((completedSteps / totalSteps) * 100) }, [state.steps, totalSteps]) const contextValue: TOMGeneratorContextValue = { state, dispatch, currentStepIndex, totalSteps, canGoNext, canGoPrevious, goToStep, goToNextStep, goToPreviousStep, completeCurrentStep, setCompanyProfile, updateCompanyProfile, setDataProfile, updateDataProfile, setArchitectureProfile, updateArchitectureProfile, setSecurityProfile, updateSecurityProfile, setRiskProfile, updateRiskProfile, addEvidence, updateEvidence, deleteEvidence, deriveTOMs, updateDerivedTOM, bulkUpdateTOMs, runGapAnalysis, addExport, saveState, loadState, resetState, isStepCompleted, getCompletionPercentage, isLoading, error, } return ( {children} ) } // ============================================================================= // HOOK // ============================================================================= export function useTOMGenerator(): TOMGeneratorContextValue { const context = useContext(TOMGeneratorContext) if (!context) { throw new Error( 'useTOMGenerator must be used within a TOMGeneratorProvider' ) } return context } // ============================================================================= // EXPORTS // ============================================================================= export { TOMGeneratorContext } export type { TOMGeneratorAction, TOMGeneratorContextValue }