"use client"; import { type Dispatch, FormEvent, MutableRefObject, SetStateAction } from "react"; import { beamAvatarSchema, type BeamAvatarPatch, } from "@/lib/beam-avatar"; import { applyPersistedBirthTime, birthTimePersistenceValues, isBirthTimeDraftReady, } from "@/lib/birth-time-intake-model"; import { birthTimeConsultationOptionsCopy, createBirthTimeConsultationConsentState, type BirthTimeConsultationConsentState, type LatestAccountRequestGuard, } from "@/lib/birth-time-consultation-consent"; import { composerDraftSnapshot } from "@/lib/composer-draft"; import { activeChartStorageKey, fetchAccount, friendlyError, LoginRedirectError, saveCloudChartProfile, } from "@/lib/home-cloud-sync"; import { birthProfileDeclarationChanged, buildSelfChartRecord, missingProfileStep, readProfile, selectedBirthPlace, } from "@/lib/home-profile"; import { resolveAyanamsa } from "@/lib/ayanamsa"; import { createStartGreeting } from "@/lib/onboarding-client"; import { timestamp, type Account, type AccountDialog, type ChartLibraryRecord, type OnboardingStep, type OpenAccountDialogOptions, type Profile, accountDialogOptions, } from "@/lib/home-types"; import { preserveShallowEqual } from "@/lib/preserve-shallow-equal"; import { requestBirthTimeAssessment, type JourneyClientResponse, } from "@/lib/birth-time-journey-client"; import { previewRectificationJourney } from "@/lib/birth-time-guided-preview"; import { selfHostedOtpActions } from "@/modules/identity/client"; import type { BirthTimeAssessmentPhase } from "@/components/birth-time-assessment-overlay"; export type ProfileOnboardingParams = { account: Account | null; accountRefreshGuard: MutableRefObject; accountTrigger: MutableRefObject; activeChartId: string; avatarSaving: boolean; birthTimeRevisionPending: MutableRefObject; chartLibrary: ChartLibraryRecord[]; dialogReturnTarget: MutableRefObject; profile: Profile; profileDraft: Profile; profileSaving: boolean; setAccount: Dispatch>; setAccountError: Dispatch>; setAccountMenuOpen: Dispatch>; setActiveAccountDialog: Dispatch>; setActiveChartId: Dispatch>; setAvatarNotice: Dispatch>; setAvatarSaving: Dispatch>; setBirthTimeAssessmentPhase: Dispatch>; setBirthTimeConsultationConsent: Dispatch>; setBirthTimeError: Dispatch>; setBirthTimeJourney: Dispatch>; setBillingPane: Dispatch>; setDraft: (value: string) => void; setOnboardingJustCompleted: Dispatch>; setOnboardingStep: Dispatch>; setPresetMessageLength: Dispatch>; setProfile: Dispatch>; setProfileDraft: Dispatch>; setProfileNotice: Dispatch>; setProfileSaving: Dispatch>; setRectificationError: Dispatch>; setSigningOut: Dispatch>; setStartGreeting: Dispatch>; signingOut: boolean; uiPreview: MutableRefObject; guidedBirthTimeReadyRef: MutableRefObject<(result: JourneyClientResponse) => void>; editDeclaredBirthTimeDetailsRef: MutableRefObject<() => void>; }; export function useProfileOnboarding(params: ProfileOnboardingParams) { const { account, accountRefreshGuard, accountTrigger, activeChartId, avatarSaving, birthTimeRevisionPending, chartLibrary, dialogReturnTarget, profile, profileDraft, profileSaving, setAccount, setAccountError, setAccountMenuOpen, setActiveAccountDialog, setActiveChartId, setAvatarNotice, setAvatarSaving, setBirthTimeAssessmentPhase, setBirthTimeConsultationConsent, setBirthTimeError, setBirthTimeJourney, setBillingPane, setDraft, setOnboardingJustCompleted, setOnboardingStep, setPresetMessageLength, setProfile, setProfileDraft, setProfileNotice, setProfileSaving, setRectificationError, setSigningOut, setStartGreeting, signingOut, uiPreview, guidedBirthTimeReadyRef, editDeclaredBirthTimeDetailsRef, } = params; async function refreshAccount() { const requestIdentity = accountRefreshGuard.current.begin(); try { const latest = await fetchAccount(); if (!accountRefreshGuard.current.isCurrent(requestIdentity)) return; const nextProfile = readProfile(latest.profile); const activeOther = activeChartId !== "self" && chartLibrary.find((record) => record.id === activeChartId && record.role === "other"); if (!activeOther) { setProfile((current) => preserveShallowEqual(current, nextProfile)); } setAccount(latest); setAccountError(""); } catch (caught) { if (caught instanceof LoginRedirectError) return; if (!accountRefreshGuard.current.isCurrent(requestIdentity)) return; setAccountError(caught instanceof Error ? caught.message : "暂时无法读取账户信息"); } } function openAccountDialog( dialog: AccountDialog, returnTargetOrOptions?: HTMLButtonElement | null | OpenAccountDialogOptions, ) { const options = accountDialogOptions(returnTargetOrOptions); dialogReturnTarget.current = options.returnTarget ?? accountTrigger.current; setAccountMenuOpen(false); setAccountError(""); if (dialog === "profile") { setProfileNotice(""); setAvatarNotice(""); } if (dialog === "chart-library") { setProfileDraft(profile); setProfileNotice(""); } if (dialog === "billing") { setBillingPane({ source: options.source, plan: options.plan, tab: options.tab, }); } setActiveAccountDialog(dialog); } function closeAccountDialog() { if (signingOut) return; setActiveAccountDialog(null); const returnTarget = dialogReturnTarget.current; window.requestAnimationFrame(() => returnTarget?.focus()); } async function persistAvatar(patch: BeamAvatarPatch) { if (!account?.avatar || avatarSaving) return; setAvatarSaving(true); setAvatarNotice(""); setAccountError(""); try { const response = await fetch("/api/account/avatar", { method: "PATCH", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify(patch), }); const payload = await response.json().catch(() => null) as { avatar?: unknown; error?: string } | null; if (!response.ok) throw new Error(payload?.error || "头像暂时无法保存。"); const avatar = beamAvatarSchema.parse(payload?.avatar); setAccount((current) => current ? { ...current, avatar } : current); setAvatarNotice(patch.action === "randomize" ? "已生成并保存新头像。" : "头像配色已保存。"); } catch (caught) { setAccountError(friendlyError(caught instanceof Error ? caught.message : "头像保存失败")); } finally { setAvatarSaving(false); } } async function persistProfile(nextProfile: Profile): Promise { if (!account) throw new Error("账户尚未加载完成"); if (process.env.NODE_ENV === "development" && uiPreview.current) return nextProfile; const birthPlace = selectedBirthPlace(nextProfile); const response = await fetch("/api/account", { method: "PATCH", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify({ name: nextProfile.name.trim() || null, birth_date: nextProfile.date || null, ...birthTimePersistenceValues(nextProfile), country_code: nextProfile.countryCode, province_code: nextProfile.provinceCode || null, city_code: nextProfile.cityCode || null, district_code: nextProfile.districtCode || null, birth_place_label: nextProfile.birthPlaceLabel || null, birth_place_type: nextProfile.birthPlaceType || null, birth_place_provider: nextProfile.birthPlaceProvider || null, birth_place_provider_id: nextProfile.birthPlaceProviderId || null, latitude: birthPlace?.lat ?? null, longitude: birthPlace?.lon ?? null, timezone_id: nextProfile.timezoneId || null, timezone_offset: birthPlace?.tz ?? null, timezone_source: nextProfile.timezoneSource || null, ayanamsa: resolveAyanamsa(nextProfile), }), }); const payload = await response.json().catch(() => null) as { error?: string; birthTime?: unknown; } | null; if (!response.ok) { throw new Error(payload?.error || "账户资料暂时无法保存。"); } const savedProfile = applyPersistedBirthTime(nextProfile, payload?.birthTime); setAccount((current) => current ? { ...current, profile: savedProfile } : current); setActiveChartId("self"); localStorage.setItem(activeChartStorageKey(account.user.id), "self"); await saveCloudChartProfile({ ...buildSelfChartRecord(savedProfile), updatedAt: timestamp() }).catch(() => null); return savedProfile; } async function assessSavedBirthTime(nextProfile: Profile) { const result = process.env.NODE_ENV === "development" && uiPreview.current ? previewRectificationJourney : await requestBirthTimeAssessment(); const nextStatus = result.snapshot.state === "ready" ? "confirmed" : result.snapshot.state === "candidate" ? "candidate" : "rectifying"; const assessedProfile: Profile = { ...nextProfile, time: result.snapshot.activeTime ?? "", birthTimeStatus: nextStatus, rectificationCaseId: result.caseId, }; setBirthTimeJourney(result); setBirthTimeError(""); setProfile(assessedProfile); setProfileDraft(assessedProfile); return assessedProfile; } async function saveProfile(event: FormEvent) { event.preventDefault(); if (!profileDraft.name.trim() || !isBirthTimeDraftReady(profileDraft) || !selectedBirthPlace(profileDraft) || !account || profileSaving) return; setProfileSaving(true); setProfileNotice(""); setAccountError(""); try { const declarationChanged = birthProfileDeclarationChanged(profile, profileDraft); const savedProfile = await persistProfile(profileDraft); setProfile(savedProfile); setProfileDraft(savedProfile); setRectificationError(""); if (declarationChanged) { setBirthTimeConsultationConsent(createBirthTimeConsultationConsentState()); void refreshAccount(); } setProfileNotice(savedProfile.birthTimeStatus === "confirmed" ? "出生资料已保存到云端,可在同一账号的其他设备使用。" : `出生资料已保存。${birthTimeConsultationOptionsCopy(savedProfile)}`); } catch (caught) { setAccountError(friendlyError(caught instanceof Error ? caught.message : "出生资料保存失败")); } finally { setProfileSaving(false); } } async function saveOnboardingName() { const name = composerDraftSnapshot().replace(/\s+/g, " ").trim().slice(0, 80); if (!name || !account || profileSaving) return; const nextProfile = { ...profileDraft, name }; setProfileSaving(true); setAccountError(""); try { const savedProfile = await persistProfile(nextProfile); setProfile(savedProfile); setProfileDraft(savedProfile); setStartGreeting(createStartGreeting(savedProfile.name)); setDraft(""); setPresetMessageLength(0); const nextStep = missingProfileStep(savedProfile); if (nextStep) setOnboardingStep(nextStep); else setOnboardingJustCompleted(false); } catch (caught) { setAccountError(friendlyError(caught instanceof Error ? caught.message : "称呼保存失败")); } finally { setProfileSaving(false); } } async function saveOnboardingBirth(event: FormEvent) { event.preventDefault(); if (!isBirthTimeDraftReady(profileDraft) || !account || profileSaving) return; setProfileSaving(true); setBirthTimeAssessmentPhase("saving_profile"); setAccountError(""); try { const savedProfile = await persistProfile(profileDraft); birthTimeRevisionPending.current = false; setProfile(savedProfile); setProfileDraft(savedProfile); setPresetMessageLength(0); const nextStep = missingProfileStep(savedProfile); if (nextStep) setOnboardingStep(nextStep); else setOnboardingJustCompleted(false); } catch (caught) { setAccountError(friendlyError(caught instanceof Error ? caught.message : "出生时间保存失败")); } finally { setBirthTimeAssessmentPhase(null); setProfileSaving(false); } } function editDeclaredBirthTimeDetails() { birthTimeRevisionPending.current = true; setBirthTimeError(""); setPresetMessageLength(0); setOnboardingStep("birth"); } async function saveOnboardingPlace(event: FormEvent) { event.preventDefault(); if (!selectedBirthPlace(profileDraft) || !account || profileSaving) return; setProfileSaving(true); setBirthTimeAssessmentPhase("entering_home"); setAccountError(""); try { const savedProfile = await persistProfile(profileDraft); setProfile(savedProfile); setProfileDraft(savedProfile); setStartGreeting(createStartGreeting(savedProfile.name)); setPresetMessageLength(0); setOnboardingJustCompleted(false); } catch (caught) { setAccountError(friendlyError(caught instanceof Error ? caught.message : "出生地点保存失败")); } finally { setBirthTimeAssessmentPhase(null); setProfileSaving(false); } } function completeGuidedBirthTime(result: JourneyClientResponse) { if (result.nextAction.kind !== "ready") return; const confirmedProfile: Profile = { ...profileDraft, time: result.nextAction.activeTime, birthTimeStatus: "confirmed", rectificationCaseId: result.caseId, }; setProfile(confirmedProfile); setProfileDraft(confirmedProfile); setPresetMessageLength(0); setOnboardingJustCompleted(false); } async function retryBirthTimeAssessment() { if (!account || profileSaving) return; setProfileSaving(true); setBirthTimeError(""); try { const assessedProfile = await assessSavedBirthTime(profileDraft); setPresetMessageLength(0); if (assessedProfile.birthTimeStatus === "confirmed") setOnboardingJustCompleted(false); } catch (caught) { setBirthTimeError(caught instanceof Error ? caught.message : "生时评估暂时不可用,请稍后重试。"); } finally { setProfileSaving(false); } } async function signOut() { if (signingOut) return; setSigningOut(true); setAccountError(""); try { await selfHostedOtpActions.signOut(); window.location.assign("/login"); } catch (caught) { const message = caught instanceof Error ? caught.message : "退出失败"; setAccountError(friendlyError(message)); setSigningOut(false); } } guidedBirthTimeReadyRef.current = completeGuidedBirthTime; editDeclaredBirthTimeDetailsRef.current = editDeclaredBirthTimeDetails; return { refreshAccount, openAccountDialog, closeAccountDialog, persistAvatar, persistProfile, assessSavedBirthTime, saveProfile, saveOnboardingName, saveOnboardingBirth, editDeclaredBirthTimeDetails, saveOnboardingPlace, completeGuidedBirthTime, retryBirthTimeAssessment, signOut, }; }