'use client' import React, { createContext, useReducer, useEffect, useCallback, useMemo, useRef } from 'react' import { useRouter, usePathname } from 'next/navigation' import { SDKState, CheckpointStatus, UseCaseAssessment, Risk, Control, CustomerType, CompanyProfile, ImportedDocument, GapAnalysis, SDKPackageId, ProjectInfo, getStepById, getStepByUrl, getNextStep, getPreviousStep, getCompletionPercentage, getPhaseCompletionPercentage, getPackageCompletionPercentage, } from './types' import { exportToPDF, exportToZIP } from './export' import { SDKApiClient, getSDKApiClient } from './api-client' import { StateSyncManager, SyncState } from './sync' import { generateDemoState } from './demo-data' import { SDKContextValue, initialState, SDK_STORAGE_KEY } from './context-types' import { sdkReducer } from './context-reducer' import { validateCheckpointLocally } from './context-validators' import { ensureApiClient, createProjectApi, listProjectsApi, archiveProjectApi, restoreProjectApi, permanentlyDeleteProjectApi, } from './context-projects' import { buildSyncCallbacks, loadInitialState, initSyncInfra, cleanupSyncInfra, } from './context-sync-helpers' export const SDKContext = createContext(null) interface SDKProviderProps { children: React.ReactNode tenantId?: string userId?: string projectId?: string enableBackendSync?: boolean } export function SDKProvider({ children, tenantId = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e', userId = 'default', projectId, enableBackendSync = false, }: SDKProviderProps) { const router = useRouter() const pathname = usePathname() const [state, dispatch] = useReducer(sdkReducer, { ...initialState, tenantId, userId, projectId: projectId || '', }) const [isCommandBarOpen, setCommandBarOpen] = React.useState(false) const [isInitialized, setIsInitialized] = React.useState(false) const [syncState, setSyncState] = React.useState({ status: 'idle', lastSyncedAt: null, localVersion: 0, serverVersion: 0, pendingChanges: 0, error: null, }) const [isOnline, setIsOnline] = React.useState(true) // Refs for sync manager and API client const apiClientRef = useRef(null) const syncManagerRef = useRef(null) const stateRef = useRef(state) stateRef.current = state // Initialize API client and sync manager useEffect(() => { const callbacks = buildSyncCallbacks(setSyncState, setIsOnline, dispatch, stateRef) initSyncInfra(enableBackendSync, tenantId, projectId, apiClientRef, syncManagerRef, callbacks) return () => cleanupSyncInfra(enableBackendSync, syncManagerRef, apiClientRef) }, [enableBackendSync, tenantId, projectId]) // Sync current step with URL useEffect(() => { if (pathname) { const step = getStepByUrl(pathname) if (step && step.id !== state.currentStep) { dispatch({ type: 'SET_CURRENT_STEP', payload: step.id }) } } }, [pathname, state.currentStep]) // Storage key — per tenant+project const storageKey = projectId ? `${SDK_STORAGE_KEY}-${tenantId}-${projectId}` : `${SDK_STORAGE_KEY}-${tenantId}` // Load state on mount (localStorage first, then server) useEffect(() => { loadInitialState({ storageKey, enableBackendSync, projectId, syncManager: syncManagerRef.current, apiClient: apiClientRef.current, dispatch, }) .catch(error => console.error('Failed to load SDK state:', error)) .finally(() => setIsInitialized(true)) }, [tenantId, projectId, enableBackendSync, storageKey]) // Auto-save to localStorage and sync to server useEffect(() => { if (!isInitialized || !state.preferences.autoSave) return const saveTimeout = setTimeout(() => { try { // Save to localStorage (per tenant+project) localStorage.setItem(storageKey, JSON.stringify(state)) // Sync to server if backend sync is enabled if (enableBackendSync && syncManagerRef.current) { syncManagerRef.current.queueSync(state) } } catch (error) { console.error('Failed to save SDK state:', error) } }, 1000) return () => clearTimeout(saveTimeout) }, [state, tenantId, projectId, isInitialized, enableBackendSync, storageKey]) // Keyboard shortcut for Command Bar useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault() setCommandBarOpen(prev => !prev) } if (e.key === 'Escape' && isCommandBarOpen) { setCommandBarOpen(false) } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [isCommandBarOpen]) // Navigation const currentStep = useMemo(() => getStepById(state.currentStep), [state.currentStep]) const goToStep = useCallback( (stepId: string) => { const step = getStepById(stepId) if (step) { dispatch({ type: 'SET_CURRENT_STEP', payload: stepId }) const url = projectId ? `${step.url}?project=${projectId}` : step.url router.push(url) } }, [router, projectId] ) const goToNextStep = useCallback(() => { const nextStep = getNextStep(state.currentStep, state) if (nextStep) { goToStep(nextStep.id) } }, [state, goToStep]) const goToPreviousStep = useCallback(() => { const prevStep = getPreviousStep(state.currentStep, state) if (prevStep) { goToStep(prevStep.id) } }, [state, goToStep]) const canGoNext = useMemo(() => { return getNextStep(state.currentStep, state) !== undefined }, [state]) const canGoPrevious = useMemo(() => { return getPreviousStep(state.currentStep, state) !== undefined }, [state]) // Progress const completionPercentage = useMemo(() => getCompletionPercentage(state), [state]) const phase1Completion = useMemo(() => getPhaseCompletionPercentage(state, 1), [state]) const phase2Completion = useMemo(() => getPhaseCompletionPercentage(state, 2), [state]) // Package Completion const packageCompletion = useMemo(() => { const completion: Record = { 'vorbereitung': getPackageCompletionPercentage(state, 'vorbereitung'), 'analyse': getPackageCompletionPercentage(state, 'analyse'), 'dokumentation': getPackageCompletionPercentage(state, 'dokumentation'), 'rechtliche-texte': getPackageCompletionPercentage(state, 'rechtliche-texte'), 'betrieb': getPackageCompletionPercentage(state, 'betrieb'), } return completion }, [state]) // Simple dispatch callbacks const setCustomerType = useCallback((type: CustomerType) => dispatch({ type: 'SET_CUSTOMER_TYPE', payload: type }), []) const setCompanyProfile = useCallback((profile: CompanyProfile) => dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile }), []) const updateCompanyProfile = useCallback((updates: Partial) => dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: updates }), []) const setComplianceScope = useCallback((scope: import('./compliance-scope-types').ComplianceScopeState) => dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: scope }), []) const updateComplianceScope = useCallback((updates: Partial) => dispatch({ type: 'UPDATE_COMPLIANCE_SCOPE', payload: updates }), []) const addImportedDocument = useCallback((doc: ImportedDocument) => dispatch({ type: 'ADD_IMPORTED_DOCUMENT', payload: doc }), []) const setGapAnalysis = useCallback((analysis: GapAnalysis) => dispatch({ type: 'SET_GAP_ANALYSIS', payload: analysis }), []) // Checkpoints const validateCheckpoint = useCallback( async (checkpointId: string): Promise => { // Try backend validation if available if (enableBackendSync && apiClientRef.current) { try { const result = await apiClientRef.current.validateCheckpoint(checkpointId, state) const status: CheckpointStatus = { checkpointId: result.checkpointId, passed: result.passed, validatedAt: new Date(result.validatedAt), validatedBy: result.validatedBy, errors: result.errors, warnings: result.warnings, } dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } }) return status } catch { // Fall back to local validation } } // Local validation const status = validateCheckpointLocally(checkpointId, state) dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } }) return status }, [state, enableBackendSync] ) const overrideCheckpoint = useCallback(async (checkpointId: string, reason: string): Promise => { const existing = state.checkpoints[checkpointId] const overridden: CheckpointStatus = { ...existing, checkpointId, passed: true, overrideReason: reason, overriddenBy: state.userId, overriddenAt: new Date(), errors: [], warnings: existing?.warnings || [], } dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status: overridden } }) }, [state.checkpoints, state.userId]) const getCheckpointStatus = useCallback( (checkpointId: string) => state.checkpoints[checkpointId], [state.checkpoints] ) const updateUseCase = useCallback((id: string, data: Partial) => dispatch({ type: 'UPDATE_USE_CASE', payload: { id, data } }), []) const addRisk = useCallback((risk: Risk) => dispatch({ type: 'ADD_RISK', payload: risk }), []) const updateControl = useCallback((id: string, data: Partial) => dispatch({ type: 'UPDATE_CONTROL', payload: { id, data } }), []) const loadDemoData = useCallback((demoState: Partial) => dispatch({ type: 'LOAD_DEMO_DATA', payload: demoState }), []) // Seed demo data via API (stores like real customer data) const seedDemoData = useCallback(async (): Promise<{ success: boolean; message: string }> => { try { // Generate demo state const demoState = generateDemoState(tenantId, userId) as SDKState // Save via API (same path as real customer data) if (enableBackendSync && apiClientRef.current) { await apiClientRef.current.saveState(demoState) } // Also save to localStorage for immediate availability localStorage.setItem(storageKey, JSON.stringify(demoState)) // Update local state dispatch({ type: 'LOAD_DEMO_DATA', payload: demoState }) return { success: true, message: `Demo-Daten erfolgreich geladen für Tenant ${tenantId}` } } catch (error) { console.error('Failed to seed demo data:', error) return { success: false, message: error instanceof Error ? error.message : 'Unbekannter Fehler beim Laden der Demo-Daten', } } }, [tenantId, userId, enableBackendSync, storageKey]) // Clear demo data const clearDemoData = useCallback(async (): Promise => { try { // Delete from API if (enableBackendSync && apiClientRef.current) { await apiClientRef.current.deleteState() } // Clear localStorage localStorage.removeItem(storageKey) // Reset local state dispatch({ type: 'RESET_STATE' }) return true } catch (error) { console.error('Failed to clear demo data:', error) return false } }, [storageKey, enableBackendSync]) // Check if demo data is loaded (has use cases with demo- prefix) const isDemoDataLoaded = useMemo(() => { return state.useCases.length > 0 && state.useCases.some(uc => uc.id.startsWith('demo-')) }, [state.useCases]) // Persistence const saveState = useCallback(async (): Promise => { try { localStorage.setItem(storageKey, JSON.stringify(state)) if (enableBackendSync && syncManagerRef.current) { await syncManagerRef.current.forcSync(state) } } catch (error) { console.error('Failed to save SDK state:', error) throw error } }, [state, storageKey, enableBackendSync]) const loadState = useCallback(async (): Promise => { try { if (enableBackendSync && syncManagerRef.current) { const serverState = await syncManagerRef.current.loadFromServer() if (serverState) { dispatch({ type: 'SET_STATE', payload: serverState }) return } } // Fall back to localStorage const stored = localStorage.getItem(storageKey) if (stored) { const parsed = JSON.parse(stored) dispatch({ type: 'SET_STATE', payload: parsed }) } } catch (error) { console.error('Failed to load SDK state:', error) throw error } }, [storageKey, enableBackendSync]) // Force sync to server const forceSyncToServer = useCallback(async (): Promise => { if (enableBackendSync && syncManagerRef.current) { await syncManagerRef.current.forcSync(state) } }, [state, enableBackendSync]) // Project Management const createProject = useCallback( async (name: string, customerType: CustomerType, copyFromProjectId?: string): Promise => { const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId) return createProjectApi(client, name, customerType, copyFromProjectId) }, [enableBackendSync, tenantId, projectId] ) const listProjectsFn = useCallback(async (): Promise => { if (!apiClientRef.current && enableBackendSync) { apiClientRef.current = getSDKApiClient(tenantId, projectId) } if (!apiClientRef.current) { return [] } return listProjectsApi(apiClientRef.current) }, [enableBackendSync, tenantId, projectId]) const switchProject = useCallback( (newProjectId: string) => { const params = new URLSearchParams(window.location.search) params.set('project', newProjectId) router.push(`/sdk?${params.toString()}`) }, [router] ) const archiveProjectFn = useCallback( async (archiveId: string): Promise => { const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId) await archiveProjectApi(client, archiveId) }, [enableBackendSync, tenantId, projectId] ) const restoreProjectFn = useCallback( async (restoreId: string): Promise => { const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId) return restoreProjectApi(client, restoreId) }, [enableBackendSync, tenantId, projectId] ) const permanentlyDeleteProjectFn = useCallback( async (deleteId: string): Promise => { const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId) await permanentlyDeleteProjectApi(client, deleteId) }, [enableBackendSync, tenantId, projectId] ) // Export const exportState = useCallback( async (format: 'json' | 'pdf' | 'zip'): Promise => { switch (format) { case 'json': return new Blob([JSON.stringify(state, null, 2)], { type: 'application/json', }) case 'pdf': return exportToPDF(state) case 'zip': return exportToZIP(state) default: throw new Error(`Unknown export format: ${format}`) } }, [state] ) const value: SDKContextValue = { state, dispatch, currentStep, goToStep, goToNextStep, goToPreviousStep, canGoNext, canGoPrevious, completionPercentage, phase1Completion, phase2Completion, packageCompletion, setCustomerType, setCompanyProfile, updateCompanyProfile, setComplianceScope, updateComplianceScope, addImportedDocument, setGapAnalysis, validateCheckpoint, overrideCheckpoint, getCheckpointStatus, updateUseCase, addRisk, updateControl, saveState, loadState, loadDemoData, seedDemoData, clearDemoData, isDemoDataLoaded, syncState, forceSyncToServer, isOnline, exportState, isCommandBarOpen, setCommandBarOpen, projectId, createProject, listProjects: listProjectsFn, switchProject, archiveProject: archiveProjectFn, restoreProject: restoreProjectFn, permanentlyDeleteProject: permanentlyDeleteProjectFn, } return {children} }