import { createReadStream, existsSync } from "node:fs"; import { join } from "node:path"; import { HttpException, HttpStatus, Injectable, Logger, OnModuleInit, } from "@nestjs/common"; import axios, { AxiosRequestConfig } from "axios"; import * as FormData from "form-data"; @Injectable() export class AiService implements OnModuleInit { private readonly logger = new Logger(AiService.name); private apiKey: string; private accessToken: string = null; // These configurations are for authentication and getting the API key. private readonly loginOptions: AxiosRequestConfig = { method: "POST", headers: { "Content-Type": "application/json" }, url: `${process.env.AI_URL_V2}/auth/login`, data: { username: process.env.AI_USERNAME, password: process.env.AI_PASSWORD, }, timeout: 30000, // 30 second timeout }; private get profileOptions(): AxiosRequestConfig { return { method: "GET", url: `${process.env.AI_URL_V2}/auth/profile`, headers: { Authorization: `Bearer ${this.accessToken}`, }, }; } // This getter dynamically creates the base options for the image processing request. private get imageProcessOptions(): AxiosRequestConfig { return { method: "POST", url: `${process.env.AI_URL_V2}/services/car-damage/detector?version=ai-v7`, headers: { Authorization: `Bearer ${this.accessToken}`, "gateway-api-key": `${this.apiKey}`, }, }; } constructor() {} async onModuleInit() { try { const res = await this.login(); if (res?.accessToken) { this.logger.verbose("AI Service Authenticated Successfully."); this.accessToken = res.accessToken; await this.getApiKey(); this.logger.log("AI Service initialized and ready."); } else { this.logger.warn( "AI Service Unavailable: Login did not return an access token. Will retry on first request.", ); } } catch (error) { // Don't prevent app startup if AI service is temporarily unavailable // The service will attempt to re-authenticate when aiRequestImage is called this.logger.warn( "AI Service Unavailable: Failed during initial login. Will retry on first request.", ); this.logger.warn(`Error: ${error.message}`); // Reset tokens so re-authentication will be attempted this.accessToken = null; this.apiKey = null; } } private async login() { try { const loginResponse = await axios.request(this.loginOptions); return loginResponse.data; } catch (err) { const errorMessage = err.response?.data?.message || err.message || "Unknown error"; const statusCode = err.response?.status || 500; this.logger.error(`AI login failed: ${errorMessage} (Status: ${statusCode})`); if (err.response?.data) { this.logger.error(`AI login error details: ${JSON.stringify(err.response.data, null, 2)}`); } throw new HttpException( `Could not authenticate with AI service: ${errorMessage}`, statusCode >= 400 && statusCode < 500 ? statusCode : HttpStatus.UNAUTHORIZED, ); } } private async getApiKey() { try { const profileResponse = await axios.request(this.profileOptions); this.apiKey = profileResponse.data.apiKey.key; this.logger.log("Successfully retrieved AI gateway API key."); return this.apiKey; } catch (err) { this.logger.error("Failed to retrieve AI API key:", err.message); throw new HttpException( "Could not get API key from AI service", HttpStatus.FAILED_DEPENDENCY, ); } } public async aiRequestImage(file: { path: string; fileName?: string }): Promise { // Ensure authentication is set up if (!this.accessToken || !this.apiKey) { this.logger.warn("AI service not authenticated, attempting to re-authenticate..."); try { const res = await this.login(); if (res?.accessToken) { this.accessToken = res.accessToken; await this.getApiKey(); } else { throw new HttpException( "AI Service authentication failed", HttpStatus.UNAUTHORIZED, ); } } catch (error) { this.logger.error("Failed to re-authenticate AI service:", error.message); throw new HttpException( "AI Service authentication failed", HttpStatus.UNAUTHORIZED, ); } } // Resolve relative paths to absolute paths const filePath = file.path.startsWith("/") ? file.path : join(process.cwd(), file.path.replace(/^\.\//, "")); this.logger.log(`Processing AI image request for: ${filePath}`); // Check if file exists if (!existsSync(filePath)) { this.logger.error(`File not found at path: ${filePath}`); throw new HttpException( `File not found: ${file.path}`, HttpStatus.NOT_FOUND, ); } const form = new FormData(); const fileStream = createReadStream(filePath); // Append file with filename if available if (file.fileName) { form.append("images", fileStream, file.fileName); } else { // Extract filename from path if not provided const pathParts = filePath.split("/"); const extractedFileName = pathParts[pathParts.length - 1]; form.append("images", fileStream, extractedFileName); } try { const requestHeaders = { ...this.imageProcessOptions.headers, ...form.getHeaders(), }; this.logger.log(`[STEP 1/4] Sending request to AI service: ${this.imageProcessOptions.url}`); this.logger.log(`[STEP 1/4] File: ${filePath}, Filename: ${file.fileName || 'extracted from path'}`); this.logger.log(`[STEP 1/4] FormData Content-Type: ${form.getHeaders()['content-type']}`); this.logger.log(`[STEP 1/4] Authorization header present: ${!!requestHeaders.Authorization}`); this.logger.log(`[STEP 1/4] Gateway API key present: ${!!this.apiKey}`); this.logger.log(`[STEP 1/4] Request method: POST`); this.logger.log(`[STEP 1/4] FormData field name: "images"`); // Get file stats for debugging const fs = require('fs'); const stats = fs.statSync(filePath); this.logger.log(`[STEP 1/4] File size: ${stats.size} bytes`); this.logger.log(`[STEP 1/4] File exists: ${existsSync(filePath)}`); const response = await axios.request({ ...this.imageProcessOptions, headers: requestHeaders, data: form, maxContentLength: Infinity, maxBodyLength: Infinity, }); this.logger.log(`[STEP 2/4] Successfully received response from AI service (Status: ${response.status})`); // Validate response structure if (!response.data) { this.logger.error(`[ERROR] AI response is empty or missing data`); throw new HttpException( "AI Service returned empty response", HttpStatus.BAD_GATEWAY, ); } // Check for error in response first (AI service returns 201 with error in body) if (response.data.error) { this.logger.error(`[ERROR] AI service returned an error in response body`); this.logger.error(`[ERROR] Error message: ${response.data.error}`); this.logger.error(`[ERROR] Full response: ${JSON.stringify(response.data, null, 2)}`); throw new HttpException( `AI Service error: ${response.data.error}`, HttpStatus.BAD_GATEWAY, ); } // Check for processed image (downloadLink) if (!response.data.downloadLink) { this.logger.error(`[ERROR] AI response missing processed image (downloadLink)`); this.logger.error(`[ERROR] Response structure: ${JSON.stringify(Object.keys(response.data))}`); this.logger.error(`[ERROR] Full response: ${JSON.stringify(response.data, null, 2)}`); throw new HttpException( "AI Service did not return processed image (downloadLink missing)", HttpStatus.BAD_GATEWAY, ); } // Check for reports if (!response.data.reports) { this.logger.warn(`[WARNING] AI response missing reports object, but downloadLink exists`); this.logger.warn(`[WARNING] Response keys: ${JSON.stringify(Object.keys(response.data))}`); } this.logger.log(`[STEP 3/4] Validated AI response - downloadLink: ${response.data.downloadLink ? 'present' : 'missing'}, reports: ${response.data.reports ? 'present' : 'missing'}`); return response.data; } catch (er) { // Determine error source let errorSource = "UNKNOWN"; let errorMessage = er.message; let errorDetails = "No error details available"; if (er.response) { errorSource = "AI_SERVICE_RESPONSE"; errorMessage = er.response?.data?.message || er.message || `HTTP ${er.response.status}`; errorDetails = er.response?.data ? JSON.stringify(er.response.data, null, 2) : `Status: ${er.response.status}, StatusText: ${er.response.statusText}`; } else if (er.request) { errorSource = "NETWORK_ERROR"; errorMessage = "Network error - AI service did not respond"; errorDetails = "Request was made but no response received"; } else { errorSource = "REQUEST_SETUP_ERROR"; errorMessage = er.message || "Error setting up request"; } this.logger.error(`[ERROR] AI request failed - Source: ${errorSource}`); this.logger.error(`[ERROR] File path: ${filePath}`); this.logger.error(`[ERROR] Error message: ${errorMessage}`); this.logger.error(`[ERROR] Error details: ${errorDetails}`); if (er.stack) { this.logger.error(`[ERROR] Stack trace: ${er.stack}`); } // Re-throw with detailed error information throw new HttpException( `[${errorSource}] ${errorMessage}`, er.response?.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } }