package com.flightticket.aspect;

import com.flightticket.dto.billing.UsageLogRequest;
import com.flightticket.model.enums.RequestStatus;
import com.flightticket.model.enums.RequestType;
import com.flightticket.service.UsageLoggerService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.UUID;

/**
 * AOP Aspect for intercepting Expedia API calls and logging usage data.
 * 
 * This aspect captures all outgoing Expedia API calls, extracts relevant metadata
 * (client ID, request type, transaction ID, response status), and asynchronously
 * logs the usage data for billing purposes.
 * 
 * The interceptor is designed to add minimal latency (< 10ms) to API calls by:
 * - Performing lightweight metadata extraction
 * - Delegating actual logging to an asynchronous service
 * - Using efficient exception handling
 * 
 * Requirements: 10.1, 10.2, 10.3, 10.5, 13.4
 */
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class ExpediaUsageAspect {
    
    private final UsageLoggerService usageLoggerService;
    
    /**
     * Intercepts all Expedia API client method calls.
     * 
     * This pointcut matches all methods in the com.flightticket.client.expedia package
     * and its sub-packages, ensuring comprehensive coverage of all Expedia API interactions.
     * 
     * The interceptor:
     * 1. Extracts client ID from Spring Security context (JWT agency_id claim)
     * 2. Determines request type from method signature
     * 3. Extracts transaction ID from method arguments (if present)
     * 4. Executes the actual API call
     * 5. Captures response status (SUCCESS or FAILURE)
     * 6. Asynchronously logs usage data
     * 
     * @param joinPoint The join point representing the intercepted method call
     * @return The result of the actual API call
     * @throws Throwable Any exception thrown by the API call is propagated
     * 
     * Requirements: 10.1, 10.2, 10.3, 10.5, 13.4
     */
    @Around("execution(* com.flightticket.client.expedia..*(..))")
    public Object interceptExpediaCall(ProceedingJoinPoint joinPoint) throws Throwable {
        // Start timing to ensure minimal latency
        long startTime = System.nanoTime();
        
        // Extract metadata before API call
        UUID clientId = extractClientId();
        RequestType requestType = extractRequestType(joinPoint);
        String transactionId = extractTransactionId(joinPoint.getArgs());
        Instant timestamp = Instant.now();
        
        // Track metadata extraction time
        long extractionTime = System.nanoTime() - startTime;
        
        RequestStatus status;
        Object result;
        
        try {
            // Execute the actual API call
            result = joinPoint.proceed();
            status = RequestStatus.SUCCESS;
            
        } catch (Throwable throwable) {
            // Capture failure status
            status = RequestStatus.FAILURE;
            
            // Log usage even for failed calls
            logUsageAsync(clientId, requestType, transactionId, status, timestamp);
            
            // Calculate and log total latency
            long totalLatency = (System.nanoTime() - startTime) / 1_000_000; // Convert to milliseconds
            if (totalLatency > 10) {
                log.warn("ExpediaUsageAspect added {}ms latency (exceeds 10ms threshold)", totalLatency);
            }
            
            // Re-throw the exception to maintain original behavior
            throw throwable;
        }
        
        // Log usage for successful calls
        logUsageAsync(clientId, requestType, transactionId, status, timestamp);
        
        // Calculate and log total latency
        long totalLatency = (System.nanoTime() - startTime) / 1_000_000; // Convert to milliseconds
        if (totalLatency > 10) {
            log.warn("ExpediaUsageAspect added {}ms latency (exceeds 10ms threshold)", totalLatency);
        } else {
            log.debug("ExpediaUsageAspect added {}ms latency (extraction: {}ms)", 
                totalLatency, extractionTime / 1_000_000);
        }
        
        return result;
    }
    
    /**
     * Extracts the client ID from the Spring Security context.
     * 
     * The client ID is retrieved from the JWT token's "agency_id" claim.
     * If the security context is not available or the claim is missing,
     * null is returned and a warning is logged.
     * 
     * @return The client UUID, or null if not available
     * 
     * Requirement: 10.1 - Extract Client_ID from security context (JWT agency_id claim)
     */
    private UUID extractClientId() {
        try {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            
            if (authentication != null && authentication.getPrincipal() instanceof Jwt) {
                Jwt jwt = (Jwt) authentication.getPrincipal();
                String agencyId = jwt.getClaimAsString("agency_id");
                
                if (agencyId != null) {
                    return UUID.fromString(agencyId);
                } else {
                    log.warn("JWT token does not contain agency_id claim");
                }
            } else {
                log.warn("No JWT authentication found in security context");
            }
        } catch (Exception e) {
            log.error("Error extracting client ID from security context: {}", e.getMessage(), e);
        }
        
        return null;
    }
    
    /**
     * Determines the request type from the method signature.
     * 
     * Maps Expedia client method names to RequestType enum values:
     * - searchAvailability -> ACCOMMODATION_SEARCH
     * - priceCheck -> PRICE_CHECK
     * - createHoldBooking -> BOOKING_CREATION
     * - resumeBooking -> BOOKING_CREATION
     * - cancelHoldBooking -> BOOKING_CANCELLATION
     * 
     * If the method name doesn't match any known pattern, ACCOMMODATION_SEARCH is used
     * as a default and a warning is logged.
     * 
     * @param joinPoint The join point containing method signature information
     * @return The corresponding RequestType enum value
     * 
     * Requirement: 10.1 - Extract request type from method name
     */
    private RequestType extractRequestType(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String methodName = signature.getName();
        
        // Map method names to RequestType enum
        RequestType requestType = switch (methodName) {
            case "searchAvailability" -> RequestType.ACCOMMODATION_SEARCH;
            case "priceCheck" -> RequestType.PRICE_CHECK;
            case "createHoldBooking", "resumeBooking" -> RequestType.BOOKING_CREATION;
            case "cancelHoldBooking" -> RequestType.BOOKING_CANCELLATION;
            default -> {
                log.warn("Unknown Expedia method name: {}. Defaulting to ACCOMMODATION_SEARCH", methodName);
                yield RequestType.ACCOMMODATION_SEARCH;
            }
        };
        
        log.debug("Mapped method {} to RequestType {}", methodName, requestType);
        return requestType;
    }
    
    /**
     * Extracts the transaction ID from method arguments if present.
     * 
     * The transaction ID is used to correlate API requests with bookings.
     * This method searches through the method arguments for common patterns:
     * - String arguments that look like transaction IDs (UUID format)
     * - Request objects with a transactionId or bookingId field
     * 
     * If no transaction ID is found, null is returned (which is acceptable
     * as transaction ID is optional).
     * 
     * @param args The method arguments
     * @return The transaction ID if found, null otherwise
     * 
     * Requirement: 10.1 - Extract Transaction_ID from method arguments if present
     */
    private String extractTransactionId(Object[] args) {
        if (args == null || args.length == 0) {
            return null;
        }
        
        // Look for String arguments that might be transaction IDs
        for (Object arg : args) {
            if (arg instanceof String) {
                String strArg = (String) arg;
                // Check if it looks like a UUID or transaction ID
                if (strArg.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")) {
                    log.debug("Extracted transaction ID from String argument: {}", strArg);
                    return strArg;
                }
            }
            
            // For request objects, try to extract transaction ID via reflection
            if (arg != null) {
                try {
                    // Try to get transactionId field
                    var field = arg.getClass().getDeclaredField("transactionId");
                    field.setAccessible(true);
                    Object value = field.get(arg);
                    if (value instanceof String) {
                        log.debug("Extracted transaction ID from request object: {}", value);
                        return (String) value;
                    }
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    // Field doesn't exist or not accessible - this is expected for many request types
                    log.trace("No transactionId field found in argument of type {}", arg.getClass().getSimpleName());
                }
                
                try {
                    // Try to get bookingId field as alternative
                    var field = arg.getClass().getDeclaredField("bookingId");
                    field.setAccessible(true);
                    Object value = field.get(arg);
                    if (value instanceof String) {
                        log.debug("Extracted booking ID as transaction ID from request object: {}", value);
                        return (String) value;
                    }
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    // Field doesn't exist or not accessible - this is expected for many request types
                    log.trace("No bookingId field found in argument of type {}", arg.getClass().getSimpleName());
                }
            }
        }
        
        log.debug("No transaction ID found in method arguments");
        return null;
    }
    
    /**
     * Asynchronously logs usage data via the UsageLoggerService.
     * 
     * This method creates a UsageLogRequest and delegates to the async logging service.
     * Any errors during this process are caught and logged to prevent impacting the
     * main API call flow.
     * 
     * @param clientId The client UUID
     * @param requestType The type of API request
     * @param transactionId The transaction ID (may be null)
     * @param status The request status (SUCCESS or FAILURE)
     * @param timestamp The timestamp of the API call
     * 
     * Requirements:
     * - 10.1: Log client ID, request type, and timestamp
     * - 10.2: Log response status (success or failure)
     * - 10.3: Execute logging asynchronously
     * - 10.5: Add less than 10ms latency to API calls
     */
    private void logUsageAsync(UUID clientId, RequestType requestType, String transactionId, 
                                RequestStatus status, Instant timestamp) {
        try {
            UsageLogRequest request = new UsageLogRequest(
                clientId,
                requestType,
                transactionId,
                status,
                timestamp
            );
            
            usageLoggerService.logUsage(request);
            
            log.debug("Initiated async usage logging for client {} with requestType {} and status {}", 
                clientId, requestType, status);
            
        } catch (Exception e) {
            // Catch any exceptions to prevent impacting the API call
            log.error("Error initiating async usage logging: {}", e.getMessage(), e);
        }
    }
}
