import { useCallback, useEffect, useMemo, useState } from "react"; import type { AppSnapshot } from "../../../types/domain"; import type { StoredScanResultEntry } from "../../../types/storage"; import { BUILD_FIT_PROFILE_CORPUS_VERSION, curatedBuildFitProfiles, } from "../../../data/buildFitProfiles"; import { BUILD_FIT_COMBINATION_ASSESSMENT_VERSION, BUILD_FIT_CONTRACT_VERSION, BUILD_FIT_EVIDENCE_RANKING_VERSION, type BuildFitEvidenceRankingResultV1, } from "../../../types/buildFit"; import { buildFitInputFromScanResult, rankBuildFitEvidence, } from "../../../lib/buildFitContract"; import { aggregateContextKey, buildExplicitAggregateContexts, type BuildFitExplicitContextDraft, } from "../../../lib/buildFitExplicitContext"; import { createRendererRepositories } from "../../../infrastructure/repositories/rendererBridgeRepositories"; import type { AppLocale } from "../../../i18n"; import { getBuildCopy } from "../buildsCopy"; const MAX_NATIVE_BUILD_FIT_RESULTS = 2400; interface UseBuildsViewModelInput { snapshot: AppSnapshot; locale: AppLocale; } export interface BuildsViewModel { artifactCount: number; hasScanData: boolean; foundationLabel: string; foundationDetail: string; contractLabel: string; contractDetail: string; profileLabel: string; profileDetail: string; nativeRunDir: string; nativeResultCount: number; isLoadingBuildFit: boolean; buildFitLoadError: string; ranking: BuildFitEvidenceRankingResultV1; aggregateContextFields: BuildFitAggregateContextField[]; setAggregateContextDraft: ( profileId: string, targetKey: string, update: Partial, ) => void; refreshBuildFit: () => Promise; } export interface BuildFitAggregateContextField { profileId: string; characterName: string; targetLabel: string; targetKey: string; stat: string; unit: "flat" | "percent"; minimum?: number; maximum?: number; reason: string; value: string; confirmed: boolean; error: string; } export function useBuildsViewModel({ snapshot, locale }: UseBuildsViewModelInput): BuildsViewModel { const copy = getBuildCopy(locale); const repositories = useMemo(() => createRendererRepositories(), []); const [nativeResults, setNativeResults] = useState([]); const [nativeRunDir, setNativeRunDir] = useState(""); const [nativeResultCount, setNativeResultCount] = useState(0); const [isLoadingBuildFit, setIsLoadingBuildFit] = useState(Boolean(repositories)); const [buildFitLoadError, setBuildFitLoadError] = useState(""); const [aggregateContextDrafts, setAggregateContextDrafts] = useState>>({}); const refreshBuildFit = useCallback(async () => { if (!repositories?.automation) { setNativeResults([]); setNativeRunDir(""); setNativeResultCount(0); setBuildFitLoadError(copy.noBridge); setIsLoadingBuildFit(false); return; } setIsLoadingBuildFit(true); setBuildFitLoadError(""); try { const result = await repositories.automation.nativeScannerLoadResults({ limit: MAX_NATIVE_BUILD_FIT_RESULTS, }); if (!result.ok) { setNativeResults([]); setNativeRunDir(""); setNativeResultCount(0); setBuildFitLoadError(result.error ?? copy.noCompleteRun); return; } setNativeResults(result.results); setNativeRunDir(result.runDir); setNativeResultCount(result.total); } catch (error) { setNativeResults([]); setNativeRunDir(""); setNativeResultCount(0); setBuildFitLoadError(error instanceof Error ? error.message : copy.loadFailed); } finally { setIsLoadingBuildFit(false); } }, [copy.loadFailed, copy.noBridge, copy.noCompleteRun, repositories]); useEffect(() => { void refreshBuildFit(); }, [refreshBuildFit]); const aggregateContextResults = useMemo(() => Object.fromEntries( curatedBuildFitProfiles.map((profile) => [ profile.id, buildExplicitAggregateContexts(profile, aggregateContextDrafts[profile.id] ?? {}), ]), ) as Record>, [aggregateContextDrafts]); const aggregateContextByProfile = useMemo(() => Object.fromEntries( Object.entries(aggregateContextResults) .filter(([, result]) => result.contexts.length > 0) .map(([profileId, result]) => [profileId, result.contexts]), ), [aggregateContextResults]); const aggregateContextFields = useMemo(() => curatedBuildFitProfiles.flatMap((profile) => profile.aggregateTargets.map((target) => { const targetKey = aggregateContextKey(target); const draft = aggregateContextDrafts[profile.id]?.[targetKey] ?? { value: "", confirmed: false }; return { profileId: profile.id, characterName: profile.character.name, targetLabel: profile.target.label, targetKey, stat: target.stat, unit: target.unit, ...(target.minimum === undefined ? {} : { minimum: target.minimum }), ...(target.maximum === undefined ? {} : { maximum: target.maximum }), reason: target.reason, value: draft.value, confirmed: draft.confirmed, error: aggregateContextResults[profile.id]?.issues[targetKey] ?? "", }; })), [aggregateContextDrafts, aggregateContextResults]); const setAggregateContextDraft = useCallback(( profileId: string, targetKey: string, update: Partial, ) => { setAggregateContextDrafts((current) => ({ ...current, [profileId]: { ...current[profileId], [targetKey]: { value: current[profileId]?.[targetKey]?.value ?? "", confirmed: current[profileId]?.[targetKey]?.confirmed ?? false, ...update, }, }, })); }, []); const ranking = useMemo(() => rankBuildFitEvidence({ version: BUILD_FIT_EVIDENCE_RANKING_VERSION, profiles: curatedBuildFitProfiles, artifacts: nativeResults .map((result) => buildFitInputFromScanResult(result)) .filter((result): result is NonNullable => result !== null), aggregateContextByProfile, }), [aggregateContextByProfile, nativeResults]); const artifactCount = Math.max(snapshot.artifacts.length, nativeResultCount); const hasScanData = artifactCount > 0; const profileCount = curatedBuildFitProfiles.length; const suggestionCount = ranking.suggestions.length; return { artifactCount, hasScanData, foundationLabel: hasScanData ? copy.foundationReady : copy.foundationEmpty, foundationDetail: hasScanData ? copy.foundationReadyDetail(nativeResultCount || artifactCount) : copy.foundationEmptyDetail, contractLabel: copy.contractLabel, contractDetail: copy.contractDetail( BUILD_FIT_CONTRACT_VERSION, BUILD_FIT_COMBINATION_ASSESSMENT_VERSION, BUILD_FIT_EVIDENCE_RANKING_VERSION, ), profileLabel: suggestionCount > 0 ? copy.profilesReady(suggestionCount) : copy.profilesChecked(profileCount), profileDetail: suggestionCount > 0 ? copy.profilesReadyDetail(BUILD_FIT_PROFILE_CORPUS_VERSION) : copy.profilesBlockedDetail(BUILD_FIT_PROFILE_CORPUS_VERSION), nativeRunDir, nativeResultCount, isLoadingBuildFit, buildFitLoadError, ranking, aggregateContextFields, setAggregateContextDraft, refreshBuildFit, }; }