import CodexBarCore import Foundation private struct CodexPreparedImportedAccount { let account: ManagedCodexAccount let homeURL: URL } struct CodexDisplacedLivePreservationExecutionResult: Equatable { let displacedLiveDisposition: CodexAccountPromotionResult.DisplacedLiveDisposition } @MainActor struct CodexDisplacedLivePreservationExecutor { private let store: any ManagedCodexAccountStoring private let homeFactory: any ManagedCodexHomeProducing private let fileManager: FileManager init( store: any ManagedCodexAccountStoring, homeFactory: any ManagedCodexHomeProducing, fileManager: FileManager = .default) { self.store = store self.homeFactory = homeFactory self.fileManager = fileManager } func execute( plan: CodexDisplacedLivePreservationPlan, context: PreparedPromotionContext) throws -> CodexDisplacedLivePreservationExecutionResult { /* Safety contract: - This executor never swaps live auth. The caller must do that only after success. - Import cleanup is best-effort and leaves no orphaned managed home on failure. - Refresh/repair may copy auth before store commit, matching current behavior. */ switch plan { case .none: return CodexDisplacedLivePreservationExecutionResult(displacedLiveDisposition: .none) case let .reject(reason): throw self.error(for: reason) case .importNew: let importedAccount = try self.importDisplacedLiveAccount(from: context) return try self.commitImportedAccount(importedAccount) case let .refreshExisting(destination, _), let .repairExisting(destination, _): guard destination.persisted.id != context.target.persisted.id else { throw CodexAccountPromotionError.managedStoreCommitFailed } let refreshed = try self.refreshExistingManagedAccount(destination, from: context) return CodexDisplacedLivePreservationExecutionResult( displacedLiveDisposition: .alreadyManaged(managedAccountID: refreshed.id)) } } private func error(for reason: CodexDisplacedLivePreservationRejectReason) -> CodexAccountPromotionError { switch reason { case .liveUnreadable: .liveAccountUnreadable case .liveAPIKeyOnlyUnsupported: .liveAccountAPIKeyOnlyUnsupported case .liveIdentityMissingForPreservation: .liveAccountMissingIdentityForPreservation case .conflictingReadableManagedHome: .displacedLiveManagedAccountConflict } } private func importDisplacedLiveAccount( from context: PreparedPromotionContext) throws -> CodexPreparedImportedAccount { guard case let .readable(liveAuthMaterial) = context.live.homeState else { throw CodexAccountPromotionError.displacedLiveImportFailed } let importedHomeURL = self.homeFactory.makeHomeURL() let importedAccountID = Self.accountID(for: importedHomeURL) do { try self.fileManager.createDirectory(at: importedHomeURL, withIntermediateDirectories: true) try self.writeManagedAuthData(liveAuthMaterial.rawData, to: importedHomeURL) guard let liveAuthIdentity = context.live.authIdentity, let email = liveAuthIdentity.email, liveAuthIdentity.identity != .unresolved else { throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation } let now = Date().timeIntervalSince1970 return CodexPreparedImportedAccount( account: ManagedCodexAccount( id: importedAccountID, email: email, providerAccountID: liveAuthIdentity.providerAccountID, workspaceLabel: liveAuthIdentity.workspaceLabel, workspaceAccountID: liveAuthIdentity.workspaceAccountID, authFingerprint: CodexAuthFingerprint.fingerprint(data: liveAuthMaterial.rawData), managedHomePath: importedHomeURL.path, createdAt: now, updatedAt: now, lastAuthenticatedAt: now), homeURL: importedHomeURL) } catch let error as CodexAccountPromotionError { try? self.removeManagedHomeIfSafe(importedHomeURL) throw error } catch { try? self.removeManagedHomeIfSafe(importedHomeURL) throw CodexAccountPromotionError.displacedLiveImportFailed } } private func commitImportedAccount(_ importedAccount: CodexPreparedImportedAccount) throws -> CodexDisplacedLivePreservationExecutionResult { do { let latestManagedAccounts = try self.store.loadAccounts() try self.store.storeAccounts(ManagedCodexAccountSet( version: latestManagedAccounts.version, accounts: latestManagedAccounts.accounts + [importedAccount.account])) return try self.resolveImportedAccountAfterCommit(importedAccount) } catch let error as CodexAccountPromotionError { try? self.removeManagedHomeIfSafe(importedAccount.homeURL) throw error } catch { try? self.removeManagedHomeIfSafe(importedAccount.homeURL) throw CodexAccountPromotionError.managedStoreCommitFailed } } private func resolveImportedAccountAfterCommit(_ importedAccount: CodexPreparedImportedAccount) throws -> CodexDisplacedLivePreservationExecutionResult { let persistedManagedAccounts = try self.store.loadAccounts() if persistedManagedAccounts.account(id: importedAccount.account.id) != nil { return CodexDisplacedLivePreservationExecutionResult( displacedLiveDisposition: .imported(managedAccountID: importedAccount.account.id)) } guard let existingManagedAccount = self.repairDestination( in: persistedManagedAccounts, for: importedAccount.account) else { throw CodexAccountPromotionError.managedStoreCommitFailed } let repairedManagedAccount = ManagedCodexAccount( id: existingManagedAccount.id, email: importedAccount.account.email, providerAccountID: importedAccount.account.providerAccountID, workspaceLabel: importedAccount.account.workspaceLabel, workspaceAccountID: importedAccount.account.workspaceAccountID, authFingerprint: importedAccount.account.authFingerprint, managedHomePath: importedAccount.homeURL.path, createdAt: existingManagedAccount.createdAt, updatedAt: importedAccount.account.updatedAt, lastAuthenticatedAt: importedAccount.account.lastAuthenticatedAt) try self.store.storeAccounts(ManagedCodexAccountSet( version: persistedManagedAccounts.version, accounts: persistedManagedAccounts.accounts.map { account in guard account.id == existingManagedAccount.id else { return account } return repairedManagedAccount })) if existingManagedAccount.managedHomePath != importedAccount.homeURL.path { try? self.removeManagedHomeIfSafe( URL(fileURLWithPath: existingManagedAccount.managedHomePath, isDirectory: true)) } return CodexDisplacedLivePreservationExecutionResult( displacedLiveDisposition: .alreadyManaged(managedAccountID: existingManagedAccount.id)) } private func repairDestination( in persistedManagedAccounts: ManagedCodexAccountSet, for importedAccount: ManagedCodexAccount) -> ManagedCodexAccount? { if let providerAccountID = importedAccount.providerAccountID { return persistedManagedAccounts.account( email: importedAccount.email, providerAccountID: providerAccountID) } let normalizedEmail = importedAccount.email .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() return persistedManagedAccounts.accounts.first { $0.email == normalizedEmail && $0.providerAccountID == nil } } private func refreshExistingManagedAccount( _ destination: PreparedStoredManagedAccount, from context: PreparedPromotionContext) throws -> ManagedCodexAccount { guard case let .readable(liveAuthMaterial) = context.live.homeState else { throw CodexAccountPromotionError.managedStoreCommitFailed } guard let liveAuthIdentity = context.live.authIdentity else { throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation } do { let latestManagedAccounts = try self.store.loadAccounts() guard let persistedManagedAccount = latestManagedAccounts.account(id: destination.persisted.id) else { throw CodexAccountPromotionError.managedStoreCommitFailed } let email = liveAuthIdentity.email ?? (liveAuthIdentity.providerAccountID != nil ? persistedManagedAccount.email : nil) guard let email, liveAuthIdentity.identity != .unresolved else { throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation } let now = Date().timeIntervalSince1970 let refreshedManagedAccount = ManagedCodexAccount( id: persistedManagedAccount.id, email: email, providerAccountID: liveAuthIdentity.providerAccountID ?? persistedManagedAccount.providerAccountID, workspaceLabel: liveAuthIdentity.workspaceLabel ?? persistedManagedAccount.workspaceLabel, workspaceAccountID: liveAuthIdentity.workspaceAccountID ?? persistedManagedAccount.workspaceAccountID, authFingerprint: CodexAuthFingerprint.fingerprint(data: liveAuthMaterial.rawData), managedHomePath: persistedManagedAccount.managedHomePath, createdAt: persistedManagedAccount.createdAt, updatedAt: now, lastAuthenticatedAt: now) let refreshedHomeURL = URL(fileURLWithPath: persistedManagedAccount.managedHomePath, isDirectory: true) do { try self.homeFactory.validateManagedHomeForDeletion(refreshedHomeURL) } catch { throw CodexAccountPromotionError.displacedLiveImportFailed } try self.fileManager.createDirectory(at: refreshedHomeURL, withIntermediateDirectories: true) try self.writeManagedAuthData(liveAuthMaterial.rawData, to: refreshedHomeURL) try self.store.storeAccounts(ManagedCodexAccountSet( version: latestManagedAccounts.version, accounts: latestManagedAccounts.accounts.map { account in guard account.id == persistedManagedAccount.id else { return account } return refreshedManagedAccount })) return refreshedManagedAccount } catch let error as CodexAccountPromotionError { throw error } catch { throw CodexAccountPromotionError.managedStoreCommitFailed } } private func writeManagedAuthData(_ data: Data, to homeURL: URL) throws { let authFileURL = CodexAccountPromotionService.authFileURL(for: homeURL) try data.write(to: authFileURL, options: .atomic) try self.fileManager.setAttributes( [.posixPermissions: NSNumber(value: Int16(0o600))], ofItemAtPath: authFileURL.path) } private func removeManagedHomeIfSafe(_ homeURL: URL) throws { try self.homeFactory.validateManagedHomeForDeletion(homeURL) if self.fileManager.fileExists(atPath: homeURL.path) { try self.fileManager.removeItem(at: homeURL) } } private static func accountID(for homeURL: URL) -> UUID { UUID(uuidString: homeURL.lastPathComponent) ?? UUID() } }