import { app } from 'electron'; import * as path from 'path'; import { InMemorySessionRepository } from '@/packages/infrastructure/repositories/InMemorySessionRepository'; import { MockBrowserAutomationAdapter } from '@/packages/infrastructure/adapters/automation/MockBrowserAutomationAdapter'; import { PlaywrightAutomationAdapter, AutomationAdapterMode } from '@/packages/infrastructure/adapters/automation/PlaywrightAutomationAdapter'; import { MockAutomationEngineAdapter } from '@/packages/infrastructure/adapters/automation/MockAutomationEngineAdapter'; import { StartAutomationSessionUseCase } from '@/packages/application/use-cases/StartAutomationSessionUseCase'; import { CheckAuthenticationUseCase } from '@/packages/application/use-cases/CheckAuthenticationUseCase'; import { InitiateLoginUseCase } from '@/packages/application/use-cases/InitiateLoginUseCase'; import { ClearSessionUseCase } from '@/packages/application/use-cases/ClearSessionUseCase'; import { ConfirmCheckoutUseCase } from '@/packages/application/use-cases/ConfirmCheckoutUseCase'; import { loadAutomationConfig, getAutomationMode, AutomationMode, BrowserModeConfigLoader } from '@/packages/infrastructure/config'; import { PinoLogAdapter } from '@/packages/infrastructure/adapters/logging/PinoLogAdapter'; import { NoOpLogAdapter } from '@/packages/infrastructure/adapters/logging/NoOpLogAdapter'; import { loadLoggingConfig } from '@/packages/infrastructure/config/LoggingConfig'; import type { ISessionRepository } from '@/packages/application/ports/ISessionRepository'; import type { IScreenAutomation } from '@/packages/application/ports/IScreenAutomation'; import type { IAutomationEngine } from '@/packages/application/ports/IAutomationEngine'; import type { IAuthenticationService } from '@/packages/application/ports/IAuthenticationService'; import type { ICheckoutConfirmationPort } from '@/packages/application/ports/ICheckoutConfirmationPort'; import type { ILogger } from '@/packages/application/ports/ILogger'; export interface BrowserConnectionResult { success: boolean; error?: string; } /** * Resolve the path to store persistent browser session data. * Uses Electron's userData directory for secure, per-user storage. * * @returns Absolute path to the iracing session directory */ function resolveSessionDataPath(): string { const userDataPath = app.getPath('userData'); return path.join(userDataPath, 'iracing-session'); } /** * Resolve the absolute path to the template directory. * Handles both development and production (packaged) Electron environments. * * @returns Absolute path to the iracing templates directory */ function resolveTemplatePath(): string { // In packaged app, app.getAppPath() returns the path to the app.asar or unpacked directory // In development, it returns the path to the app directory (apps/companion) const appPath = app.getAppPath(); if (app.isPackaged) { // Production: resources are in the app.asar or unpacked directory return path.join(appPath, 'resources/templates/iracing'); } // Development: navigate from apps/companion to project root // __dirname is apps/companion/main (or dist equivalent) // appPath is apps/companion return path.join(appPath, '../../resources/templates/iracing'); } /** * Create logger based on environment configuration. * In test environment, returns NoOpLogAdapter for silent logging. */ function createLogger(): ILogger { const config = loadLoggingConfig(); if (process.env.NODE_ENV === 'test') { return new NoOpLogAdapter(); } return new PinoLogAdapter(config); } /** * Determine the adapter mode based on environment. * - 'production' → 'real' (uses iRacing website selectors) * - 'development' → 'real' (uses iRacing website selectors) * - 'test' → 'mock' (uses data-* attribute selectors) */ function getAdapterMode(envMode: AutomationMode): AutomationAdapterMode { return envMode === 'test' ? 'mock' : 'real'; } /** * Create screen automation adapter based on configuration mode. * * Mode mapping: * - 'production' → PlaywrightAutomationAdapter with mode='real' for iRacing website * - 'development' → PlaywrightAutomationAdapter with mode='real' for iRacing website * - 'test' → MockBrowserAutomationAdapter * * @param mode - The automation mode from configuration * @param logger - Logger instance for the adapter * @returns PlaywrightAutomationAdapter instance (implements both IScreenAutomation and IAuthenticationService) */ function createBrowserAutomationAdapter( mode: AutomationMode, logger: ILogger, browserModeConfigLoader: BrowserModeConfigLoader ): PlaywrightAutomationAdapter | MockBrowserAutomationAdapter { const config = loadAutomationConfig(); // Resolve absolute template path for Electron environment const absoluteTemplatePath = resolveTemplatePath(); const sessionDataPath = resolveSessionDataPath(); logger.debug('Resolved paths', { absoluteTemplatePath, sessionDataPath, appPath: app.getAppPath(), isPackaged: app.isPackaged, cwd: process.cwd() }); const adapterMode = getAdapterMode(mode); // Get browser mode configuration from provided loader const browserModeConfig = browserModeConfigLoader.load(); logger.info('Creating browser automation adapter', { envMode: mode, adapterMode, browserMode: browserModeConfig.mode, browserModeSource: browserModeConfig.source, }); switch (mode) { case 'production': case 'development': return new PlaywrightAutomationAdapter( { headless: browserModeConfig.mode === 'headless', mode: adapterMode, userDataDir: sessionDataPath, }, logger.child({ adapter: 'Playwright', mode: adapterMode }), browserModeConfigLoader ); case 'test': default: return new MockBrowserAutomationAdapter(); } } export class DIContainer { private static instance: DIContainer; private logger: ILogger; private sessionRepository: ISessionRepository; private browserAutomation: PlaywrightAutomationAdapter | MockBrowserAutomationAdapter; private automationEngine: IAutomationEngine; private startAutomationUseCase: StartAutomationSessionUseCase; private checkAuthenticationUseCase: CheckAuthenticationUseCase | null = null; private initiateLoginUseCase: InitiateLoginUseCase | null = null; private clearSessionUseCase: ClearSessionUseCase | null = null; private confirmCheckoutUseCase: ConfirmCheckoutUseCase | null = null; private automationMode: AutomationMode; private browserModeConfigLoader: BrowserModeConfigLoader; private constructor() { // Initialize logger first - it's needed by other components this.logger = createLogger(); this.automationMode = getAutomationMode(); this.logger.info('DIContainer initializing', { automationMode: this.automationMode, nodeEnv: process.env.NODE_ENV }); const config = loadAutomationConfig(); // Initialize browser mode config loader as singleton this.browserModeConfigLoader = new BrowserModeConfigLoader(); this.sessionRepository = new InMemorySessionRepository(); this.browserAutomation = createBrowserAutomationAdapter( config.mode, this.logger, this.browserModeConfigLoader ); this.automationEngine = new MockAutomationEngineAdapter( this.browserAutomation, this.sessionRepository ); this.startAutomationUseCase = new StartAutomationSessionUseCase( this.automationEngine, this.browserAutomation, this.sessionRepository ); // Create authentication use cases only for real mode (PlaywrightAutomationAdapter) if (this.browserAutomation instanceof PlaywrightAutomationAdapter) { const authService = this.browserAutomation as IAuthenticationService; this.checkAuthenticationUseCase = new CheckAuthenticationUseCase(authService); this.initiateLoginUseCase = new InitiateLoginUseCase(authService); this.clearSessionUseCase = new ClearSessionUseCase(authService); } this.logger.info('DIContainer initialized', { automationMode: config.mode, sessionRepositoryType: 'InMemorySessionRepository', browserAutomationType: this.getBrowserAutomationType(config.mode) }); } private getBrowserAutomationType(mode: AutomationMode): string { switch (mode) { case 'production': case 'development': return 'PlaywrightAutomationAdapter'; case 'test': default: return 'MockBrowserAutomationAdapter'; } } public static getInstance(): DIContainer { if (!DIContainer.instance) { DIContainer.instance = new DIContainer(); } return DIContainer.instance; } public getStartAutomationUseCase(): StartAutomationSessionUseCase { return this.startAutomationUseCase; } public getSessionRepository(): ISessionRepository { return this.sessionRepository; } public getAutomationEngine(): IAutomationEngine { return this.automationEngine; } public getAutomationMode(): AutomationMode { return this.automationMode; } public getBrowserAutomation(): IScreenAutomation { return this.browserAutomation; } public getLogger(): ILogger { return this.logger; } public getCheckAuthenticationUseCase(): CheckAuthenticationUseCase | null { return this.checkAuthenticationUseCase; } public getInitiateLoginUseCase(): InitiateLoginUseCase | null { return this.initiateLoginUseCase; } public getClearSessionUseCase(): ClearSessionUseCase | null { return this.clearSessionUseCase; } public getAuthenticationService(): IAuthenticationService | null { if (this.browserAutomation instanceof PlaywrightAutomationAdapter) { return this.browserAutomation as IAuthenticationService; } return null; } public setConfirmCheckoutUseCase( checkoutConfirmationPort: ICheckoutConfirmationPort ): void { // Create ConfirmCheckoutUseCase with checkout service from browser automation // and the provided confirmation port this.confirmCheckoutUseCase = new ConfirmCheckoutUseCase( this.browserAutomation as any, // implements ICheckoutService checkoutConfirmationPort ); } public getConfirmCheckoutUseCase(): ConfirmCheckoutUseCase | null { return this.confirmCheckoutUseCase; } /** * Initialize automation connection based on mode. * In production/development mode, connects via Playwright browser automation. * In test mode, returns success immediately (no connection needed). */ public async initializeBrowserConnection(): Promise { this.logger.info('Initializing automation connection', { mode: this.automationMode }); if (this.automationMode === 'production' || this.automationMode === 'development') { try { const playwrightAdapter = this.browserAutomation as PlaywrightAutomationAdapter; const result = await playwrightAdapter.connect(); if (!result.success) { this.logger.error('Automation connection failed', new Error(result.error || 'Unknown error'), { mode: this.automationMode }); return { success: false, error: result.error }; } this.logger.info('Automation connection established', { mode: this.automationMode, adapter: 'Playwright' }); return { success: true }; } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Failed to initialize Playwright'; this.logger.error('Automation connection failed', error instanceof Error ? error : new Error(errorMsg), { mode: this.automationMode }); return { success: false, error: errorMsg }; } } this.logger.debug('Test mode - no automation connection needed'); return { success: true }; } /** * Shutdown the container and cleanup resources. * Should be called when the application is closing. */ public async shutdown(): Promise { this.logger.info('DIContainer shutting down'); if (this.browserAutomation && 'disconnect' in this.browserAutomation) { try { await (this.browserAutomation as PlaywrightAutomationAdapter).disconnect(); this.logger.info('Automation adapter disconnected'); } catch (error) { this.logger.error('Error disconnecting automation adapter', error instanceof Error ? error : new Error('Unknown error')); } } this.logger.info('DIContainer shutdown complete'); } /** * Get the browser mode configuration loader. * Provides access to runtime browser mode control (headed/headless). */ public getBrowserModeConfigLoader(): BrowserModeConfigLoader { return this.browserModeConfigLoader; } /** * Reset the singleton instance (useful for testing with different configurations). */ public static resetInstance(): void { DIContainer.instance = undefined as unknown as DIContainer; } }