import Foundation public final class SongProjectGenerationDirector: Sendable { private let aiService: any AIService public init(aiService: any AIService) { self.aiService = aiService } public func generateCompleteProject( fromIdea idea: String, seedProject: SongProject? = nil, discussionMode: ConversationMode = .auto, conversation: [AIConversationMessage] = [], localeIdentifier: String? = nil, generatedAt: Date = Date() ) async throws -> SongProjectGenerationResult { let trimmedIdea = idea.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedIdea.isEmpty else { throw SongProjectGenerationDirectorError.emptyIdea } let request = SongProjectGenerationRequest( context: AIRequestContext( userInstruction: trimmedIdea, conversation: conversation, localeIdentifier: localeIdentifier ), seedProject: seedProject, discussionMode: discussionMode ) let result = try await aiService.generateSongProject(from: request) let project = normalizeGeneratedProject( result.project, idea: trimmedIdea, seedProject: seedProject, discussionMode: discussionMode, generatedAt: generatedAt ) return SongProjectGenerationResult( project: project, notes: result.notes, followUpQuestions: result.followUpQuestions ) } private func normalizeGeneratedProject( _ generatedProject: SongProject, idea: String, seedProject: SongProject?, discussionMode: ConversationMode, generatedAt: Date ) -> SongProject { let normalizedIdea = generatedProject.idea.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? idea : generatedProject.idea guard let seedProject else { var project = generatedProject project.idea = normalizedIdea project.conversationMode = discussionMode project.updatedAt = generatedAt return project } let update = SongProjectAIUpdateMerger().apply( snapshot: SongProjectSnapshot(project: generatedProject), changedScopes: SongProjectUpdateScope.allCases, to: seedProject, updatedAt: generatedAt ) var project = update.project project.title = generatedProject.title project.idea = normalizedIdea project.conversationMode = discussionMode project.moods = generatedProject.moods return project } } public enum SongProjectGenerationDirectorError: Error, Equatable, Sendable { case emptyIdea }