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 Amadeus API calls and logging usage data.
 * 
 * This aspect captures all outgoing Amadeus 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: 1.1, 1.2, 1.3, 1.4, 1.5, 5.2
 */
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class AmadeusUsageAspect {
    
    private final UsageLoggerService usageLoggerService;
    
    /**
     * Intercepts all Amadeus API client method calls.
     * 
     * This pointcut matches all methods in the com.flightticket.client.amadeus package
     * and its sub-packages, ensuring comprehensive coverage of all Amadeus API interactions.
     * 
     * The interceptor:
     * 1. Extracts client ID from Spring Security context
     * 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
     */
    @Around("execution(* com.flightticket.client.amadeus..*(..))")
    public Object interceptAmadeusCall(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("AmadeusUsageAspect 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("AmadeusUsageAspect added {}ms latency (exceeds 10ms threshold)", totalLatency);
        } else {
            log.debug("AmadeusUsageAspect 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: 1.2 - Extract Client_ID from security context
     */
    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 Amadeus client method names to RequestType enum values:
     * - searchFlights -> FLIGHT_SEARCH
     * - getFareRules -> PRICE_CHECK
     * - createBooking -> PNR_CREATION
     * - issueTicket -> PNR_CREATION (ticket issuance is part of booking process)
     * - voidTicket -> BOOKING_CANCELLATION
     * - refundTicket -> BOOKING_CANCELLATION
     * - getQueueMessages -> BOOKING_RETRIEVAL
     * 
     * If the method name doesn't match any known pattern, FLIGHT_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: 1.3 - Identify Request_Type from endpoint
     */
    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 "searchFlights" -> RequestType.FLIGHT_SEARCH;
            case "getFareRules" -> RequestType.PRICE_CHECK;
            case "createBooking" -> RequestType.PNR_CREATION;
            case "issueTicket" -> RequestType.PNR_CREATION; // Ticket issuance is part of booking
            case "voidTicket", "refundTicket" -> RequestType.BOOKING_CANCELLATION;
            case "getQueueMessages" -> RequestType.BOOKING_RETRIEVAL;
            default -> {
                log.warn("Unknown Amadeus method name: {}. Defaulting to FLIGHT_SEARCH", methodName);
                yield RequestType.FLIGHT_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: 1.5 - Extract Transaction_ID from method arguments
     */
    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
            // This is a simple implementation - can be enhanced based on actual request structures
            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:
     * - 1.5: Store usage data to Usage_Ledger
     * - 1.6: Execute logging asynchronously
     * - 5.2: Add minimal latency (< 10ms)
     */
    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);
        }
    }
}
