import { randomUUID } from 'crypto' import type { AutoWritingConfig, InteractiveFlow, InteractiveFlowStatus, InteractiveScene, NamingPause, WizardWritingConfig, WritingFlowMode, WritingStep } from '../../../shared/types' import type { SqliteDb } from '../types' export class InteractiveRepository { constructor(private db: SqliteDb) {} getBySession(sessionId: string): InteractiveFlow | null { const row = this.db .prepare( `SELECT * FROM interactive_flows WHERE session_id = ? AND status = 'in_progress' ORDER BY updated_at DESC LIMIT 1` ) .get(sessionId) as Record | undefined return row ? this.mapFlow(row) : null } get(flowId: string): InteractiveFlow | null { const row = this.db.prepare('SELECT * FROM interactive_flows WHERE id = ?').get(flowId) as | Record | undefined return row ? this.mapFlow(row) : null } create( sessionId: string, flowMode: WritingFlowMode = 'interactive', initialStep?: WritingStep ): InteractiveFlow { const id = randomUUID() const step = initialStep ?? (flowMode === 'auto' ? 'goal_setup' : flowMode === 'wizard' ? 'wizard_goal' : 'context_confirm') this.db .prepare( `INSERT INTO interactive_flows (id, session_id, step, status, context_json, flow_mode, mode_config_json) VALUES (?, ?, ?, 'in_progress', '{}', ?, '{}')` ) .run(id, sessionId, step, flowMode) return this.get(id)! } resetInProgress(sessionId: string, flowMode: WritingFlowMode = 'interactive'): InteractiveFlow { this.db .prepare( `UPDATE interactive_flows SET status = 'abandoned', updated_at = datetime('now') WHERE session_id = ? AND status = 'in_progress'` ) .run(sessionId) return this.create(sessionId, flowMode) } updateStep( flowId: string, step: WritingStep, patch: { contextJson?: string plotSelectionJson?: string | null namingPendingJson?: string | null sceneDraftHtml?: string | null modeConfigJson?: string flowMode?: WritingFlowMode status?: InteractiveFlowStatus } = {} ): InteractiveFlow { const sets: string[] = ['step = ?', `updated_at = datetime('now')`] const vals: unknown[] = [step] if (patch.contextJson !== undefined) { sets.push('context_json = ?') vals.push(patch.contextJson) } if (patch.plotSelectionJson !== undefined) { sets.push('plot_selection_json = ?') vals.push(patch.plotSelectionJson) } if (patch.namingPendingJson !== undefined) { sets.push('naming_pending_json = ?') vals.push(patch.namingPendingJson) } if (patch.sceneDraftHtml !== undefined) { sets.push('scene_draft_html = ?') vals.push(patch.sceneDraftHtml) } if (patch.modeConfigJson !== undefined) { sets.push('mode_config_json = ?') vals.push(patch.modeConfigJson) } if (patch.flowMode !== undefined) { sets.push('flow_mode = ?') vals.push(patch.flowMode) } if (patch.status !== undefined) { sets.push('status = ?') vals.push(patch.status) } vals.push(flowId) this.db.prepare(`UPDATE interactive_flows SET ${sets.join(', ')} WHERE id = ?`).run(...vals) return this.get(flowId)! } getModeConfig( flowId: string ): T { const row = this.db .prepare('SELECT mode_config_json FROM interactive_flows WHERE id = ?') .get(flowId) as { mode_config_json: string } | undefined try { return JSON.parse(row?.mode_config_json ?? '{}') as T } catch { return {} as T } } setModeConfig(flowId: string, config: AutoWritingConfig | WizardWritingConfig): void { this.db .prepare( `UPDATE interactive_flows SET mode_config_json = ?, updated_at = datetime('now') WHERE id = ?` ) .run(JSON.stringify(config), flowId) } listScenes(flowId: string): InteractiveScene[] { const rows = this.db .prepare('SELECT * FROM interactive_scenes WHERE flow_id = ? ORDER BY sort_order') .all(flowId) as Record[] return rows.map((r) => ({ id: r.id as string, sortOrder: r.sort_order as number, contentHtml: r.content_html as string, plotLabel: r.plot_label as string, refinedCount: r.refined_count as number })) } addScene(flowId: string, contentHtml: string, plotLabel: string): InteractiveScene { const id = randomUUID() const max = this.db .prepare('SELECT COALESCE(MAX(sort_order), -1) AS m FROM interactive_scenes WHERE flow_id = ?') .get(flowId) as { m: number } const sortOrder = max.m + 1 this.db .prepare( `INSERT INTO interactive_scenes (id, flow_id, sort_order, content_html, plot_label) VALUES (?, ?, ?, ?, ?)` ) .run(id, flowId, sortOrder, contentHtml, plotLabel) return this.listScenes(flowId).find((s) => s.id === id)! } updateSceneContent(flowId: string, sceneId: string, contentHtml: string, refinedCount: number): void { this.db .prepare( `UPDATE interactive_scenes SET content_html = ?, refined_count = ? WHERE id = ? AND flow_id = ?` ) .run(contentHtml, refinedCount, sceneId, flowId) } getSceneDraft(flowId: string): string { const row = this.db .prepare('SELECT scene_draft_html FROM interactive_flows WHERE id = ?') .get(flowId) as { scene_draft_html: string | null } | undefined return row?.scene_draft_html ?? '' } setSceneDraft(flowId: string, html: string): void { this.db .prepare(`UPDATE interactive_flows SET scene_draft_html = ?, updated_at = datetime('now') WHERE id = ?`) .run(html, flowId) } getPlotSelection(flowId: string): string { const row = this.db .prepare('SELECT plot_selection_json FROM interactive_flows WHERE id = ?') .get(flowId) as { plot_selection_json: string | null } | undefined return row?.plot_selection_json ?? '{}' } getContextJson(flowId: string): string { const row = this.db .prepare('SELECT context_json FROM interactive_flows WHERE id = ?') .get(flowId) as { context_json: string } | undefined return row?.context_json ?? '{}' } private mapFlow(row: Record): InteractiveFlow { const flowId = row.id as string let namingPending: NamingPause | null = null if (row.naming_pending_json) { try { namingPending = JSON.parse(row.naming_pending_json as string) as NamingPause } catch { namingPending = null } } let modeConfig: AutoWritingConfig | WizardWritingConfig | Record = {} try { modeConfig = JSON.parse((row.mode_config_json as string) ?? '{}') } catch { modeConfig = {} } return { id: flowId, sessionId: row.session_id as string, flowMode: (row.flow_mode as WritingFlowMode) ?? 'interactive', step: row.step as WritingStep, status: row.status as InteractiveFlowStatus, modeConfig, scenes: this.listScenes(flowId), namingPending, contextSummary: undefined } } }