import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { BackendStatus } from '../../app/app-state'; import { AssessorApiClient } from '../../api/assessor-api-client'; import type { RegulatoryMappingCandidate, RegulatoryMappingDecisionValue, RegulatoryMappingResponse } from '../../domain/regulatory-mapping'; const assessorApi = new AssessorApiClient(); export function useRegulatoryMapping( projectId: string, occurrenceId: string | undefined, backendStatus: BackendStatus ) { const [open, setOpen] = useState(false); const [storedResponse, setStoredResponse] = useState<{ scope: { key: string }; value: RegulatoryMappingResponse }>(); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const resultProjectionId = occurrenceId ? `scanner-finding:${occurrenceId}` : ''; const scopeKey = `${projectId}\u0000${resultProjectionId}\u0000${backendStatus}`; const activeScope = useRef({ key: scopeKey }); if (activeScope.current.key !== scopeKey) activeScope.current = { key: scopeKey }; const requestScope = activeScope.current; const response = storedResponse?.scope === requestScope ? storedResponse.value : undefined; useEffect(() => { setOpen(false); setStoredResponse(undefined); setLoading(false); setError(''); }, [scopeKey]); const load = useCallback(async () => { if (!projectId || !resultProjectionId || backendStatus !== 'connected' || activeScope.current !== requestScope) return; setLoading(true); try { const next = await assessorApi.getRegulatoryMappings(projectId, resultProjectionId); if (activeScope.current !== requestScope) return; setStoredResponse({ scope: requestScope, value: next }); setError(''); } catch (reason) { if (activeScope.current !== requestScope) return; setError(reason instanceof Error ? reason.message : 'Regulatory mapping could not be loaded.'); } finally { if (activeScope.current === requestScope) setLoading(false); } }, [backendStatus, projectId, requestScope, resultProjectionId]); useEffect(() => { if (open && !response && !loading && !error) void load(); }, [error, load, loading, open, response]); useEffect(() => { if (!open || !response || !['queued', 'running'].includes(response.state)) return; const timeout = window.setTimeout(() => void load(), response.pollAfterMs ?? 1_500); return () => window.clearTimeout(timeout); }, [load, open, response]); const start = useCallback(async () => { if (!projectId || !resultProjectionId || activeScope.current !== requestScope) return; setLoading(true); try { const next = await assessorApi.generateRegulatoryMappings(projectId, { resultProjectionId }); if (activeScope.current !== requestScope) return; setStoredResponse({ scope: requestScope, value: next }); setError(''); } catch (reason) { if (activeScope.current !== requestScope) return; setError(reason instanceof Error ? reason.message : 'Regulatory mapping could not be started.'); } finally { if (activeScope.current === requestScope) setLoading(false); } }, [projectId, requestScope, resultProjectionId]); const decide = useCallback(async (candidate: RegulatoryMappingCandidate, decision: RegulatoryMappingDecisionValue, comment?: string) => { if (!projectId || !resultProjectionId || !response?.run || activeScope.current !== requestScope) return; const latest = response.decisions.find(item => item.controlId === candidate.controlId); setLoading(true); try { const next = await assessorApi.saveRegulatoryMappingDecision(projectId, { resultProjectionId, controlId: candidate.controlId, decision, ...(comment?.trim() ? { comment: comment.trim() } : {}), ...(latest ? { expectedLatestDecisionId: latest.id } : {}) }); if (activeScope.current !== requestScope) return; setStoredResponse({ scope: requestScope, value: next }); setError(''); } catch (reason) { if (activeScope.current !== requestScope) return; setError(reason instanceof Error ? reason.message : 'The mapping decision could not be saved.'); } finally { if (activeScope.current === requestScope) setLoading(false); } }, [projectId, requestScope, response, resultProjectionId]); const acceptedCount = useMemo(() => { const latest = new Map(); response?.decisions.forEach(item => { if (!latest.has(item.controlId)) latest.set(item.controlId, item.decision); }); return [...latest.values()].filter(value => value === 'accepted').length; }, [response]); const localDisabledReason = !occurrenceId ? 'This record has no canonical finding occurrence.' : backendStatus !== 'connected' ? 'The backend is not connected.' : response && !response.generationEnabled ? response.disabledReason ?? 'Regulatory mapping is unavailable.' : undefined; return { open, setOpen, response, loading, error, start, decide, acceptedCount, disabledReason: localDisabledReason }; }