def generate_jtl_graph(jtl_file, output_image=None):
    import pandas as pd
    import matplotlib.pyplot as plt
    import seaborn as sns
    import numpy as np
    import os
    # Read JTL as CSV
    df = pd.read_csv(jtl_file)
    # Use 'elapsed' as response time, 'label' as endpoint, 'success', 'responseCode'
    if 'elapsed' not in df.columns:
        print("No 'elapsed' column found in JTL file.")
        return
    response_col = 'elapsed'
    cpu_col = None
    if not output_image:
        output_image = os.path.splitext(jtl_file)[0] + '_analysis.png'
    plt.figure(figsize=(18, 10))
    plt.subplot(2, 2, 1)
    sns.histplot(df[response_col], bins=30, kde=True, color='skyblue')
    plt.title('Response Time Distribution')
    plt.xlabel('Response Time (ms)')
    plt.ylabel('Frequency')
    plt.subplot(2, 2, 2)
    sns.boxplot(x=df[response_col], color='orange')
    plt.title('Response Time Boxplot')
    plt.xlabel('Response Time (ms)')
    plt.subplot(2, 2, 3)
    bins = [0, 100, 200, 300, 400, 500, 1000, 2000, 5000, np.inf]
    labels = ['<100ms', '100-200ms', '200-300ms', '300-400ms', '400-500ms', '500ms-1s', '1-2s', '2-5s', '>5s']
    df['rt_category'] = pd.cut(df[response_col], bins=bins, labels=labels, right=False)
    sns.barplot(x=labels, y=df['rt_category'].value_counts().sort_index().values, palette='Reds')
    plt.title('Response Time Categories')
    plt.xlabel('Category')
    plt.ylabel('Count')
    plt.subplot(2, 2, 4)
    if cpu_col and cpu_col in df.columns:
        sns.scatterplot(x=df[cpu_col], y=df[response_col], alpha=0.6)
        plt.title('CPU Time vs Response Time')
        plt.xlabel('CPU Time (ms)')
        plt.ylabel('Response Time (ms)')
    else:
        sns.violinplot(x=df['label'], y=df[response_col], inner='quartile')
        plt.title('Response Time by Endpoint')
        plt.xlabel('Endpoint')
        plt.ylabel('Response Time (ms)')
        plt.xticks(rotation=45, ha='right')
    plt.tight_layout()
    plt.savefig(output_image, dpi=300)
    print(f"\nGraphs saved as {output_image}")
import subprocess
import os
import re
import json
from pathlib import Path
from mcp.server.fastmcp import FastMCP
import logging
import datetime
import uuid
import csv
import statistics
from collections import defaultdict, Counter
from typing import Dict, List, Any, Optional

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("jmeter_new")

# Initialize MCP server
def create_mcp_server():
    return FastMCP("jmeter_new")

# Utility to generate unique filenames
def unique_name(prefix, ext):
    now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    rand = str(uuid.uuid4())[:8]
    return f"{prefix}_{now}_{rand}.{ext}"

# Advanced Log Parser for multiple formats
class AdvancedLogParser:
    """Universal log parser supporting JTL, CSV, and LOG formats"""
    
    def __init__(self):
        self.supported_formats = ['.jtl', '.csv', '.log', '.txt']
        
    def detect_format(self, file_path: str) -> str:
        """Auto-detect log format based on content and extension"""
        path = Path(file_path)
        extension = path.suffix.lower()
        
        if not path.exists():
            raise FileNotFoundError(f"File not found: {file_path}")
            
        # Read first few lines to analyze structure
        with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
            first_lines = [f.readline().strip() for _ in range(5)]
            
        content = '\n'.join(first_lines)
        
        # Detection logic
        if extension == '.jtl' or 'timeStamp,elapsed,label' in content:
            return 'jmeter_jtl'
        elif 'purePathsData.infoData.serviceName' in content or 'timingData.responseTime' in content:
            # Dynamic naming for distributed trace CSV files
            return 'distributed_traces_csv'
        elif ',' in content and any(header in content.lower() for header in ['timestamp', 'response_time', 'duration', 'elapsed']):
            return 'performance_csv'
        # Apache access log detection (improved)
        elif self._is_apache_access_log(content):
            return 'apache_access_log'
        elif any(pattern in content for pattern in ['ERROR', 'WARN', 'INFO', 'DEBUG']) or extension == '.log':
            return 'application_log'
        elif extension == '.csv':
            return 'generic_csv'
        else:
            return 'text_log'
    
    def _is_apache_access_log(self, content: str) -> bool:
        """Detect Apache access log format"""
        # Common Apache log patterns
        apache_patterns = [
            # Common Log Format: IP - - [timestamp] "method URI protocol" status size
            r'\d+\.\d+\.\d+\.\d+ - - \[\d+/\w+/\d+:\d+:\d+:\d+ \+\d+\] "[\w]+ /.*?" \d+ \d+',
            # Extended Log Format (with user agent, referer, etc.)
            r'\d+\.\d+\.\d+\.\d+ - - \[\d+/\w+/\d+:\d+:\d+:\d+ \+\d+\] "[^"]*" \d+ \d+ \d+',
            # Windchill specific pattern
            r'\d+\.\d+\.\d+\.\d+ - - \[\d+/\w+/\d+:\d+:\d+:\d+ \+\d+\] "GET /Windchill'
        ]
        
        for pattern in apache_patterns:
            if re.search(pattern, content):
                return True
        return False
    
    def parse_apache_access_log(self, file_path: str) -> Dict[str, Any]:
        """Parse Apache access log files with comprehensive analysis"""
        logger.info(f"Parsing Apache access log file: {file_path}")
        
        log_entries = []
        ip_addresses = Counter()
        status_codes = Counter()
        http_methods = Counter()
        urls = Counter()
        response_times = []
        response_sizes = []
        user_agents = Counter()
        error_entries = []
        time_patterns = []
        
        # Apache log regex patterns
        # Common Log Format: IP - - [timestamp] "method URI protocol" status size response_time
        common_pattern = r'(\d+\.\d+\.\d+\.\d+) - - \[([^\]]+)\] "(\w+) ([^"]*)" (\d+) (\d+)(?: (\d+))?'
        
        with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
            for line_num, line in enumerate(f, 1):
                line = line.strip()
                if not line:
                    continue
                
                match = re.match(common_pattern, line)
                if match:
                    ip, timestamp, method, uri, status, size, response_time = match.groups()
                    
                    # Extract data
                    ip_addresses[ip] += 1
                    status_codes[int(status)] += 1
                    http_methods[method] += 1
                    
                    # Clean and categorize URLs
                    clean_uri = uri.split('?')[0] if '?' in uri else uri  # Remove query params
                    urls[clean_uri] += 1
                    
                    # Response time (microseconds to milliseconds)
                    if response_time:
                        rt_ms = float(response_time) / 1000  # Convert microseconds to ms
                        response_times.append(rt_ms)
                    
                    # Response size
                    if size and size != '-':
                        response_sizes.append(int(size))
                    
                    # Parse timestamp for temporal analysis
                    try:
                        # Apache timestamp format: 17/Aug/2025:00:00:25 +0100
                        time_obj = datetime.datetime.strptime(timestamp.split(' ')[0], '%d/%b/%Y:%H:%M:%S')
                        time_patterns.append(time_obj)
                    except ValueError:
                        pass
                    
                    # Identify errors
                    status_int = int(status)
                    if status_int >= 400:
                        error_entries.append({
                            'line_number': line_num,
                            'ip': ip,
                            'method': method,
                            'uri': uri,
                            'status': status_int,
                            'timestamp': timestamp,
                            'size': size
                        })
                    
                    log_entries.append({
                        'ip': ip,
                        'timestamp': timestamp,
                        'method': method,
                        'uri': uri,
                        'status': status_int,
                        'size': int(size) if size != '-' else 0,
                        'response_time': rt_ms if response_time else 0
                    })
        
        total_requests = len(log_entries)
        total_errors = len(error_entries)
        error_rate = (total_errors / total_requests * 100) if total_requests > 0 else 0
        
        # Calculate response time statistics
        response_time_stats = self._calculate_response_time_stats(response_times) if response_times else {}
        
        # Traffic analysis
        unique_ips = len(ip_addresses)
        time_span = None
        requests_per_hour = 0
        if time_patterns and len(time_patterns) > 1:
            time_span = (max(time_patterns) - min(time_patterns)).total_seconds() / 3600  # hours
            requests_per_hour = total_requests / time_span if time_span > 0 else 0
        
        # Security analysis
        security_analysis = self._analyze_security_patterns(log_entries, error_entries)
        
        # URL analysis
        url_analysis = self._analyze_url_patterns(urls, total_requests)
        
        # IP analysis
        ip_analysis = self._analyze_ip_patterns(ip_addresses, log_entries)
        
        # Temporal analysis
        temporal_analysis = self._analyze_apache_temporal_patterns(time_patterns, log_entries)
        
        return {
            "format": "apache_access_log",
            "total_requests": total_requests,
            "total_errors": total_errors,
            "error_rate": error_rate,
            "unique_ips": unique_ips,
            "time_span_hours": time_span,
            "requests_per_hour": requests_per_hour,
            "response_time_stats": response_time_stats,
            "status_code_distribution": dict(status_codes),
            "http_method_distribution": dict(http_methods),
            "top_ips": dict(ip_addresses.most_common(10)),
            "top_urls": dict(urls.most_common(20)),
            "security_analysis": security_analysis,
            "url_analysis": url_analysis,
            "ip_analysis": ip_analysis,
            "temporal_analysis": temporal_analysis,
            "error_details": {
                "total_errors": total_errors,
                "error_breakdown": self._categorize_http_errors(status_codes),
                "sample_errors": error_entries[:50]  # First 50 errors
            },
            "insights": self._generate_apache_insights(
                error_rate, security_analysis, response_time_stats, 
                ip_analysis, url_analysis, requests_per_hour
            )
        }
    
    def parse_jmeter_jtl(self, file_path: str) -> Dict[str, Any]:
        """Parse JMeter JTL format"""
        logger.info(f"Parsing JMeter JTL file: {file_path}")
        
        with open(file_path, 'r') as f:
            reader = csv.DictReader(f)
            samples = list(reader)
        
        if not samples:
            return {"error": "No data found in JTL file"}
            
        # Extract metrics
        total = len(samples)
        errors = sum(1 for s in samples if s.get('success', 'true').lower() == 'false')
        elapsed = [float(s.get('elapsed', 0)) for s in samples if s.get('elapsed')]
        
        # Calculate statistics
        stats = self._calculate_response_time_stats(elapsed)
        
        # Throughput analysis
        timestamps = [float(s.get('timeStamp', 0))/1000 for s in samples if s.get('timeStamp')]
        duration = (max(timestamps) - min(timestamps)) if len(timestamps) > 1 else 1
        throughput = total / duration if duration > 0 else 0
        
        # Endpoint analysis
        endpoint_analysis = self._analyze_endpoints_jtl(samples)
        
        # Error analysis
        error_analysis = self._analyze_errors_jtl(samples)
        
        return {
            "format": "jmeter_jtl",
            "total_samples": total,
            "error_count": errors,
            "error_rate": (errors/total)*100 if total > 0 else 0,
            "response_time_stats": stats,
            "throughput": throughput,
            "duration": duration,
            "endpoint_analysis": endpoint_analysis,
            "error_analysis": error_analysis,
            "insights": self._generate_jmeter_insights(stats, endpoint_analysis, error_analysis)
        }
    
    def parse_performance_csv(self, file_path: str) -> Dict[str, Any]:
        """Parse performance monitoring CSV (like EasyTravel traces)"""
        logger.info(f"Parsing performance CSV file: {file_path}")
        
        with open(file_path, 'r') as f:
            reader = csv.DictReader(f)
            records = list(reader)
        
        if not records:
            return {"error": "No data found in CSV file"}
        
        # Auto-detect time and duration columns
        headers = list(records[0].keys())
        time_col = self._find_column(headers, ['timestamp', 'time', 'start_time', 'date'])
        duration_col = self._find_column(headers, ['duration', 'response_time', 'elapsed', 'execution_time'])
        status_col = self._find_column(headers, ['status', 'success', 'result', 'error'])
        service_col = self._find_column(headers, ['service', 'endpoint', 'operation', 'method'])
        
        # Extract performance data
        durations = []
        services = []
        statuses = []
        
        for record in records:
            if duration_col and record.get(duration_col):
                try:
                    # Handle different duration formats (ms, s, ns)
                    duration = float(record[duration_col])
                    if duration > 100000:  # Likely nanoseconds
                        duration = duration / 1000000  # Convert to ms
                    elif duration < 100:  # Likely seconds
                        duration = duration * 1000  # Convert to ms
                    durations.append(duration)
                except ValueError:
                    continue
            
            if service_col:
                services.append(record.get(service_col, 'Unknown'))
            if status_col:
                statuses.append(record.get(status_col, 'Unknown'))
        
        # Calculate statistics
        stats = self._calculate_response_time_stats(durations)
        
        # Service analysis
        service_analysis = self._analyze_services(records, service_col, duration_col, status_col)
        
        # Time-based analysis
        temporal_analysis = self._analyze_temporal_patterns(records, time_col, duration_col)
        
        return {
            "format": "performance_csv",
            "total_records": len(records),
            "response_time_stats": stats,
            "service_analysis": service_analysis,
            "temporal_analysis": temporal_analysis,
            "detected_columns": {
                "timestamp": time_col,
                "duration": duration_col,
                "status": status_col,
                "service": service_col
            },
            "insights": self._generate_csv_insights(stats, service_analysis, temporal_analysis)
        }
    
    def parse_distributed_traces_csv(self, file_path: str) -> Dict[str, Any]:
        """Parse distributed traces CSV format (EasyTravel, Dynatrace, APM tools, etc.)"""
        logger.info(f"Parsing distributed traces CSV file: {file_path}")
        
        with open(file_path, 'r') as f:
            reader = csv.DictReader(f)
            records = list(reader)
        
        if not records:
            return {"error": "No data found in distributed traces CSV file"}
        
        # Get headers to detect the actual format dynamically
        headers = list(records[0].keys())
        
        # Detect service source (EasyTravel, Dynatrace, etc.)
        format_source = "unknown"
        if any("purePathsData" in h for h in headers):
            format_source = "dynatrace_easytravel"
        elif any("service" in h.lower() for h in headers):
            format_source = "generic_apm"
        elif any("span" in h.lower() for h in headers):
            format_source = "opentelemetry"
        
        # Extract distributed traces data
        total_records = len(records)
        errors = []
        services = []
        urls = []
        response_codes = []
        response_times = []
        timestamps = []
        
        # Dynamic field mapping based on detected format
        service_field = self._find_service_field(headers)
        response_time_field = self._find_response_time_field(headers)
        url_field = self._find_url_field(headers)
        error_field = self._find_error_field(headers)
        timestamp_field = self._find_timestamp_field(headers)
        http_code_field = self._find_http_code_field(headers)
        
        # Process each record with dynamic field mapping
        for record in records:
            # Extract response time
            if response_time_field and record.get(response_time_field):
                try:
                    rt = float(record[response_time_field])
                    # Handle different time units
                    if rt > 1000000:  # Likely microseconds
                        rt = rt / 1000  # Convert to ms
                    elif rt < 1:  # Likely seconds
                        rt = rt * 1000  # Convert to ms
                    response_times.append(rt)
                except ValueError:
                    pass
            
            # Extract service name
            service_name = record.get(service_field, 'Unknown') if service_field else 'Unknown'
            services.append(service_name)
            
            # Extract URL/URI
            url = record.get(url_field, 'Unknown') if url_field else 'Unknown'
            urls.append(url)
            
            # Extract HTTP response code
            if http_code_field and record.get(http_code_field):
                response_codes.append(record[http_code_field])
            
            # Extract error/failed status
            if error_field:
                error_status = record.get(error_field, 'false')
                if error_status.lower() in ['true', 'error', 'failed', '1']:
                    errors.append({
                        'service': service_name,
                        'url': url,
                        'http_code': record.get(http_code_field, 'Unknown'),
                        'trace_id': record.get('traceId', record.get('trace_id', 'Unknown'))
                    })
            
            # Extract timestamp
            if timestamp_field and record.get(timestamp_field):
                try:
                    ts = float(record[timestamp_field])
                    # Handle different timestamp formats
                    if ts > 1e12:  # Likely milliseconds
                        ts = ts / 1000
                    timestamps.append(ts)
                except ValueError:
                    pass
        
        # Calculate response time statistics
        response_time_stats = self._calculate_response_time_stats(response_times)
        
        # Analyze services
        service_counter = Counter(services)
        unique_services = list(service_counter.keys())
        
        # Analyze URLs
        url_counter = Counter(urls)
        unique_urls = list(url_counter.keys())
        
        # Analyze response codes
        response_code_counter = Counter(response_codes)
        
        # Calculate hits per hour
        if len(timestamps) > 1:
            time_span_hours = (max(timestamps) - min(timestamps)) / 3600
            hits_per_hour = total_records / time_span_hours if time_span_hours > 0 else total_records
        else:
            hits_per_hour = 0
        
        # Error analysis
        error_count = len(errors)
        error_rate = (error_count / total_records * 100) if total_records > 0 else 0
        
        # Service performance analysis
        service_performance = {}
        for service in unique_services:
            service_records = [r for r in records if r.get(service_field) == service]
            service_response_times = []
            for sr in service_records:
                if response_time_field and sr.get(response_time_field):
                    try:
                        rt = float(sr[response_time_field])
                        if rt > 1000000:  # Microseconds to ms
                            rt = rt / 1000
                        elif rt < 1:  # Seconds to ms
                            rt = rt * 1000
                        service_response_times.append(rt)
                    except ValueError:
                        pass
            
            if service_response_times:
                service_performance[service] = {
                    'request_count': len(service_records),
                    'avg_response_time': statistics.mean(service_response_times),
                    'min_response_time': min(service_response_times),
                    'max_response_time': max(service_response_times),
                    'p95_response_time': statistics.quantiles(service_response_times, n=20)[18] if len(service_response_times) > 1 else service_response_times[0]
                }
        
        return {
            "format": f"distributed_traces_csv_{format_source}",
            "detected_source": format_source,
            "field_mappings": {
                "service_field": service_field,
                "response_time_field": response_time_field,
                "url_field": url_field,
                "error_field": error_field,
                "timestamp_field": timestamp_field,
                "http_code_field": http_code_field
            },
            "total_records": total_records,
            "error_details": {
                "error_count": error_count,
                "error_rate": error_rate,
                "failed_requests": errors
            },
            "service_analysis": {
                "unique_services": unique_services,
                "service_distribution": dict(service_counter),
                "service_performance": service_performance
            },
            "url_analysis": {
                "unique_urls": unique_urls,
                "url_distribution": dict(url_counter)
            },
            "response_code_analysis": {
                "code_distribution": dict(response_code_counter),
                "success_rate": ((total_records - error_count) / total_records * 100) if total_records > 0 else 0
            },
            "response_time_stats": response_time_stats,
            "traffic_analysis": {
                "hits_per_hour": hits_per_hour,
                "time_span_hours": (max(timestamps) - min(timestamps)) / 3600 if len(timestamps) > 1 else 0,
                "total_requests": total_records
            },
            "insights": self._generate_distributed_traces_insights(
                error_rate, service_performance, response_time_stats, hits_per_hour, format_source
            )
        }
    
    def parse_application_log(self, file_path: str) -> Dict[str, Any]:
        """Parse application log files"""
        logger.info(f"Parsing application log file: {file_path}")
        
        log_entries = []
        error_patterns = []
        performance_data = []
        
        # Common log patterns
        timestamp_patterns = [
            r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}',
            r'\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}',
            r'\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\]'
        ]
        
        level_pattern = r'\b(ERROR|WARN|INFO|DEBUG|TRACE|FATAL)\b'
        response_time_pattern = r'(\d+\.?\d*)\s*(ms|milliseconds|seconds|s)\b'
        
        with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
            for line_num, line in enumerate(f, 1):
                line = line.strip()
                if not line:
                    continue
                
                # Extract timestamp
                timestamp = None
                for pattern in timestamp_patterns:
                    match = re.search(pattern, line)
                    if match:
                        timestamp = match.group()
                        break
                
                # Extract log level
                level_match = re.search(level_pattern, line)
                level = level_match.group() if level_match else 'UNKNOWN'
                
                # Extract response times
                rt_matches = re.findall(response_time_pattern, line, re.IGNORECASE)
                for value, unit in rt_matches:
                    rt_ms = float(value)
                    if unit.lower() in ['s', 'seconds']:
                        rt_ms *= 1000
                    performance_data.append(rt_ms)
                
                # Categorize errors and warnings
                if level in ['ERROR', 'WARN', 'FATAL']:
                    error_patterns.append({
                        'line_number': line_num,
                        'level': level,
                        'message': line[:200],  # Truncate long messages
                        'timestamp': timestamp
                    })
                
                log_entries.append({
                    'line_number': line_num,
                    'level': level,
                    'timestamp': timestamp,
                    'message': line[:100]  # Store truncated message
                })
        
        # Analyze log patterns
        level_counts = Counter(entry['level'] for entry in log_entries)
        error_analysis = self._analyze_log_errors(error_patterns)
        
        # Performance statistics from extracted response times
        perf_stats = self._calculate_response_time_stats(performance_data) if performance_data else {}
        
        return {
            "format": "application_log",
            "total_lines": len(log_entries),
            "level_distribution": dict(level_counts),
            "error_count": len(error_patterns),
            "performance_data_points": len(performance_data),
            "performance_stats": perf_stats,
            "error_analysis": error_analysis,
            "insights": self._generate_log_insights(level_counts, error_analysis, perf_stats)
        }
    
    def parse_generic_csv(self, file_path: str) -> Dict[str, Any]:
        """Parse generic CSV files with basic analysis"""
        logger.info(f"Parsing generic CSV file: {file_path}")
        
        with open(file_path, 'r') as f:
            reader = csv.DictReader(f)
            records = list(reader)
        
        if not records:
            return {"error": "No data found in CSV file"}
        
        headers = list(records[0].keys())
        
        # Basic statistics
        numeric_columns = []
        for header in headers:
            try:
                # Test if column contains numeric data
                sample_values = [float(record[header]) for record in records[:10] if record[header]]
                if len(sample_values) >= 5:  # At least 5 numeric values
                    numeric_columns.append(header)
            except (ValueError, TypeError):
                continue
        
        # Calculate statistics for numeric columns
        column_stats = {}
        for col in numeric_columns:
            values = []
            for record in records:
                try:
                    values.append(float(record[col]))
                except (ValueError, TypeError):
                    continue
            
            if values:
                column_stats[col] = self._calculate_basic_stats(values)
        
        return {
            "format": "generic_csv",
            "total_records": len(records),
            "headers": headers,
            "numeric_columns": numeric_columns,
            "column_statistics": column_stats,
            "insights": self._generate_generic_insights(column_stats, len(records))
        }
    
    def _find_column(self, headers: List[str], candidates: List[str]) -> Optional[str]:
        """Find column name matching candidates"""
        headers_lower = [h.lower() for h in headers]
        for candidate in candidates:
            for i, header in enumerate(headers_lower):
                if candidate in header:
                    return headers[i]
        return None
    
    def _find_service_field(self, headers: List[str]) -> Optional[str]:
        """Dynamically find service field in headers"""
        service_candidates = [
            'purePathsData.infoData.serviceName',  # EasyTravel/Dynatrace
            'service_name', 'serviceName', 'service',
            'component', 'application', 'app_name'
        ]
        return self._find_column(headers, service_candidates)
    
    def _find_response_time_field(self, headers: List[str]) -> Optional[str]:
        """Dynamically find response time field in headers"""
        response_time_candidates = [
            'timingData.responseTime',  # EasyTravel/Dynatrace
            'response_time', 'responseTime', 'duration',
            'elapsed', 'latency', 'execution_time'
        ]
        return self._find_column(headers, response_time_candidates)
    
    def _find_url_field(self, headers: List[str]) -> Optional[str]:
        """Dynamically find URL field in headers"""
        url_candidates = [
            'purePathsData.infoData.callURI',  # EasyTravel/Dynatrace
            'url', 'uri', 'endpoint', 'path', 'request_url'
        ]
        return self._find_column(headers, url_candidates)
    
    def _find_error_field(self, headers: List[str]) -> Optional[str]:
        """Dynamically find error/failed field in headers"""
        error_candidates = [
            'purePathsData.infoData.failed',  # EasyTravel/Dynatrace
            'failed', 'error', 'success', 'status'
        ]
        return self._find_column(headers, error_candidates)
    
    def _find_timestamp_field(self, headers: List[str]) -> Optional[str]:
        """Dynamically find timestamp field in headers"""
        timestamp_candidates = [
            'purePathsData.infoData.callStartTime',  # EasyTravel/Dynatrace
            'timestamp', 'start_time', 'time', 'datetime'
        ]
        return self._find_column(headers, timestamp_candidates)
    
    def _find_http_code_field(self, headers: List[str]) -> Optional[str]:
        """Dynamically find HTTP code field in headers"""
        http_code_candidates = [
            'purePathsData.requestData.httpCode',  # EasyTravel/Dynatrace
            'http_code', 'status_code', 'response_code'
        ]
        return self._find_column(headers, http_code_candidates)
    
    def _calculate_response_time_stats(self, times: List[float]) -> Dict[str, float]:
        """Calculate comprehensive response time statistics"""
        if not times:
            return {}
        
        times_sorted = sorted(times)
        n = len(times)
        
        stats = {
            "count": n,
            "min": min(times),
            "max": max(times),
            "mean": statistics.mean(times),
            "median": statistics.median(times),
            "std_dev": statistics.stdev(times) if n > 1 else 0
        }
        
        # Percentiles
        if n >= 100:
            percentiles = statistics.quantiles(times, n=100)
            stats.update({
                "p50": percentiles[49],
                "p90": percentiles[89],
                "p95": percentiles[94],
                "p99": percentiles[98]
            })
        else:
            # Approximate percentiles for smaller datasets
            stats.update({
                "p50": times_sorted[n//2],
                "p90": times_sorted[int(n*0.9)] if n > 10 else times_sorted[-1],
                "p95": times_sorted[int(n*0.95)] if n > 20 else times_sorted[-1],
                "p99": times_sorted[int(n*0.99)] if n > 100 else times_sorted[-1]
            })
        
        return stats
    
    def _calculate_basic_stats(self, values: List[float]) -> Dict[str, float]:
        """Calculate basic statistics for any numeric column"""
        if not values:
            return {}
        
        return {
            "count": len(values),
            "min": min(values),
            "max": max(values),
            "mean": statistics.mean(values),
            "median": statistics.median(values),
            "std_dev": statistics.stdev(values) if len(values) > 1 else 0
        }
    
    def _analyze_endpoints_jtl(self, samples: List[Dict]) -> Dict[str, Any]:
        """Analyze JMeter endpoints performance"""
        endpoint_data = defaultdict(list)
        endpoint_errors = defaultdict(int)
        
        for sample in samples:
            label = sample.get('label', 'Unknown')
            elapsed = float(sample.get('elapsed', 0))
            success = sample.get('success', 'true').lower() == 'true'
            
            endpoint_data[label].append(elapsed)
            if not success:
                endpoint_errors[label] += 1
        
        endpoint_stats = {}
        for endpoint, times in endpoint_data.items():
            stats = self._calculate_response_time_stats(times)
            stats['error_count'] = endpoint_errors[endpoint]
            stats['error_rate'] = (endpoint_errors[endpoint] / len(times)) * 100
            endpoint_stats[endpoint] = stats
        
        return endpoint_stats
    
    def _analyze_errors_jtl(self, samples: List[Dict]) -> Dict[str, Any]:
        """Analyze JMeter error patterns"""
        error_messages = []
        error_codes = Counter()
        
        for sample in samples:
            if sample.get('success', 'true').lower() == 'false':
                response_code = sample.get('responseCode', 'Unknown')
                response_message = sample.get('responseMessage', 'Unknown')
                error_codes[response_code] += 1
                error_messages.append({
                    'code': response_code,
                    'message': response_message,
                    'label': sample.get('label', 'Unknown')
                })
        
        return {
            "error_codes": dict(error_codes),
            "error_messages": error_messages[:10],  # Top 10 error messages
            "total_errors": len(error_messages)
        }
    
    def _analyze_services(self, records: List[Dict], service_col: str, duration_col: str, status_col: str) -> Dict[str, Any]:
        """Analyze service performance from CSV data"""
        if not service_col:
            return {}
        
        service_data = defaultdict(list)
        service_errors = defaultdict(int)
        
        for record in records:
            service = record.get(service_col, 'Unknown')
            
            if duration_col and record.get(duration_col):
                try:
                    duration = float(record[duration_col])
                    service_data[service].append(duration)
                except ValueError:
                    continue
            
            if status_col:
                status = record.get(status_col, '').lower()
                if 'error' in status or 'fail' in status or status == 'false':
                    service_errors[service] += 1
        
        service_stats = {}
        for service, durations in service_data.items():
            if durations:
                stats = self._calculate_response_time_stats(durations)
                stats['error_count'] = service_errors[service]
                service_stats[service] = stats
        
        return service_stats
    
    def _analyze_temporal_patterns(self, records: List[Dict], time_col: str, duration_col: str) -> Dict[str, Any]:
        """Analyze temporal patterns in performance data"""
        if not time_col or not duration_col:
            return {}
        
        # This is a simplified temporal analysis
        # In a real implementation, you'd parse timestamps and create time series
        return {
            "note": "Temporal analysis requires timestamp parsing implementation",
            "total_timespan": "Analysis not implemented",
            "peak_periods": []
        }
    
    def _analyze_log_errors(self, error_patterns: List[Dict]) -> Dict[str, Any]:
        """Analyze error patterns in logs"""
        if not error_patterns:
            return {}
        
        error_levels = Counter(err['level'] for err in error_patterns)
        
        # Group similar errors
        error_groups = defaultdict(list)
        for error in error_patterns:
            # Simple grouping by first 50 characters of message
            key = error['message'][:50]
            error_groups[key].append(error)
        
        top_errors = sorted(error_groups.items(), key=lambda x: len(x[1]), reverse=True)[:5]
        
        return {
            "error_levels": dict(error_levels),
            "total_errors": len(error_patterns),
            "top_error_patterns": [{"pattern": k, "count": len(v)} for k, v in top_errors],
            "error_distribution": dict(error_levels)
        }
    
    def _generate_jmeter_insights(self, stats: Dict, endpoint_analysis: Dict, error_analysis: Dict) -> List[str]:
        """Generate insights for JMeter results"""
        insights = []
        
        if stats.get('p95', 0) > 2000:
            insights.append("⚠️ High 95th percentile response time (>2s) indicates performance issues")
        
        if error_analysis.get('total_errors', 0) > 0:
            error_rate = (error_analysis['total_errors'] / stats.get('count', 1)) * 100
            insights.append(f"🔴 Error rate: {error_rate:.2f}% - requires investigation")
        
        # Identify bottleneck endpoints
        if endpoint_analysis:
            slowest = max(endpoint_analysis.items(), key=lambda x: x[1].get('p95', 0))
            if slowest[1].get('p95', 0) > 1000:
                insights.append(f"🐌 Slowest endpoint: '{slowest[0]}' (95th percentile: {slowest[1]['p95']:.0f}ms)")
        
        return insights
    
    def _generate_csv_insights(self, stats: Dict, service_analysis: Dict, temporal_analysis: Dict) -> List[str]:
        """Generate insights for CSV performance data"""
        insights = []
        
        if stats.get('p95', 0) > 1000:
            insights.append("⚠️ Performance concern: 95th percentile > 1000ms")
        
        if service_analysis:
            service_count = len(service_analysis)
            insights.append(f"📊 Analyzed {service_count} service(s)")
            
            # Find problematic services
            for service, data in service_analysis.items():
                if data.get('error_count', 0) > 0:
                    insights.append(f"🔴 Service '{service}' has {data['error_count']} error(s)")
        
        return insights
    
    def _generate_log_insights(self, level_counts: Counter, error_analysis: Dict, perf_stats: Dict) -> List[str]:
        """Generate insights for log analysis"""
        insights = []
        
        total_logs = sum(level_counts.values())
        error_count = level_counts.get('ERROR', 0) + level_counts.get('FATAL', 0)
        
        if error_count > 0:
            error_rate = (error_count / total_logs) * 100
            insights.append(f"🔴 Error rate: {error_rate:.2f}% ({error_count}/{total_logs} entries)")
        
        warn_count = level_counts.get('WARN', 0)
        if warn_count > total_logs * 0.1:  # More than 10% warnings
            insights.append(f"⚠️ High warning rate: {warn_count} warnings detected")
        
        if perf_stats:
            avg_response = perf_stats.get('mean', 0)
            if avg_response > 1000:
                insights.append(f"🐌 Average response time from logs: {avg_response:.0f}ms")
        
        return insights
    
    def _generate_generic_insights(self, column_stats: Dict, record_count: int) -> List[str]:
        """Generate insights for generic CSV"""
        insights = []
        
        insights.append(f"📊 Analyzed {record_count} records")
        insights.append(f"🔢 Found {len(column_stats)} numeric columns")
        
        for col, stats in column_stats.items():
            if stats.get('std_dev', 0) > stats.get('mean', 0):
                insights.append(f"📈 High variability in column '{col}' (std_dev > mean)")
        
        return insights
    
    def _generate_easytravel_insights(self, error_rate: float, service_performance: Dict, 
                                    response_time_stats: Dict, hits_per_hour: float) -> List[str]:
        """Generate insights for EasyTravel distributed traces (legacy compatibility)"""
        return self._generate_distributed_traces_insights(
            error_rate, service_performance, response_time_stats, hits_per_hour, "dynatrace_easytravel"
        )
    
    def _generate_distributed_traces_insights(self, error_rate: float, service_performance: Dict, 
                                            response_time_stats: Dict, hits_per_hour: float, 
                                            format_source: str) -> List[str]:
        """Generate insights for distributed traces from any APM tool"""
        insights = []
        
        # Add format source info
        source_names = {
            "dynatrace_easytravel": "Dynatrace EasyTravel",
            "generic_apm": "Generic APM Tool",
            "opentelemetry": "OpenTelemetry",
            "unknown": "Unknown APM Tool"
        }
        insights.append(f"📡 Data source: {source_names.get(format_source, format_source)}")
        
        # Error analysis insights
        if error_rate == 0:
            insights.append("✅ Excellent reliability: No failed requests detected")
        elif error_rate < 1:
            insights.append(f"✅ Good reliability: Low error rate ({error_rate:.2f}%)")
        elif error_rate < 5:
            insights.append(f"⚠️ Moderate reliability: Error rate is {error_rate:.2f}%")
        else:
            insights.append(f"❌ Poor reliability: High error rate ({error_rate:.2f}%)")
        
        # Performance insights
        avg_response_time = response_time_stats.get('mean', 0)
        if avg_response_time > 0:
            if avg_response_time < 500:
                insights.append(f"🚀 Good performance: Average response time {avg_response_time:.0f}ms")
            elif avg_response_time < 1000:
                insights.append(f"⚠️ Moderate performance: Average response time {avg_response_time:.0f}ms")
            else:
                insights.append(f"🐌 Performance concern: Slow average response time {avg_response_time:.0f}ms")
        
        # Response time distribution
        p95 = response_time_stats.get('p95', 0)
        if p95 > avg_response_time * 2:
            insights.append(f"📈 High response time variability: 95th percentile ({p95:.0f}ms) is much higher than average")
        
        # Service performance analysis
        if service_performance:
            slowest_service = max(service_performance.items(), key=lambda x: x[1]['avg_response_time'])
            fastest_service = min(service_performance.items(), key=lambda x: x[1]['avg_response_time'])
            
            if slowest_service[1]['avg_response_time'] > fastest_service[1]['avg_response_time'] * 2:
                insights.append(f"🔍 Service performance gap: '{slowest_service[0]}' is significantly slower than '{fastest_service[0]}'")
        
        # Traffic insights
        if hits_per_hour > 0:
            insights.append(f"📊 Traffic rate: {hits_per_hour:.1f} requests per hour")
            if hits_per_hour > 1000:
                insights.append("🔥 High traffic volume detected")
            elif hits_per_hour < 10:
                insights.append("📉 Low traffic volume detected")
        
        return insights

    def _analyze_security_patterns(self, log_entries: List[Dict], error_entries: List[Dict]) -> Dict[str, Any]:
        """Analyze security patterns in Apache access logs"""
        security_indicators = {
            'scan_attempts': 0,
            'attack_patterns': [],
            'suspicious_ips': [],
            'common_attacks': Counter()
        }
        
        # Common attack patterns
        attack_patterns = {
            'sql_injection': [r'union.*select', r'or.*1=1', r'drop.*table', r'insert.*into'],
            'xss': [r'<script', r'javascript:', r'onerror=', r'onload='],
            'directory_traversal': [r'\.\./', r'%2e%2e', r'..\\'],
            'admin_probe': [r'/admin', r'/wp-admin', r'/phpmyadmin', r'/administrator'],
            'cgi_exploit': [r'/cgi-bin/', r'\.cgi', r'\.pl$'],
            'file_inclusion': [r'include=', r'file=', r'path=', r'page=.*\.(php|asp|jsp)']
        }
        
        ip_error_count = Counter()
        ip_request_patterns = defaultdict(list)
        
        for entry in log_entries:
            uri = entry['uri'].lower()
            ip = entry['ip']
            ip_request_patterns[ip].append(uri)
            
            # Check for attack patterns
            for attack_type, patterns in attack_patterns.items():
                for pattern in patterns:
                    if re.search(pattern, uri, re.IGNORECASE):
                        security_indicators['attack_patterns'].append({
                            'type': attack_type,
                            'ip': ip,
                            'uri': entry['uri'],
                            'timestamp': entry['timestamp']
                        })
                        security_indicators['common_attacks'][attack_type] += 1
        
        # Count errors per IP
        for error in error_entries:
            ip_error_count[error['ip']] += 1
        
        # Identify suspicious IPs (high error rates or attack patterns)
        for ip, error_count in ip_error_count.items():
            total_requests = len([e for e in log_entries if e['ip'] == ip])
            error_rate = (error_count / total_requests * 100) if total_requests > 0 else 0
            
            if error_rate > 50 or error_count > 100:  # High error rate or volume
                security_indicators['suspicious_ips'].append({
                    'ip': ip,
                    'total_requests': total_requests,
                    'error_count': error_count,
                    'error_rate': error_rate
                })
        
        security_indicators['scan_attempts'] = len(security_indicators['attack_patterns'])
        
        return {
            'total_attack_attempts': security_indicators['scan_attempts'],
            'attack_types': dict(security_indicators['common_attacks']),
            'suspicious_ips': security_indicators['suspicious_ips'],
            'sample_attacks': security_indicators['attack_patterns'][:20],  # First 20 attacks
            'security_score': self._calculate_security_score(security_indicators, len(log_entries))
        }
    
    def _analyze_url_patterns(self, urls: Counter, total_requests: int) -> Dict[str, Any]:
        """Analyze URL access patterns"""
        url_categories = {
            'static_resources': 0,
            'api_endpoints': 0,
            'admin_pages': 0,
            'application_pages': 0,
            'error_prone_urls': []
        }
        
        for url, count in urls.items():
            url_lower = url.lower()
            
            if any(ext in url_lower for ext in ['.css', '.js', '.png', '.jpg', '.gif', '.ico', '.svg']):
                url_categories['static_resources'] += count
            elif '/api/' in url_lower or '/rest/' in url_lower:
                url_categories['api_endpoints'] += count
            elif any(admin in url_lower for admin in ['/admin', '/wp-admin', '/phpmyadmin']):
                url_categories['admin_pages'] += count
            else:
                url_categories['application_pages'] += count
        
        return {
            'total_unique_urls': len(urls),
            'url_categories': url_categories,
            'most_requested': dict(urls.most_common(10)),
            'url_diversity': len(urls) / total_requests if total_requests > 0 else 0
        }
    
    def _analyze_ip_patterns(self, ip_addresses: Counter, log_entries: List[Dict]) -> Dict[str, Any]:
        """Analyze IP address patterns and behavior"""
        ip_behavior = {}
        
        for ip, count in ip_addresses.items():
            ip_entries = [e for e in log_entries if e['ip'] == ip]
            
            # Calculate metrics for this IP
            response_times = [e['response_time'] for e in ip_entries if e['response_time'] > 0]
            avg_response_time = statistics.mean(response_times) if response_times else 0
            
            status_distribution = Counter(e['status'] for e in ip_entries)
            error_count = sum(count for status, count in status_distribution.items() if status >= 400)
            error_rate = (error_count / len(ip_entries) * 100) if ip_entries else 0
            
            unique_urls = len(set(e['uri'] for e in ip_entries))
            
            ip_behavior[ip] = {
                'total_requests': count,
                'unique_urls_accessed': unique_urls,
                'error_rate': error_rate,
                'avg_response_time': avg_response_time,
                'status_distribution': dict(status_distribution)
            }
        
        return {
            'total_unique_ips': len(ip_addresses),
            'top_requesters': dict(ip_addresses.most_common(10)),
            'ip_behavior_analysis': {k: v for k, v in ip_behavior.items() if v['total_requests'] > 10},  # Only IPs with significant traffic
            'geographic_diversity': len(ip_addresses)  # Could be enhanced with GeoIP
        }
    
    def _analyze_apache_temporal_patterns(self, time_patterns: List, log_entries: List[Dict]) -> Dict[str, Any]:
        """Analyze temporal patterns in Apache logs"""
        if not time_patterns:
            return {"note": "No valid timestamps found for temporal analysis"}
        
        hourly_distribution = Counter()
        daily_pattern = Counter()
        
        for timestamp in time_patterns:
            hourly_distribution[timestamp.hour] += 1
            daily_pattern[timestamp.strftime('%Y-%m-%d')] += 1
        
        # Find peak hours and days
        peak_hour = hourly_distribution.most_common(1)[0] if hourly_distribution else (0, 0)
        peak_day = daily_pattern.most_common(1)[0] if daily_pattern else ('', 0)
        
        return {
            'time_span': f"{min(time_patterns)} to {max(time_patterns)}",
            'total_days': len(daily_pattern),
            'hourly_distribution': dict(hourly_distribution),
            'daily_distribution': dict(daily_pattern),
            'peak_hour': f"{peak_hour[0]}:00 ({peak_hour[1]} requests)",
            'peak_day': f"{peak_day[0]} ({peak_day[1]} requests)",
            'traffic_consistency': len(daily_pattern) > 1  # Multi-day data
        }
    
    def _categorize_http_errors(self, status_codes: Counter) -> Dict[str, int]:
        """Categorize HTTP error codes"""
        categories = {
            'client_errors_4xx': 0,
            'server_errors_5xx': 0,
            'not_found_404': 0,
            'forbidden_403': 0,
            'bad_request_400': 0,
            'unauthorized_401': 0
        }
        
        for status, count in status_codes.items():
            if status >= 400 and status < 500:
                categories['client_errors_4xx'] += count
                if status == 404:
                    categories['not_found_404'] += count
                elif status == 403:
                    categories['forbidden_403'] += count
                elif status == 400:
                    categories['bad_request_400'] += count
                elif status == 401:
                    categories['unauthorized_401'] += count
            elif status >= 500:
                categories['server_errors_5xx'] += count
        
        return categories
    
    def _calculate_security_score(self, security_indicators: Dict, total_requests: int) -> float:
        """Calculate security score (0-10, where 10 is most secure)"""
        base_score = 10.0
        
        # Deduct points for security issues
        attack_ratio = security_indicators['scan_attempts'] / total_requests if total_requests > 0 else 0
        base_score -= min(attack_ratio * 100, 5)  # Max 5 points deduction for attacks
        
        suspicious_ip_ratio = len(security_indicators['suspicious_ips']) / max(1, total_requests / 100)
        base_score -= min(suspicious_ip_ratio, 3)  # Max 3 points deduction for suspicious IPs
        
        return max(0, round(base_score, 1))
    
    def _generate_apache_insights(self, error_rate: float, security_analysis: Dict, 
                                response_time_stats: Dict, ip_analysis: Dict, 
                                url_analysis: Dict, requests_per_hour: float) -> List[str]:
        """Generate insights specific to Apache access logs"""
        insights = []
        
        # Error analysis
        if error_rate == 0:
            insights.append("✅ Excellent reliability: No HTTP errors detected")
        elif error_rate < 5:
            insights.append(f"✅ Good reliability: Low error rate ({error_rate:.1f}%)")
        elif error_rate < 15:
            insights.append(f"⚠️ Moderate error rate: {error_rate:.1f}% - monitor for issues")
        else:
            insights.append(f"❌ High error rate: {error_rate:.1f}% - immediate attention required")
        
        # Security analysis
        security_score = security_analysis.get('security_score', 10)
        attack_count = security_analysis.get('total_attack_attempts', 0)
        
        if security_score >= 9:
            insights.append(f"🔒 Strong security posture (score: {security_score}/10)")
        elif security_score >= 7:
            insights.append(f"⚠️ Moderate security concerns (score: {security_score}/10)")
        else:
            insights.append(f"🚨 Significant security issues detected (score: {security_score}/10)")
        
        if attack_count > 0:
            insights.append(f"🛡️ Detected {attack_count} attack attempts - review security logs")
            
        # Suspicious IP analysis
        suspicious_ips = security_analysis.get('suspicious_ips', [])
        if suspicious_ips:
            top_suspicious = suspicious_ips[0]['ip']
            insights.append(f"🔍 Suspicious activity from IP: {top_suspicious} - consider blocking")
        
        # Performance analysis
        if response_time_stats:
            avg_response = response_time_stats.get('mean', 0)
            if avg_response > 0:
                if avg_response < 100:
                    insights.append(f"🚀 Excellent performance: {avg_response:.0f}ms average response time")
                elif avg_response < 500:
                    insights.append(f"✅ Good performance: {avg_response:.0f}ms average response time")
                elif avg_response < 1000:
                    insights.append(f"⚠️ Moderate performance: {avg_response:.0f}ms average response time")
                else:
                    insights.append(f"🐌 Performance issues: {avg_response:.0f}ms average response time")
        
        # Traffic analysis
        if requests_per_hour > 0:
            if requests_per_hour > 10000:
                insights.append(f"🔥 High traffic volume: {requests_per_hour:.0f} requests/hour")
            elif requests_per_hour > 1000:
                insights.append(f"📈 Moderate traffic: {requests_per_hour:.0f} requests/hour")
            else:
                insights.append(f"📉 Low traffic: {requests_per_hour:.0f} requests/hour")
        
        # URL diversity analysis
        url_diversity = url_analysis.get('url_diversity', 0)
        if url_diversity > 0.8:
            insights.append("🔍 High URL diversity - possible scanning activity")
        elif url_diversity < 0.1:
            insights.append("📊 Low URL diversity - focused traffic pattern")
        
        return insights

# Enhanced universal log analyzer
def analyze_any_log(file_path: str) -> Dict[str, Any]:
    """Analyze any supported log format"""
    parser = AdvancedLogParser()
    
    try:
        # Detect format
        log_format = parser.detect_format(file_path)
        logger.info(f"Detected format: {log_format}")
        
        # Parse based on detected format
        if log_format == 'jmeter_jtl':
            return parser.parse_jmeter_jtl(file_path)
        elif log_format == 'distributed_traces_csv':
            return parser.parse_distributed_traces_csv(file_path)
        elif log_format == 'performance_csv':
            return parser.parse_performance_csv(file_path)
        elif log_format == 'apache_access_log':
            return parser.parse_apache_access_log(file_path)
        elif log_format == 'application_log':
            return parser.parse_application_log(file_path)
        elif log_format == 'generic_csv':
            return parser.parse_generic_csv(file_path)
        else:
            return parser.parse_application_log(file_path)  # Default to log parsing
            
    except Exception as e:
        logger.error(f"Error analyzing log file: {str(e)}")
        return {
            "error": f"Failed to analyze log file: {str(e)}",
            "file_path": file_path
        }
        if log_format == 'jmeter_jtl':
            return parser.parse_jmeter_jtl(file_path)
        elif log_format == 'distributed_traces_csv':
            return parser.parse_distributed_traces_csv(file_path)
        elif log_format == 'performance_csv':
            return parser.parse_performance_csv(file_path)
        elif log_format == 'apache_access_log':
            return parser.parse_apache_access_log(file_path)
        elif log_format == 'application_log':
            return parser.parse_application_log(file_path)
        elif log_format == 'generic_csv':
            return parser.parse_generic_csv(file_path)
        else:
            return parser.parse_application_log(file_path)  # Default to log parsing
            
    except Exception as e:
        logger.error(f"Error analyzing log file: {str(e)}")
        return {
            "error": f"Failed to analyze log file: {str(e)}",
            "file_path": file_path
        }

# Run JMeter test
def run_jmeter_test(test_file, users=1, log_file=None, extra_args=None):
    # Use user's installed JMeter path by default
    default_jmeter_path = r"C:\\apache-jmeter-5.6.3\\apache-jmeter-5.6.3\\bin\\jmeter.bat"
    jmeter_path = os.getenv("JMETER_PATH", default_jmeter_path)
    test_file = str(Path(test_file).resolve())
    if not log_file:
        log_file = unique_name("jmeter_results", "jtl")
    # Security: Validate file paths
    if not Path(test_file).exists():
        logger.error(f"Test file not found: {test_file}")
        return {"success": False, "error": f"Test file not found: {test_file}", "log_file": log_file}
    cmd = [jmeter_path, "-n", "-t", test_file, "-l", log_file, f"-JThreadGroup.num_threads={users}"]
    if extra_args:
        cmd.extend([str(arg) for arg in extra_args])
    logger.info(f"Running JMeter: {' '.join(cmd)}")
    try:
        # Stream output live to console for summary visibility
        process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True)
        output_lines = []
        for line in process.stdout:
            print(line, end='')  # Print each line as it arrives
            output_lines.append(line)
        process.stdout.close()
        returncode = process.wait()
    except Exception as e:
        logger.error(str(e))
        return {"success": False, "error": str(e), "log_file": log_file}
    if returncode != 0:
        logger.error(''.join(output_lines))
        return {"success": False, "error": ''.join(output_lines), "log_file": log_file}
    return {"success": True, "stdout": ''.join(output_lines), "log_file": log_file}

# Analyze JTL file (simple summary) - Enhanced for multiple formats
def analyze_jtl(jtl_file):
    """Legacy function - now redirects to universal analyzer"""
    return analyze_any_log(jtl_file)

# Enhanced report generator
def generate_comprehensive_report(analysis_result: Dict[str, Any], output_file: str = None) -> str:
    """Generate a comprehensive analysis report"""
    if not output_file:
        output_file = unique_name("log_analysis_report", "md")
    
    report_lines = []
    
    # Header
    report_lines.append("# 📊 Advanced Log Analysis Report")
    report_lines.append(f"**Generated:** {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    report_lines.append(f"**Format:** {analysis_result.get('format', 'Unknown')}")
    report_lines.append("")
    
    # Executive Summary
    report_lines.append("## 🎯 Executive Summary")
    
    if analysis_result.get('error'):
        report_lines.append(f"❌ **Error:** {analysis_result['error']}")
        report_lines.append("")
    else:
        # Summary based on format
        format_type = analysis_result.get('format', '')
        
        if 'jtl' in format_type:
            total = analysis_result.get('total_samples', 0)
            errors = analysis_result.get('error_count', 0)
            avg_rt = analysis_result.get('response_time_stats', {}).get('mean', 0)
            report_lines.append(f"- **Total Samples:** {total:,}")
            report_lines.append(f"- **Error Rate:** {analysis_result.get('error_rate', 0):.2f}%")
            report_lines.append(f"- **Average Response Time:** {avg_rt:.0f}ms")
            
        elif 'csv' in format_type:
            total = analysis_result.get('total_records', 0)
            report_lines.append(f"- **Total Records:** {total:,}")
            if 'response_time_stats' in analysis_result:
                avg_rt = analysis_result['response_time_stats'].get('mean', 0)
                report_lines.append(f"- **Average Response Time:** {avg_rt:.0f}ms")
                
        elif 'log' in format_type:
            total = analysis_result.get('total_lines', 0) or analysis_result.get('total_requests', 0)
            errors = analysis_result.get('error_count', 0) or analysis_result.get('total_errors', 0)
            report_lines.append(f"- **Total Log Entries:** {total:,}")
            report_lines.append(f"- **Error Entries:** {errors:,}")
            
            # Apache-specific summary
            if analysis_result.get('format') == 'apache_access_log':
                unique_ips = analysis_result.get('unique_ips', 0)
                requests_per_hour = analysis_result.get('requests_per_hour', 0)
                security_score = analysis_result.get('security_analysis', {}).get('security_score', 0)
                report_lines.append(f"- **Unique IP Addresses:** {unique_ips:,}")
                report_lines.append(f"- **Requests per Hour:** {requests_per_hour:.1f}")
                report_lines.append(f"- **Security Score:** {security_score}/10")
    
    report_lines.append("")
    
    # Insights
    insights = analysis_result.get('insights', [])
    if insights:
        report_lines.append("## 💡 Key Insights")
        for insight in insights:
            report_lines.append(f"- {insight}")
        report_lines.append("")
    
    # Detailed Analysis
    report_lines.append("## 📈 Detailed Analysis")
    
    # Response Time Statistics
    if 'response_time_stats' in analysis_result:
        stats = analysis_result['response_time_stats']
        report_lines.append("### ⏱️ Response Time Statistics")
        report_lines.append("| Metric | Value |")
        report_lines.append("|--------|-------|")
        for key, value in stats.items():
            if isinstance(value, (int, float)):
                if 'count' in key:
                    report_lines.append(f"| {key.replace('_', ' ').title()} | {value:,} |")
                else:
                    report_lines.append(f"| {key.replace('_', ' ').title()} | {value:.2f}ms |")
        report_lines.append("")
    
    # Service/Endpoint Analysis
    if 'endpoint_analysis' in analysis_result:
        report_lines.append("### 🎯 Endpoint Performance")
        endpoint_stats = analysis_result['endpoint_analysis']
        for endpoint, stats in list(endpoint_stats.items())[:10]:  # Top 10
            report_lines.append(f"**{endpoint}:**")
            report_lines.append(f"- Samples: {stats.get('count', 0)}")
            report_lines.append(f"- Avg Response: {stats.get('mean', 0):.0f}ms")
            report_lines.append(f"- 95th Percentile: {stats.get('p95', 0):.0f}ms")
            if stats.get('error_count', 0) > 0:
                report_lines.append(f"- Errors: {stats['error_count']} ({stats.get('error_rate', 0):.1f}%)")
            report_lines.append("")
    
    elif 'service_analysis' in analysis_result:
        report_lines.append("### 🔧 Service Performance")
        service_stats = analysis_result['service_analysis']
        for service, stats in service_stats.items():
            report_lines.append(f"**{service}:**")
            report_lines.append(f"- Records: {stats.get('count', 0)}")
            report_lines.append(f"- Avg Response: {stats.get('mean', 0):.0f}ms")
            if stats.get('error_count', 0) > 0:
                report_lines.append(f"- Errors: {stats['error_count']}")
            report_lines.append("")
    
    # Error Analysis
    if 'error_analysis' in analysis_result:
        error_data = analysis_result['error_analysis']
        if error_data.get('total_errors', 0) > 0:
            report_lines.append("### 🔴 Error Analysis")
            
            # Error codes
            if 'error_codes' in error_data:
                report_lines.append("**Error Codes:**")
                for code, count in error_data['error_codes'].items():
                    report_lines.append(f"- {code}: {count} occurrences")
                report_lines.append("")
    
    # Level Distribution (for logs)
    if 'level_distribution' in analysis_result:
        report_lines.append("### 📊 Log Level Distribution")
        level_dist = analysis_result['level_distribution']
        for level, count in level_dist.items():
            report_lines.append(f"- **{level}:** {count:,}")
        report_lines.append("")
    
    # Apache Access Log Specific Analysis
    if analysis_result.get('format') == 'apache_access_log':
        
        # Security Analysis
        if 'security_analysis' in analysis_result:
            security_data = analysis_result['security_analysis']
            report_lines.append("### 🛡️ Security Analysis")
            report_lines.append(f"- **Security Score:** {security_data.get('security_score', 0)}/10")
            report_lines.append(f"- **Attack Attempts:** {security_data.get('total_attack_attempts', 0):,}")
            
            if security_data.get('attack_types'):
                report_lines.append("**Attack Types:**")
                for attack_type, count in security_data['attack_types'].items():
                    report_lines.append(f"- {attack_type.replace('_', ' ').title()}: {count} attempts")
                
            if security_data.get('suspicious_ips'):
                report_lines.append("**Suspicious IPs:**")
                for ip_info in security_data['suspicious_ips'][:5]:  # Top 5
                    report_lines.append(f"- {ip_info['ip']}: {ip_info['error_count']} errors ({ip_info['error_rate']:.1f}% error rate)")
            report_lines.append("")
        
        # Status Code Distribution
        if 'status_code_distribution' in analysis_result:
            report_lines.append("### 📊 HTTP Status Code Distribution")
            status_codes = analysis_result['status_code_distribution']
            report_lines.append("| Status Code | Count | Percentage |")
            report_lines.append("|-------------|-------|------------|")
            total_requests = sum(status_codes.values())
            
            # Sort by status code
            for status in sorted(status_codes.keys()):
                count = status_codes[status]
                percentage = (count / total_requests * 100) if total_requests > 0 else 0
                status_name = {
                    200: "OK", 404: "Not Found", 403: "Forbidden", 
                    400: "Bad Request", 500: "Internal Server Error"
                }.get(status, f"HTTP {status}")
                report_lines.append(f"| {status} ({status_name}) | {count:,} | {percentage:.1f}% |")
            report_lines.append("")
        
        # Top IP Addresses
        if 'top_ips' in analysis_result:
            report_lines.append("### 🌐 Top IP Addresses")
            top_ips = analysis_result['top_ips']
            report_lines.append("| IP Address | Requests | % of Total |")
            report_lines.append("|------------|----------|------------|")
            total_requests = analysis_result.get('total_requests', 0)
            
            for ip, count in list(top_ips.items())[:10]:  # Top 10
                percentage = (count / total_requests * 100) if total_requests > 0 else 0
                report_lines.append(f"| {ip} | {count:,} | {percentage:.1f}% |")
            report_lines.append("")
        
        # Top URLs
        if 'top_urls' in analysis_result:
            report_lines.append("### 🔗 Most Requested URLs")
            top_urls = analysis_result['top_urls']
            report_lines.append("| URL | Requests | % of Total |")
            report_lines.append("|-----|----------|------------|")
            total_requests = analysis_result.get('total_requests', 0)
            
            for url, count in list(top_urls.items())[:15]:  # Top 15
                percentage = (count / total_requests * 100) if total_requests > 0 else 0
                # Truncate long URLs
                display_url = url if len(url) <= 50 else url[:47] + "..."
                report_lines.append(f"| {display_url} | {count:,} | {percentage:.1f}% |")
            report_lines.append("")
        
        # Temporal Analysis
        if 'temporal_analysis' in analysis_result:
            temporal_data = analysis_result['temporal_analysis']
            if temporal_data.get('time_span') != "No valid timestamps found for temporal analysis":
                report_lines.append("### ⏰ Temporal Analysis")
                report_lines.append(f"- **Time Span:** {temporal_data.get('time_span', 'Unknown')}")
                report_lines.append(f"- **Peak Hour:** {temporal_data.get('peak_hour', 'Unknown')}")
                report_lines.append(f"- **Peak Day:** {temporal_data.get('peak_day', 'Unknown')}")
                report_lines.append("")
    
    # Write report
    report_content = '\n'.join(report_lines)
    
    with open(output_file, 'w', encoding='utf-8') as f:
        f.write(report_content)
    
    logger.info(f"Report generated: {output_file}")
    return output_file

# ============================================================================
# JMeter XML Correlation Engine
# ============================================================================
import xml.etree.ElementTree as ET
from xml.etree.ElementTree import ParseError
import re

def parse_jmeter_xml(xml_path):
    """
    Step 1: Parse JMeter XML logs and extract request/response data with invalid character handling
    
    Args:
        xml_path (str): Path to the JMeter XML log file
        
    Returns:
        list: List of sample dictionaries containing response, request, headers, and cookies
    """
    try:
        # Try standard parsing first
        tree = ET.parse(xml_path)
        root = tree.getroot()
    except ParseError as e:
        # If parsing fails, clean invalid XML entities
        print(f"Warning: XML parsing error, cleaning invalid character entities...")
        try:
            with open(xml_path, 'r', encoding='utf-8', errors='replace') as f:
                xml_content = f.read()
            
            # Remove or replace invalid XML character entities
            # XML 1.0 doesn't allow control characters except tab, newline, carriage return
            # Pattern: &#xHH; or &#DDD;
            def replace_entity(match):
                entity = match.group(0)
                # Extract the character code
                if entity.startswith('&#x'):
                    # Hexadecimal
                    char_code = int(entity[3:-1], 16)
                else:
                    # Decimal
                    char_code = int(entity[2:-1])
                
                # Keep valid XML 1.0 characters
                if char_code == 0x9 or char_code == 0xA or char_code == 0xD:
                    return entity
                if char_code >= 0x20:
                    return entity
                # Remove invalid control characters
                return ''
            
            # Clean XML entities
            xml_content_clean = re.sub(r'&#x?[0-9a-fA-F]+;', replace_entity, xml_content)
            
            # Parse cleaned content
            root = ET.fromstring(xml_content_clean)
        except Exception as e2:
            print(f"Error: {str(e2)}")
            raise Exception(f"Failed to parse XML file even after cleaning: {str(e2)}")

    samples = []
    for sample in root.findall(".//httpSample"):
        response_data = sample.findtext("responseData") or ""
        request_data = sample.findtext("queryString") or ""
        headers = sample.findtext("requestHeader") or ""
        cookies = sample.findtext("cookies") or ""
        label = sample.get("lb") or ""
        samples.append({
            "response": response_data,
            "request": request_data,
            "headers": headers,
            "cookies": cookies,
            "label": label
        })
    return samples

def find_dynamic_values(samples1, samples2):
    """
    Step 2: Identify dynamic values that appear in responses and are reused in requests
    
    Args:
        samples1 (list): First set of samples
        samples2 (list): Second set of samples
        
    Returns:
        dict: Dictionary of dynamic values with their occurrence information
    """
    dynamic_values = {}

    all_samples = samples1 + samples2
    for i, sample in enumerate(all_samples):
        response = sample["response"]
        headers = sample["headers"]
        cookies = sample["cookies"]

        # Extract potential dynamic values from response body, headers, and cookies
        matches = re.findall(r'value="(\w+?)"', response)
        matches += re.findall(r'Set-Cookie:.*?=(\w+?);', headers)
        matches += [val for _, val in re.findall(r'(\w+?)=(\w+?);', cookies)]
        matches = list(set(matches))  # Remove duplicates

        for match in matches:
            for j, future_sample in enumerate(all_samples[i+1:], start=i+1):
                if match in future_sample["request"] or match in future_sample["headers"] or match in future_sample["cookies"]:
                    dynamic_values[match] = {
                        "response_index": i,
                        "reuse_index": j,
                        "value": match,
                        "source_label": all_samples[i]["label"],
                        "target_label": all_samples[j]["label"]
                    }
                    break
    return dynamic_values

def generate_regex(value, response_text):
    """
    Step 3: Generate regex pattern using left and right boundaries
    
    Args:
        value (str): The dynamic value to create regex for
        response_text (str): The response text containing the value
        
    Returns:
        str: Generated regex pattern or None if not found
    """
    pattern = re.escape(value)
    match = re.search(rf'(.{{0,20}}){pattern}(.{{0,20}})', response_text)
    if match:
        left = match.group(1)
        right = match.group(2)
        regex = f'{re.escape(left)}(.+?){re.escape(right)}'
        return regex
    return None

def replace_hardcoded_value_in_jmx(jmx_path, hardcoded_value, variable_name, target_sampler_name=None, output_path=None):
    """
    Replace hardcoded values in JMX file with variable references
    
    Args:
        jmx_path (str): Path to the JMX file
        hardcoded_value (str): The hardcoded value to replace
        variable_name (str): Name of the variable to use (without ${})
        target_sampler_name (str): Specific sampler to replace in (optional, replaces in all if None)
        output_path (str): Optional output path for the updated JMX
    
    Returns:
        int: Number of replacements made
    """
    tree = ET.parse(jmx_path)
    root = tree.getroot()
    replacements_made = 0
    
    # Find all HTTPSamplerProxy elements
    for sampler in root.findall(".//HTTPSamplerProxy"):
        sampler_name = sampler.get("testname", "")
        
        # If target_sampler_name specified, only process that sampler
        if target_sampler_name and sampler_name != target_sampler_name:
            continue
        
        # Find all arguments in this sampler
        for arg_element in sampler.findall(".//elementProp[@elementType='HTTPArgument']"):
            # Check the value stringProp
            for value_prop in arg_element.findall(".//stringProp[@name='Argument.value']"):
                if value_prop.text == hardcoded_value:
                    # Replace with variable reference
                    value_prop.text = f"${{{variable_name}}}"
                    replacements_made += 1
                    param_name = arg_element.get("name", "unknown")
                    logger.info(f"Replaced '{hardcoded_value}' with '${{{variable_name}}}' in sampler '{sampler_name}' parameter '{param_name}'")
    
    if replacements_made > 0:
        # Save the updated JMX
        if not output_path:
            output_path = jmx_path  # Overwrite original if no output path specified
        
        tree.write(output_path, encoding="UTF-8", xml_declaration=True)
        logger.info(f"Made {replacements_made} replacements in JMX file, saved to {output_path}")
    
    return replacements_made

def insert_regex_extractor(jmx_path, variable_name, regex, sampler_name, output_path=None):
    """
    Step 4: Insert Regular Expression Extractor into JMX file
    
    Args:
        jmx_path (str): Path to the JMX file
        variable_name (str): Name of the variable to extract
        regex (str): Regular expression pattern
        sampler_name (str): Name of the sampler to add extractor to
        output_path (str): Optional output path for the updated JMX
    """
    tree = ET.parse(jmx_path)
    root = tree.getroot()

    extractor_added = False
    # Create parent map for ElementTree
    parent_map = {c: p for p in root.iter() for c in p}
    
    for sampler in root.findall(".//HTTPSamplerProxy"):
        if sampler.get("testname") == sampler_name:
            # Find the hashTree element after this sampler using parent map
            parent = parent_map.get(sampler)
            if parent is None:
                continue
                
            sampler_index = list(parent).index(sampler)
            
            if sampler_index + 1 < len(parent) and parent[sampler_index + 1].tag == "hashTree":
                hashtree = parent[sampler_index + 1]
                
                # Create RegexExtractor element
                extractor = ET.Element("RegexExtractor", {
                    "guiclass": "RegexExtractorGui",
                    "testclass": "RegexExtractor",
                    "testname": f"Extract {variable_name}",
                    "enabled": "true"
                })
                
                # Add properties
                ET.SubElement(extractor, "stringProp", {"name": "RegexExtractor.useHeaders"}).text = "false"
                ET.SubElement(extractor, "stringProp", {"name": "RegexExtractor.refname"}).text = variable_name
                ET.SubElement(extractor, "stringProp", {"name": "RegexExtractor.regex"}).text = regex
                ET.SubElement(extractor, "stringProp", {"name": "RegexExtractor.template"}).text = "$1$"
                ET.SubElement(extractor, "stringProp", {"name": "RegexExtractor.default"}).text = "NOT_FOUND"
                ET.SubElement(extractor, "stringProp", {"name": "RegexExtractor.match_number"}).text = "1"
                ET.SubElement(extractor, "boolProp", {"name": "RegexExtractor.default_empty_value"}).text = "false"
                
                # Add extractor to hashtree
                hashtree.append(extractor)
                hashtree.append(ET.Element("hashTree"))
                
                extractor_added = True
                logger.info(f"Added RegexExtractor '{variable_name}' to sampler '{sampler_name}'")
                break

    if not extractor_added:
        logger.warning(f"Sampler '{sampler_name}' not found in JMX file")
        return False

    if not output_path:
        output_path = "updated_" + os.path.basename(jmx_path)
    
    tree.write(output_path, encoding='utf-8', xml_declaration=True)
    logger.info(f"Updated JMX saved to: {output_path}")
    return True

def add_dynatrace_headers_to_jmx(jmx_path, application_name, transaction_controller_name, output_path=None):
    """
    Add Dynatrace headers to all HTTP requests in a JMeter JMX file.
    
    This function adds the following headers to each HTTPSamplerProxy element:
    - LSN=<application_name>
    - TSN=<transaction_controller_name>
    
    Args:
        jmx_path (str): Path to the JMX file to modify
        application_name (str): Application name for LSN header
        transaction_controller_name (str): Transaction controller name for TSN header
        output_path (str): Optional output path for the updated JMX file
        
    Returns:
        dict: Results containing success status, count of updated samplers, and output file path
    """
    try:
        logger.info(f"Adding Dynatrace headers to JMX file: {jmx_path}")
        logger.info(f"Application Name (LSN): {application_name}")
        logger.info(f"Transaction Name (TSN): {transaction_controller_name}")
        
        # Parse the JMX file
        tree = ET.parse(jmx_path)
        root = tree.getroot()
        
        updated_samplers = 0
        sampler_names = []
        
        # Find all HTTPSamplerProxy elements
        for sampler in root.findall(".//HTTPSamplerProxy"):
            sampler_name = sampler.get("testname", "Unknown")
            
            # Find or create elementProp for headers
            element_prop = None
            for prop in sampler.findall("elementProp"):
                if prop.get("name") == "HTTPsampler.Arguments":
                    # This is for parameters, not headers
                    continue
                elif prop.get("elementType") == "Header":
                    # Found existing header manager
                    element_prop = prop
                    break
            
            # If no header element exists, create one
            if element_prop is None:
                element_prop = ET.SubElement(sampler, "elementProp", {
                    "name": "HTTPsampler.header_manager",
                    "elementType": "Header",
                    "guiclass": "HeaderPanel",
                    "testclass": "HeaderManager",
                    "testname": "HTTP Header Manager"
                })
                # Create collectionProp to hold headers
                collection_prop = ET.SubElement(element_prop, "collectionProp", {
                    "name": "HeaderManager.headers"
                })
            else:
                # Find or create collectionProp
                collection_prop = element_prop.find("collectionProp[@name='HeaderManager.headers']")
                if collection_prop is None:
                    collection_prop = ET.SubElement(element_prop, "collectionProp", {
                        "name": "HeaderManager.headers"
                    })
            
            # Check if LSN header already exists
            lsn_exists = False
            tsn_exists = False
            
            for elem_prop in collection_prop.findall("elementProp"):
                for string_prop in elem_prop.findall("stringProp[@name='Header.name']"):
                    if string_prop.text == "LSN":
                        lsn_exists = True
                        # Update the value
                        for value_prop in elem_prop.findall("stringProp[@name='Header.value']"):
                            value_prop.text = application_name
                    elif string_prop.text == "TSN":
                        tsn_exists = True
                        # Update the value
                        for value_prop in elem_prop.findall("stringProp[@name='Header.value']"):
                            value_prop.text = transaction_controller_name
            
            # Add LSN header if it doesn't exist
            if not lsn_exists:
                lsn_header = ET.SubElement(collection_prop, "elementProp", {
                    "name": "",
                    "elementType": "Header"
                })
                ET.SubElement(lsn_header, "stringProp", {"name": "Header.name"}).text = "LSN"
                ET.SubElement(lsn_header, "stringProp", {"name": "Header.value"}).text = application_name
            
            # Add TSN header if it doesn't exist
            if not tsn_exists:
                tsn_header = ET.SubElement(collection_prop, "elementProp", {
                    "name": "",
                    "elementType": "Header"
                })
                ET.SubElement(tsn_header, "stringProp", {"name": "Header.name"}).text = "TSN"
                ET.SubElement(tsn_header, "stringProp", {"name": "Header.value"}).text = transaction_controller_name
            
            updated_samplers += 1
            sampler_names.append(sampler_name)
            logger.info(f"Updated sampler: '{sampler_name}' with Dynatrace headers")
        
        # Determine output path
        if not output_path:
            output_path = "dynatrace_" + os.path.basename(jmx_path)
        
        # Write the updated JMX file
        tree.write(output_path, encoding='utf-8', xml_declaration=True)
        logger.info(f"Updated JMX file saved to: {output_path}")
        
        return {
            "success": True,
            "jmx_file": jmx_path,
            "output_file": output_path,
            "application_name": application_name,
            "transaction_controller_name": transaction_controller_name,
            "updated_samplers": updated_samplers,
            "sampler_names": sampler_names,
            "headers_added": {
                "LSN": application_name,
                "TSN": transaction_controller_name
            },
            "message": f"Successfully added/updated Dynatrace headers in {updated_samplers} HTTP sampler(s)"
        }
        
    except FileNotFoundError:
        error_msg = f"JMX file not found: {jmx_path}"
        logger.error(error_msg)
        return {
            "success": False,
            "error": error_msg
        }
    except ET.ParseError as e:
        error_msg = f"Failed to parse JMX file: {str(e)}"
        logger.error(error_msg)
        return {
            "success": False,
            "error": error_msg
        }
    except Exception as e:
        error_msg = f"Error adding Dynatrace headers: {str(e)}"
        logger.error(error_msg)
        return {
            "success": False,
            "error": error_msg
        }

def auto_add_dynatrace_headers_to_jmx(jmx_path, application_name, output_path=None):
    """
    Automatically add Dynatrace headers to all HTTP requests in a JMeter JMX file.
    
    This function automatically detects Transaction Controllers and adds appropriate headers:
    - LSN=<application_name>
    - TSN=<TransactionControllerName> (auto-detected from the hierarchy)
    
    For HTTP requests under a Transaction Controller, TSN will be set to that controller's name.
    For HTTP requests not under any Transaction Controller, TSN will be set to the sampler's own name.
    
    Args:
        jmx_path (str): Path to the JMX file to modify
        application_name (str): Application name for LSN header
        output_path (str): Optional output path for the updated JMX file
        
    Returns:
        dict: Results containing success status, count of updated samplers, and mapping details
    """
    try:
        logger.info(f"Auto-tagging Dynatrace headers in JMX file: {jmx_path}")
        logger.info(f"Application Name (LSN): {application_name}")
        
        # Parse the JMX file
        tree = ET.parse(jmx_path)
        root = tree.getroot()
        
        updated_samplers = 0
        transaction_mapping = {}
        
        # Find all GenericController (Transaction Controllers) and their child HTTP samplers
        # Create parent map for ElementTree
        parent_map = {c: p for p in root.iter() for c in p}
        
        def find_transaction_controller_parent(element, root):
            """Find the parent Transaction Controller for an element"""
            # Look for GenericController in the hierarchy
            for controller in root.findall(".//GenericController"):
                # Check if this sampler is a descendant of this controller
                controller_parent = parent_map.get(controller)
                if controller_parent is not None:
                    # Find the hashTree after this controller
                    controller_index = list(controller_parent).index(controller)
                    if controller_index + 1 < len(controller_parent):
                        hashtree = controller_parent[controller_index + 1]
                        if hashtree.tag == "hashTree":
                            # Check if our element is in this hashTree
                            for descendant in hashtree.iter():
                                if descendant == element:
                                    return controller.get("testname", "Unknown")
            return None
        
        # Process all HTTPSamplerProxy elements
        for sampler in root.findall(".//HTTPSamplerProxy"):
            sampler_name = sampler.get("testname", "Unknown")
            
            # Auto-detect the transaction controller name
            transaction_name = find_transaction_controller_parent(sampler, root)
            
            # If no transaction controller found, use the sampler name itself
            if transaction_name is None:
                transaction_name = sampler_name
                logger.info(f"Sampler '{sampler_name}' has no Transaction Controller, using sampler name as TSN")
            else:
                logger.info(f"Sampler '{sampler_name}' belongs to Transaction Controller '{transaction_name}'")
            
            # Find or create elementProp for headers
            element_prop = None
            for prop in sampler.findall("elementProp"):
                if prop.get("name") == "HTTPsampler.Arguments":
                    continue
                elif prop.get("elementType") == "Header":
                    element_prop = prop
                    break
            
            # Create header manager if it doesn't exist
            if element_prop is None:
                element_prop = ET.SubElement(sampler, "elementProp", {
                    "name": "HTTPsampler.header_manager",
                    "elementType": "Header",
                    "guiclass": "HeaderPanel",
                    "testclass": "HeaderManager",
                    "testname": "HTTP Header Manager"
                })
                collection_prop = ET.SubElement(element_prop, "collectionProp", {
                    "name": "HeaderManager.headers"
                })
            else:
                collection_prop = element_prop.find("collectionProp[@name='HeaderManager.headers']")
                if collection_prop is None:
                    collection_prop = ET.SubElement(element_prop, "collectionProp", {
                        "name": "HeaderManager.headers"
                    })
            
            # Check if headers already exist and update them
            lsn_exists = False
            tsn_exists = False
            
            for elem_prop in collection_prop.findall("elementProp"):
                for string_prop in elem_prop.findall("stringProp[@name='Header.name']"):
                    if string_prop.text == "LSN":
                        lsn_exists = True
                        for value_prop in elem_prop.findall("stringProp[@name='Header.value']"):
                            value_prop.text = application_name
                    elif string_prop.text == "TSN":
                        tsn_exists = True
                        for value_prop in elem_prop.findall("stringProp[@name='Header.value']"):
                            value_prop.text = transaction_name
            
            # Add LSN header if it doesn't exist
            if not lsn_exists:
                lsn_header = ET.SubElement(collection_prop, "elementProp", {
                    "name": "",
                    "elementType": "Header"
                })
                ET.SubElement(lsn_header, "stringProp", {"name": "Header.name"}).text = "LSN"
                ET.SubElement(lsn_header, "stringProp", {"name": "Header.value"}).text = application_name
            
            # Add TSN header if it doesn't exist
            if not tsn_exists:
                tsn_header = ET.SubElement(collection_prop, "elementProp", {
                    "name": "",
                    "elementType": "Header"
                })
                ET.SubElement(tsn_header, "stringProp", {"name": "Header.name"}).text = "TSN"
                ET.SubElement(tsn_header, "stringProp", {"name": "Header.value"}).text = transaction_name
            
            updated_samplers += 1
            
            # Track transaction mapping
            if transaction_name not in transaction_mapping:
                transaction_mapping[transaction_name] = []
            transaction_mapping[transaction_name].append(sampler_name)
        
        # Determine output path
        if not output_path:
            output_path = "dynatrace_auto_" + os.path.basename(jmx_path)
        
        # Write the updated JMX file
        tree.write(output_path, encoding='utf-8', xml_declaration=True)
        logger.info(f"Updated JMX file saved to: {output_path}")
        
        return {
            "success": True,
            "jmx_file": jmx_path,
            "output_file": output_path,
            "application_name": application_name,
            "updated_samplers": updated_samplers,
            "transaction_controllers_found": len(transaction_mapping),
            "transaction_mapping": transaction_mapping,
            "headers_added": {
                "LSN": application_name,
                "TSN": "Auto-detected from Transaction Controllers"
            },
            "message": f"Successfully auto-tagged {updated_samplers} HTTP sampler(s) across {len(transaction_mapping)} transaction(s)",
            "details": f"LSN set to '{application_name}', TSN auto-detected for each sampler based on parent Transaction Controller"
        }
        
    except FileNotFoundError:
        error_msg = f"JMX file not found: {jmx_path}"
        logger.error(error_msg)
        return {
            "success": False,
            "error": error_msg
        }
    except ET.ParseError as e:
        error_msg = f"Failed to parse JMX file: {str(e)}"
        logger.error(error_msg)
        return {
            "success": False,
            "error": error_msg
        }
    except Exception as e:
        error_msg = f"Error auto-tagging Dynatrace headers: {str(e)}"
        logger.error(error_msg)
        return {
            "success": False,
            "error": error_msg
        }

def correlate_jmeter(xml1_path, xml2_path, jmx_path, sampler_name=None, output_path=None):
    """
    Step 5: Main function to run the full correlation workflow
    
    Args:
        xml1_path (str): Path to first XML log file
        xml2_path (str): Path to second XML log file
        jmx_path (str): Path to JMX test plan file
        sampler_name (str): Specific sampler name to add extractors to (optional)
        output_path (str): Output path for updated JMX file (optional)
        
    Returns:
        dict: Results of the correlation process
    """
    try:
        logger.info("Starting JMeter correlation analysis...")
        
        # Parse XML files
        samples1 = parse_jmeter_xml(xml1_path)
        samples2 = parse_jmeter_xml(xml2_path)
        
        logger.info(f"Parsed {len(samples1)} samples from {xml1_path}")
        logger.info(f"Parsed {len(samples2)} samples from {xml2_path}")
        
        # Find dynamic values
        dynamic_values = find_dynamic_values(samples1, samples2)
        logger.info(f"Found {len(dynamic_values)} potential dynamic values")
        
        if not dynamic_values:
            return {
                "success": True,
                "message": "No dynamic values requiring correlation were found",
                "dynamic_values_count": 0,
                "extractors_added": 0
            }
        
        all_samples = samples1 + samples2
        extractors_added = 0
        values_replaced = 0
        correlation_results = []
        
        # Determine output file path
        correlation_output = output_path or f"updated_{os.path.basename(jmx_path)}"
        
        for value, info in dynamic_values.items():
            response_text = all_samples[info["response_index"]]["response"]
            regex = generate_regex(value, response_text)
            
            if regex:
                variable_name = f"extracted_{value}"
                source_sampler = sampler_name or info.get("source_label", "Unknown")
                target_sampler = info.get("target_label")
                
                # Step 1: Insert extractor at source sampler
                extractor_success = insert_regex_extractor(jmx_path, variable_name, regex, source_sampler, correlation_output)
                
                if extractor_success:
                    extractors_added += 1
                    logger.info(f"Successfully added extractor for '{value}' as '${variable_name}' at sampler '{source_sampler}'")
                    
                    # Step 2: Replace hardcoded value in target sampler
                    if target_sampler:
                        replacements = replace_hardcoded_value_in_jmx(
                            correlation_output,  # Use output file from previous step
                            value,
                            variable_name,
                            target_sampler,
                            correlation_output  # Continue updating same file
                        )
                        if replacements > 0:
                            values_replaced += replacements
                            logger.info(f"Replaced {replacements} occurrence(s) of '{value}' with '${{{variable_name}}}' in target sampler '{target_sampler}'")
                    
                    correlation_results.append({
                        "value": value,
                        "variable_name": variable_name,
                        "regex": regex,
                        "source_sampler": info.get("source_label"),
                        "target_sampler": target_sampler,
                        "sampler_used": source_sampler,
                        "replacements_made": replacements if target_sampler else 0
                    })
                else:
                    logger.warning(f"Failed to add extractor for '{value}'")
        
        return {
            "success": True,
            "dynamic_values_count": len(dynamic_values),
            "extractors_added": extractors_added,
            "values_replaced": values_replaced,
            "correlation_results": correlation_results,
            "output_file": correlation_output,
            "recommendations": [
                f"Found {len(dynamic_values)} dynamic values that may need correlation",
                f"Successfully added {extractors_added} RegexExtractors to your JMX file",
                f"Automatically replaced {values_replaced} hardcoded value(s) with variable references",
                "Test the updated JMX file to ensure correlations work correctly",
                "Review the correlation report for details on each dynamic value"
            ]
        }
        
    except Exception as e:
        logger.error(f"Error in JMeter correlation: {str(e)}")
        return {
            "success": False,
            "error": f"Correlation failed: {str(e)}",
            "dynamic_values_count": 0,
            "extractors_added": 0
        }

# MCP tool registration
def register_tools(mcp):
    @mcp.tool()
    def run_test(test_file: str, users: int = 1, log_file: str = None, extra_args: list = None) -> dict:
        """Run a JMeter test plan with specified users."""
        try:
            return run_jmeter_test(test_file, users, log_file, extra_args)
        except Exception as e:
            logger.error(f"Error running JMeter test: {str(e)}")
            return {"success": False, "error": str(e)}

    @mcp.tool()
    def analyze_results(jtl_file: str) -> dict:
        try:
            return analyze_jtl(jtl_file)
        except Exception as e:
            logger.error(f"Error analyzing JTL: {str(e)}")
            return {"error": str(e)}

    @mcp.tool()
    def analyze_any_log_file(log_file: str) -> dict:
        try:
            return analyze_any_log(log_file)
        except Exception as e:
            logger.error(f"Error analyzing log file: {str(e)}")
            return {"error": str(e)}

    @mcp.tool()
    def generate_analysis_report(log_file: str, output_file: str = None) -> dict:
        try:
            analysis_result = analyze_any_log(log_file)
            if analysis_result.get('error'):
                return {"success": False, "error": analysis_result['error']}
            report_file = generate_comprehensive_report(analysis_result, output_file)
            return {
                "success": True,
                "analysis_result": analysis_result,
                "report_file": report_file,
                "format_detected": analysis_result.get('format'),
                "summary": {
                    "total_records": analysis_result.get('total_samples') or analysis_result.get('total_records') or analysis_result.get('total_lines', 0),
                    "insights_count": len(analysis_result.get('insights', [])),
                    "format": analysis_result.get('format')
                }
            }
        except Exception as e:
            logger.error(f"Error generating report: {str(e)}")
            return {"success": False, "error": f"Failed to generate report: {str(e)}"}

    @mcp.tool()
    def detect_log_format(file_path: str) -> dict:
        try:
            parser = AdvancedLogParser()
            detected_format = parser.detect_format(file_path)
            return {
                "success": True,
                "file_path": file_path,
                "detected_format": detected_format,
                "supported_formats": parser.supported_formats
            }
        except Exception as e:
            return {"success": False, "error": str(e)}

    @mcp.tool()
    def analyze_easytravel_traces(csv_file: str) -> dict:
        try:
            parser = AdvancedLogParser()
            analysis_result = parser.parse_distributed_traces_csv(csv_file)
            if analysis_result.get('error'):
                return {"success": False, "error": analysis_result['error']}
            error_details = analysis_result.get('error_details', {})
            service_analysis = analysis_result.get('service_analysis', {})
            url_analysis = analysis_result.get('url_analysis', {})
            response_code_analysis = analysis_result.get('response_code_analysis', {})
            response_time_stats = analysis_result.get('response_time_stats', {})
            traffic_analysis = analysis_result.get('traffic_analysis', {})
            return {
                "success": True,
                "file_analyzed": csv_file,
                "detected_source": analysis_result.get('detected_source', 'unknown'),
                "field_mappings": analysis_result.get('field_mappings', {}),
                "total_records": analysis_result.get('total_records', 0),
                "error_details": {
                    "error_count": error_details.get('error_count', 0),
                    "error_rate": error_details.get('error_rate', 0),
                    "failed_requests": error_details.get('failed_requests', [])
                },
                "service_names": service_analysis.get('unique_services', []),
                "service_distribution": service_analysis.get('service_distribution', {}),
                "service_performance": service_analysis.get('service_performance', {}),
                "urls": url_analysis.get('unique_urls', []),
                "url_distribution": url_analysis.get('url_distribution', {}),
                "response_codes": response_code_analysis.get('code_distribution', {}),
                "success_rate": response_code_analysis.get('success_rate', 0),
                "response_time_analysis": {
                    "average_ms": response_time_stats.get('mean', 0),
                    "median_ms": response_time_stats.get('median', 0),
                    "min_ms": response_time_stats.get('min', 0),
                    "max_ms": response_time_stats.get('max', 0),
                    "p95_ms": response_time_stats.get('p95', 0),
                    "p99_ms": response_time_stats.get('p99', 0)
                },
                "traffic_metrics": {
                    "hits_per_hour": traffic_analysis.get('hits_per_hour', 0),
                    "time_span_hours": traffic_analysis.get('time_span_hours', 0),
                    "total_requests": traffic_analysis.get('total_requests', 0)
                },
                "insights": analysis_result.get('insights', []),
                "format": analysis_result.get('format', 'distributed_traces_csv')
            }
        except Exception as e:
            logger.error(f"Error analyzing distributed traces: {str(e)}")
            return {"success": False, "error": f"Failed to analyze distributed traces: {str(e)}"}
    
    @mcp.tool()
    def correlate_jmeter_files(xml1_path: str, xml2_path: str, jmx_path: str, sampler_name: str = None, output_path: str = None) -> dict:
        """Run full JMeter correlation workflow to identify and add dynamic value extractors."""
        try:
            return correlate_jmeter(xml1_path, xml2_path, jmx_path, sampler_name, output_path)
        except Exception as e:
            logger.error(f"Error in JMeter correlation: {str(e)}")
            return {"success": False, "error": f"Correlation failed: {str(e)}"}
    
    @mcp.tool()
    def parse_jmeter_xml_samples(xml_path: str) -> dict:
        """Parse JMeter XML log file and extract sample data."""
        try:
            samples = parse_jmeter_xml(xml_path)
            return {
                "success": True,
                "xml_file": xml_path,
                "samples_count": len(samples),
                "samples": samples[:10],  # Return first 10 samples for preview
                "sample_labels": list(set([s.get('label', '') for s in samples]))
            }
        except Exception as e:
            logger.error(f"Error parsing JMeter XML: {str(e)}")
            return {"success": False, "error": f"Failed to parse XML: {str(e)}"}
    
    @mcp.tool()
    def find_jmeter_dynamic_values(xml1_path: str, xml2_path: str) -> dict:
        """Find dynamic values between two JMeter XML execution logs."""
        try:
            samples1 = parse_jmeter_xml(xml1_path)
            samples2 = parse_jmeter_xml(xml2_path)
            dynamic_values = find_dynamic_values(samples1, samples2)
            
            return {
                "success": True,
                "xml1_file": xml1_path,
                "xml2_file": xml2_path,
                "samples1_count": len(samples1),
                "samples2_count": len(samples2),
                "dynamic_values_count": len(dynamic_values),
                "dynamic_values": dynamic_values,
                "recommendations": [
                    f"Found {len(dynamic_values)} dynamic values that may need correlation",
                    "Use correlate_jmeter_files to automatically add extractors to your JMX file",
                    "Review dynamic values to ensure they are legitimate correlation candidates"
                ]
            }
        except Exception as e:
            logger.error(f"Error finding dynamic values: {str(e)}")
            return {"success": False, "error": f"Failed to find dynamic values: {str(e)}"}
    
    @mcp.tool()
    def add_regex_extractor_to_jmx(jmx_path: str, variable_name: str, regex: str, sampler_name: str, output_path: str = None) -> dict:
        """Add a Regular Expression Extractor to a specific sampler in JMX file."""
        try:
            success = insert_regex_extractor(jmx_path, variable_name, regex, sampler_name, output_path)
            if success:
                return {
                    "success": True,
                    "jmx_file": jmx_path,
                    "variable_name": variable_name,
                    "regex_pattern": regex,
                    "target_sampler": sampler_name,
                    "output_file": output_path or f"updated_{os.path.basename(jmx_path)}",
                    "message": f"Successfully added RegexExtractor '{variable_name}' to sampler '{sampler_name}'"
                }
            else:
                return {
                    "success": False,
                    "error": f"Failed to add extractor - sampler '{sampler_name}' not found in JMX file"
                }
        except Exception as e:
            logger.error(f"Error adding regex extractor: {str(e)}")
            return {"success": False, "error": f"Failed to add extractor: {str(e)}"}
    
    @mcp.tool()
    def add_dynatrace_headers(jmx_path: str, application_name: str, transaction_controller_name: str, output_path: str = None) -> dict:
        """
        Add Dynatrace headers to all HTTP requests in a JMeter JMX file.
        
        This tool adds the following headers to each HTTP request (HTTPSamplerProxy):
        - LSN=<application_name>
        - TSN=<transaction_controller_name>
        
        Args:
            jmx_path: Path to the JMX file to modify
            application_name: Application name for the LSN (Load Service Name) header
            transaction_controller_name: Transaction name for the TSN (Transaction Service Name) header
            output_path: Optional output path for the updated JMX file (defaults to 'dynatrace_<original_filename>')
            
        Returns:
            dict: Results containing success status, updated sampler count, and output file path
        """
        try:
            return add_dynatrace_headers_to_jmx(jmx_path, application_name, transaction_controller_name, output_path)
        except Exception as e:
            logger.error(f"Error adding Dynatrace headers: {str(e)}")
            return {"success": False, "error": f"Failed to add Dynatrace headers: {str(e)}"}
    
    @mcp.tool()
    def auto_tag_dynatrace_headers(jmx_path: str, application_name: str, output_path: str = None) -> dict:
        """
        Automatically add Dynatrace headers to all HTTP requests in a JMeter JMX file with auto-detection of Transaction Controllers.
        
        This tool automatically detects Transaction Controllers and adds appropriate headers:
        - LSN=<application_name>
        - TSN=<TransactionControllerName> (auto-detected from the JMX hierarchy)
        
        For HTTP requests under a Transaction Controller, TSN will be set to that controller's name.
        For HTTP requests not under any Transaction Controller, TSN will be set to the sampler's own name.
        
        Args:
            jmx_path: Path to the JMX file to modify
            application_name: Application name for the LSN (Load Service Name) header
            output_path: Optional output path for the updated JMX file (defaults to 'dynatrace_auto_<original_filename>')
            
        Returns:
            dict: Results containing success status, updated sampler count, transaction mapping, and output file path
        """
        try:
            return auto_add_dynatrace_headers_to_jmx(jmx_path, application_name, output_path)
        except Exception as e:
            logger.error(f"Error auto-tagging Dynatrace headers: {str(e)}")
            return {"success": False, "error": f"Failed to auto-tag Dynatrace headers: {str(e)}"}
    
    @mcp.tool()
    def batch_analyze_logs(file_patterns: list, output_dir: str = None) -> dict:
        """Analyze multiple log files matching given patterns."""
        try:
            results = []
            
            for pattern in file_patterns:
                # Simple glob-like pattern matching
                from glob import glob
                matching_files = glob(pattern)
                
                for file_path in matching_files:
                    logger.info(f"Analyzing: {file_path}")
                    analysis = analyze_any_log(file_path)
                    
                    if not analysis.get('error'):
                        # Generate individual report
                        base_name = Path(file_path).stem
                        report_file = None
                        if output_dir:
                            report_file = os.path.join(output_dir, f"{base_name}_analysis.md")
                        
                        report_path = generate_comprehensive_report(analysis, report_file)
                        
                        results.append({
                            "file": file_path,
                            "format": analysis.get('format'),
                            "report": report_path,
                            "summary": {
                                "records": analysis.get('total_samples') or analysis.get('total_records') or analysis.get('total_lines', 0),
                                "errors": analysis.get('error_count', 0),
                                "insights": len(analysis.get('insights', []))
                            }
                        })
                    else:
                        results.append({
                            "file": file_path,
                            "error": analysis['error']
                        })
            
            return {
                "success": True,
                "analyzed_files": len(results),
                "results": results
            }
            
        except Exception as e:
            return {"success": False, "error": str(e)}

if __name__ == "__main__":
    # Automatically generate detailed analysis and graphs for the latest JTL file
    import glob
    jtl_files = sorted(glob.glob("*.jtl"), key=os.path.getmtime, reverse=True)
    if jtl_files:
        latest_jtl = jtl_files[0]
        print(f"\nGenerating detailed analysis and graphs for: {latest_jtl}\n")
        try:
            generate_jtl_graph(latest_jtl)
            print("\nGraph generation complete. Check the output image in your folder.")
        except Exception as e:
            print(f"Error generating graphs: {e}")
    else:
        print("No JTL files found in the current directory.")
    mcp = create_mcp_server()
    register_tools(mcp)
    mcp.run(transport='stdio')