/**
 * Google Cloud Trace types and utilities
 */
import * as traceAgent from '@google-cloud/trace-agent';
import { PluginTypes } from '@google-cloud/trace-agent';
import { getProjectId } from '../../utils/auth.js';
import { GcpMcpError } from '../../utils/error.js';
import { logger } from '../../utils/logger.js';

// Global trace client that can be reused
let traceClient: PluginTypes.Tracer | null = null;

/**
 * Trace span status
 */
export enum TraceStatus {
  OK = 'OK',
  ERROR = 'ERROR',
  UNSPECIFIED = 'UNSPECIFIED'
}

/**
 * Trace span data
 */
export interface TraceSpan {
  /** Span ID */
  spanId: string;
  /** Display name */
  displayName: string;
  /** Start time */
  startTime: string;
  /** End time */
  endTime: string;
  /** Span kind */
  kind: string;
  /** Status */
  status: TraceStatus;
  /** Parent span ID (if any) */
  parentSpanId?: string;
  /** Attributes/labels */
  attributes: Record<string, string>;
  /** Child spans */
  childSpans?: TraceSpan[];
}

/**
 * Trace data
 */
export interface TraceData {
  /** Trace ID */
  traceId: string;
  /** Project ID */
  projectId: string;
  /** Root spans */
  rootSpans: TraceSpan[];
  /** All spans (flat list) */
  allSpans: TraceSpan[];
}

/**
 * Gets the Google Cloud Trace client
 * 
 * @returns The trace client
 */
export function getTraceClient(): PluginTypes.Tracer {
  if (!traceClient) {
    try {
      // Initialize the trace agent
      traceClient = traceAgent.start({
        samplingRate: 1, // Sample all traces for now
        ignoreUrls: ['/health', '/readiness'], // Ignore health check endpoints
        serviceContext: {
          service: 'google-cloud-mcp',
          version: '0.1.0'
        }
      });
      
      // Successfully initialized Google Cloud Trace client
    } catch (error) {
      // Failed to initialize Google Cloud Trace client
      throw new GcpMcpError(
        'Failed to initialize Google Cloud Trace client',
        'INTERNAL',
        500
      );
    }
  }
  
  return traceClient;
}

/**
 * Formats trace data for display
 * 
 * @param traceData The trace data to format
 * @returns Formatted markdown string
 */
export function formatTraceData(traceData: TraceData): string {
  let markdown = `## Trace Details\n\n`;
  markdown += `- **Trace ID**: ${traceData.traceId}\n`;
  markdown += `- **Project ID**: ${traceData.projectId}\n`;
  markdown += `- **Total Spans**: ${traceData.allSpans.length}\n`;
  markdown += `- **Associated Logs**: [View logs for this trace](gcp-trace://${traceData.projectId}/traces/${traceData.traceId}/logs)\n\n`;
  
  // Add a summary of span types if we have them
  const spanTypes = new Map<string, number>();
  traceData.allSpans.forEach(span => {
    if (span.kind && span.kind !== 'UNSPECIFIED') {
      spanTypes.set(span.kind, (spanTypes.get(span.kind) || 0) + 1);
    }
  });
  
  if (spanTypes.size > 0) {
    markdown += `- **Span Types**:\n`;
    for (const [type, count] of spanTypes.entries()) {
      markdown += `  - ${type}: ${count}\n`;
    }
    markdown += `\n`;
  }
  
  // Format root spans and their children
  markdown += `## Trace Hierarchy\n\n`;
  
  for (const rootSpan of traceData.rootSpans) {
    markdown += formatSpanHierarchy(rootSpan, 0);
  }
  
  // Add section for failed spans if any
  const failedSpans = traceData.allSpans.filter(span => span.status === TraceStatus.ERROR);
  if (failedSpans.length > 0) {
    markdown += `\n## Failed Spans (${failedSpans.length})\n\n`;
    
    for (const span of failedSpans) {
      markdown += `- **${span.displayName}** (${span.spanId})\n`;
      markdown += `  - Start: ${new Date(span.startTime).toISOString()}\n`;
      markdown += `  - End: ${new Date(span.endTime).toISOString()}\n`;
      markdown += `  - Duration: ${calculateDuration(span.startTime, span.endTime)}\n`;
      
      // Add error details if available
      if (span.attributes['error.message']) {
        markdown += `  - Error: ${span.attributes['error.message']}\n`;
      }
      
      markdown += '\n';
    }
  }
  
  return markdown;
}

/**
 * Formats a span hierarchy for display
 * 
 * @param span The span to format
 * @param depth The current depth in the hierarchy
 * @returns Formatted markdown string
 */
function formatSpanHierarchy(span: TraceSpan, depth: number): string {
  const indent = '  '.repeat(depth);
  const statusEmoji = span.status === TraceStatus.ERROR ? '❌ ' : 
                     span.status === TraceStatus.OK ? '✅ ' : '⚪ ';
  
  // Format the span name with more details if available
  let displayText = span.displayName;
  
  // If the display name still contains 'Unknown Span', try to enhance it with attributes
  if (displayText.includes('Unknown Span')) {
    // Try to add more context from attributes if the name is unknown
    const contextAttributes = [];
    
    // Check for common HTTP attributes
    if (span.attributes['/http/method']) contextAttributes.push(span.attributes['/http/method']);
    if (span.attributes['/http/path']) contextAttributes.push(span.attributes['/http/path']);
    if (span.attributes['/http/url']) contextAttributes.push(span.attributes['/http/url']);
    if (span.attributes['/http/status_code']) contextAttributes.push(`Status: ${span.attributes['/http/status_code']}`);
    
    // Check for component or service name
    if (span.attributes['/component']) contextAttributes.push(`Component: ${span.attributes['/component']}`);
    if (span.attributes['service.name']) contextAttributes.push(`Service: ${span.attributes['service.name']}`);
    
    // Check for database attributes
    if (span.attributes['/db/system']) contextAttributes.push(`DB: ${span.attributes['/db/system']}`);
    if (span.attributes['/db/operation']) contextAttributes.push(span.attributes['/db/operation']);
    
    // Check for agent information
    if (span.attributes['g.co/agent']) contextAttributes.push(`Agent: ${span.attributes['g.co/agent']}`);
    
    // Add any context found to the display text
    if (contextAttributes.length > 0) {
      displayText = `${displayText} (${contextAttributes.join(' | ')})`;
    }
  }
  
  // Format the span display line with ID and status
  let markdown = `${indent}- ${statusEmoji}**${displayText}**\n`;
  markdown += `${indent}  - Span ID: ${span.spanId}\n`;
  
  // Add timing information
  if (span.startTime && span.endTime) {
    const duration = calculateDuration(span.startTime, span.endTime);
    markdown += `${indent}  - Duration: ${duration}\n`;
    
    // Add start and end times if they exist
    const startDate = new Date(span.startTime);
    const endDate = new Date(span.endTime);
    if (!isNaN(startDate.getTime()) && !isNaN(endDate.getTime())) {
      // Format dates in a more readable format
      const formatDate = (date: Date) => {
        return date.toISOString().replace('T', ' ').replace('Z', '');
      };
      
      markdown += `${indent}  - Start: ${formatDate(startDate)}\n`;
      markdown += `${indent}  - End: ${formatDate(endDate)}\n`;
    }
  }
  
  // Add span kind if it's not the default
  if (span.kind && span.kind !== 'UNSPECIFIED') {
    markdown += `${indent}  - Kind: ${span.kind}\n`;
  }
  
  // Add error information if present
  if (span.status === TraceStatus.ERROR) {
    markdown += `${indent}  - Status: ERROR\n`;
    
    // Look for error details in attributes
    const errorMessage = span.attributes['/error/message'] || 
                        span.attributes['error.message'] || 
                        span.attributes['error'];
    
    if (errorMessage) {
      markdown += `${indent}  - Error: ${errorMessage}\n`;
    }
  }
  
  // Add all attributes, prioritizing important ones
  const importantAttributes = [
    // HTTP attributes
    '/http/method', '/http/url', '/http/status_code', '/http/host', '/http/path', '/http/route', '/http/user_agent',
    'http.method', 'http.url', 'http.status_code', 'http.host', 'http.path', 'http.route', 'http.user_agent',
    
    // Error attributes
    '/error/message', 'error.message', 'error',
    
    // Database attributes
    '/db/system', '/db/name', '/db/operation', '/db/statement',
    'db.system', 'db.name', 'db.operation', 'db.statement',
    
    // Service and component attributes
    'service.name', 'span.kind', 'component', '/component',
    
    // Google Cloud specific attributes
    'g.co/agent', 'g.co/gae/app/module', 'g.co/gae/app/version', 'g.co/gce/instance_id'
  ];
  
  // First show important attributes
  const importantAttributeEntries = Object.entries(span.attributes)
    .filter(([key]) => importantAttributes.includes(key));
  
  // Then show other attributes
  const otherAttributeEntries = Object.entries(span.attributes)
    .filter(([key]) => !importantAttributes.includes(key));
  
  if (importantAttributeEntries.length > 0 || otherAttributeEntries.length > 0) {
    markdown += `${indent}  - Attributes:\n`;
    
    // Show important attributes first
    for (const [key, value] of importantAttributeEntries) {
      markdown += `${indent}    - ${key}: ${value}\n`;
    }
    
    // Show other attributes (up to a reasonable limit)
    const maxOtherAttributes = 5;
    const shownOtherAttributes = otherAttributeEntries.slice(0, maxOtherAttributes);
    
    for (const [key, value] of shownOtherAttributes) {
      markdown += `${indent}    - ${key}: ${value}\n`;
    }
    
    // Indicate if there are more attributes
    if (otherAttributeEntries.length > maxOtherAttributes) {
      const remaining = otherAttributeEntries.length - maxOtherAttributes;
      markdown += `${indent}    - ... ${remaining} more attributes\n`;
    }
  }
  
  // Format child spans
  if (span.childSpans && span.childSpans.length > 0) {
    // Sort child spans by start time if available
    const sortedChildren = [...span.childSpans];
    sortedChildren.sort((a, b) => {
      if (a.startTime && b.startTime) {
        return new Date(a.startTime).getTime() - new Date(b.startTime).getTime();
      }
      return 0;
    });
    
    for (const childSpan of sortedChildren) {
      markdown += formatSpanHierarchy(childSpan, depth + 1);
    }
  }
  
  return markdown;
}

/**
 * Calculates the duration between two timestamps
 * 
 * @param startTime The start time
 * @param endTime The end time
 * @returns Formatted duration string
 */
function calculateDuration(startTime: string, endTime: string): string {
  const start = new Date(startTime).getTime();
  const end = new Date(endTime).getTime();
  const durationMs = end - start;
  
  if (durationMs < 1000) {
    return `${durationMs}ms`;
  } else if (durationMs < 60000) {
    return `${(durationMs / 1000).toFixed(2)}s`;
  } else {
    const minutes = Math.floor(durationMs / 60000);
    const seconds = ((durationMs % 60000) / 1000).toFixed(2);
    return `${minutes}m ${seconds}s`;
  }
}

/**
 * Builds a hierarchical trace structure from flat spans
 * 
 * @param projectId The Google Cloud project ID
 * @param traceId The trace ID
 * @param spans The flat list of spans
 * @returns Structured trace data
 */
export function buildTraceHierarchy(
  projectId: string,
  traceId: string,
  spans: any[]
): TraceData {
  // Log the raw spans for debugging
  logger.debug(`Building trace hierarchy for trace ${traceId} with ${spans.length} spans`);
  
  // Debug: Log the structure of the first span to understand the format
  if (spans.length > 0) {
    logger.debug(`First span structure: ${JSON.stringify(spans[0], null, 2)}`);
    logger.debug(`First span keys: ${Object.keys(spans[0]).join(', ')}`);
  }
  
  // Map to store spans by ID for quick lookup
  const spanMap = new Map<string, TraceSpan>();
  
  // Convert raw spans to our TraceSpan format
  logger.debug('Converting spans to TraceSpan format...');
  const traceSpans: TraceSpan[] = spans.map((span, index) => {
    // Extract span data - ensure we have a valid spanId
    const spanId = span.spanId || '';
    logger.debug(`Processing span ${index} with ID: ${spanId}`);
    
    // Handle different display name formats in v1 API
    // According to https://cloud.google.com/trace/docs/reference/v1/rest/v1/projects.traces#TraceSpan
    let displayName = 'Unknown Span';
    
    // Debug: Log the name-related fields
    logger.debug(`Span ${spanId} name fields:`);
    logger.debug(`- name: ${span.name || 'undefined'}`);
    logger.debug(`- displayName: ${typeof span.displayName === 'object' ? JSON.stringify(span.displayName) : span.displayName || 'undefined'}`);
    
    // In v1 API, the name field is the actual span name
    if (span.name) {
      displayName = span.name;
      logger.debug(`Using span.name: ${displayName}`);
    } else if (span.displayName?.value) {
      displayName = span.displayName.value;
      logger.debug(`Using span.displayName.value: ${displayName}`);
    } else if (typeof span.displayName === 'string') {
      displayName = span.displayName;
      logger.debug(`Using span.displayName string: ${displayName}`);
    }
    
    // For v1 API, check if the name is a full URL path (common in Google Cloud Trace)
    if (displayName.startsWith('/')) {
      // This is likely an HTTP path
      displayName = `HTTP ${displayName}`;
    }
    
    // If we still have an unknown span, try to extract a meaningful name from labels
    if (displayName === 'Unknown Span') {
      // Try to extract from common label patterns in Google Cloud Trace v1 API
      if (span.labels) {
        // Common Google Cloud Trace labels
        if (span.labels['/http/path']) {
          const method = span.labels['/http/method'] || '';
          displayName = `${method} ${span.labels['/http/path']}`;
        } else if (span.labels['/http/url']) {
          displayName = `HTTP ${span.labels['/http/url']}`;
        } else if (span.labels['/http/status_code']) {
          displayName = `HTTP Status: ${span.labels['/http/status_code']}`;
        } else if (span.labels['/component']) {
          displayName = `Component: ${span.labels['/component']}`;
        } else if (span.labels['/db/statement']) {
          const dbSystem = span.labels['/db/system'] || 'DB';
          displayName = `${dbSystem}: ${span.labels['/db/statement'].substring(0, 30)}...`;
        } else if (span.labels['g.co/agent']) {
          displayName = `Agent: ${span.labels['g.co/agent']}`;
        } else if (span.labels['g.co/gae/app/module']) {
          displayName = `GAE Module: ${span.labels['g.co/gae/app/module']}`;
        } else if (span.labels['g.co/gae/app/version']) {
          displayName = `GAE Version: ${span.labels['g.co/gae/app/version']}`;
        } else if (span.labels['g.co/gce/instance_id']) {
          displayName = `GCE Instance: ${span.labels['g.co/gce/instance_id']}`;
        }
        
        // If still unknown, check for any label that might be descriptive
        if (displayName === 'Unknown Span') {
          // Look for descriptive labels
          const descriptiveLabels = Object.entries(span.labels)
            .filter(([key, value]) => 
              typeof value === 'string' && 
              !key.startsWith('/') && 
              !key.startsWith('g.co/') &&
              value.length < 50
            );
          
          if (descriptiveLabels.length > 0) {
            // Use the first descriptive label
            const [key, value] = descriptiveLabels[0];
            displayName = `${key}: ${value}`;
          }
        }
      }
      
      // Try alternative fields from the span object
      if (displayName === 'Unknown Span') {
        // Check for operation name (common in Cloud Trace)
        if (span.operation && span.operation.name) {
          displayName = `Operation: ${span.operation.name}`;
        }
        
        // Check for any other descriptive fields
        const possibleNameFields = ['operationName', 'description', 'type', 'method', 'rpcName', 'kind'];
        for (const field of possibleNameFields) {
          if (span[field] && typeof span[field] === 'string') {
            displayName = `${field}: ${span[field]}`;
            break;
          }
        }
      }
    }
    
    // If we still have an unknown span, include the span ID in the display name
    if (displayName === 'Unknown Span' && spanId) {
      displayName = `Unknown Span (ID: ${spanId})`;
    }
    
    // Extract timestamps - in v1 API these are RFC3339 strings
    const startTime = span.startTime || '';
    const endTime = span.endTime || '';
    const parentSpanId = span.parentSpanId || '';
    
    // Extract span kind - in v1 API, this might be in different formats
    let kind = 'UNSPECIFIED';
    if (span.kind) {
      kind = span.kind;
    } else if (span.spanKind) {
      kind = span.spanKind;
    }
    
    // In v1 API, the span kind might be encoded in labels
    if (kind === 'UNSPECIFIED' && span.labels) {
      if (span.labels['/span/kind']) {
        kind = span.labels['/span/kind'];
      } else if (span.labels['span.kind']) {
        kind = span.labels['span.kind'];
      }
    }
    
    // Extract status - in v1 API this might be in different formats
    let status = TraceStatus.UNSPECIFIED;
    if (span.status) {
      if (span.status.code === 0) {
        status = TraceStatus.OK;
      } else if (span.status.code > 0) {
        status = TraceStatus.ERROR;
      }
    }
    
    // In v1 API, error status might be in labels
    if (status === TraceStatus.UNSPECIFIED && span.labels) {
      if (span.labels['/error/message'] || span.labels['error']) {
        status = TraceStatus.ERROR;
      } else if (span.labels['/http/status_code']) {
        const statusCode = parseInt(span.labels['/http/status_code'], 10);
        if (statusCode >= 400) {
          status = TraceStatus.ERROR;
        } else if (statusCode >= 200 && statusCode < 400) {
          status = TraceStatus.OK;
        }
      }
    }
    
    // Extract attributes/labels - handle both v1 and v2 formats
    const attributes: Record<string, string> = {};
    
    // Debug: Log label information
    logger.debug(`Span ${spanId} labels:`);
    logger.debug(`- Has labels: ${!!span.labels}`);
    if (span.labels) {
      logger.debug(`- Label keys: ${Object.keys(span.labels).join(', ')}`);
    }
    
    // Handle v1 API format (labels)
    if (span.labels) {
      logger.debug(`Processing ${Object.keys(span.labels).length} labels for span ${spanId}`);
      for (const [key, value] of Object.entries(span.labels)) {
        if (value !== undefined && value !== null) {
          attributes[key] = String(value);
        }
      }
    }
    
    // Also handle v2 API format (attributes.attributeMap)
    if (span.attributes && span.attributes.attributeMap) {
      for (const [key, value] of Object.entries(span.attributes.attributeMap)) {
        if (value !== undefined && value !== null) {
          attributes[key] = (value as any)?.stringValue || 
                          String((value as any)?.intValue || 
                          (value as any)?.boolValue || '');
        }
      }
    }
    
    // Handle any other fields that might contain useful information
    for (const [key, value] of Object.entries(span)) {
      // Skip keys we've already processed or that are part of the standard span structure
      if (['spanId', 'name', 'displayName', 'startTime', 'endTime', 'parentSpanId', 
           'kind', 'status', 'labels', 'attributes', 'childSpans'].includes(key)) {
        continue;
      }
      
      // Add any other fields as attributes
      if (value !== undefined && value !== null) {
        if (typeof value !== 'object') {
          attributes[`raw.${key}`] = String(value);
        } else if (!Array.isArray(value)) {
          // For simple objects, flatten one level
          try {
            attributes[`raw.${key}`] = JSON.stringify(value).substring(0, 100);
            if (JSON.stringify(value).length > 100) {
              attributes[`raw.${key}`] += '...';
            }
          } catch (e) {
            // If we can't stringify, just note that it exists
            attributes[`raw.${key}`] = '[Complex Object]';
          }
        }
      }
    }
    
    // Create the trace span
    const traceSpan: TraceSpan = {
      spanId,
      displayName,
      startTime,
      endTime,
      kind,
      status,
      attributes,
      childSpans: []
    };
    
    if (parentSpanId) {
      traceSpan.parentSpanId = parentSpanId;
      logger.debug(`Span ${spanId} has parent: ${parentSpanId}`);
    } else {
      logger.debug(`Span ${spanId} has no parent (will be a root span)`);
    }
    
    // Debug: Log the final display name
    logger.debug(`Final display name for span ${spanId}: "${displayName}"`);
    
    // Store in map for quick lookup
    spanMap.set(spanId, traceSpan);
    
    return traceSpan;
  });
  
  // Build the hierarchy
  const rootSpans: TraceSpan[] = [];
  
  logger.debug(`Building trace hierarchy for ${traceSpans.length} spans...`);
  logger.debug(`Span map contains ${spanMap.size} spans`);
  
  for (const span of traceSpans) {
    logger.debug(`Processing hierarchy for span ${span.spanId} (${span.displayName})`);
    
    if (span.parentSpanId) {
      logger.debug(`Span ${span.spanId} has parent ${span.parentSpanId}`);
      // This is a child span, add it to its parent
      const parentSpan = spanMap.get(span.parentSpanId);
      if (parentSpan) {
        logger.debug(`Found parent span ${span.parentSpanId} for child ${span.spanId}`);
        if (!parentSpan.childSpans) {
          parentSpan.childSpans = [];
          logger.debug(`Initialized childSpans array for parent ${span.parentSpanId}`);
        }
        parentSpan.childSpans.push(span);
        logger.debug(`Added span ${span.spanId} as child of ${span.parentSpanId}`);
      } else {
        // Parent not found, treat as root
        logger.debug(`Parent ${span.parentSpanId} not found for span ${span.spanId}, treating as root`);
        rootSpans.push(span);
      }
    } else {
      // This is a root span
      logger.debug(`Span ${span.spanId} has no parent, adding as root span`);
      rootSpans.push(span);
    }
  }
  
  // Sort child spans by start time
  for (const span of traceSpans) {
    if (span.childSpans && span.childSpans.length > 0) {
      logger.debug(`Sorting ${span.childSpans.length} child spans for parent ${span.spanId}`);
      span.childSpans.sort((a, b) => {
        return new Date(a.startTime).getTime() - new Date(b.startTime).getTime();
      });
    }
  }
  
  // Debug: Log the root spans and their children
  logger.debug(`Final hierarchy: ${rootSpans.length} root spans`);
  for (const rootSpan of rootSpans) {
    logger.debug(`Root span: ${rootSpan.spanId} (${rootSpan.displayName})`);
    logger.debug(`- Has ${rootSpan.childSpans?.length || 0} direct children`);
    
    // Count total descendants
    let totalDescendants = 0;
    const countDescendants = (span: TraceSpan) => {
      if (span.childSpans) {
        totalDescendants += span.childSpans.length;
        for (const child of span.childSpans) {
          countDescendants(child);
        }
      }
    };
    countDescendants(rootSpan);
    logger.debug(`- Total descendants: ${totalDescendants}`);
  }
  
  return {
    traceId,
    projectId,
    rootSpans,
    allSpans: traceSpans
  };
}

/**
 * Extracts a trace ID from a log entry if present
 * 
 * @param logEntry The log entry to check
 * @returns The trace ID if found, otherwise undefined
 */
export function extractTraceIdFromLog(logEntry: any): string | undefined {
  // Check for trace in the standard logging.googleapis.com/trace field
  if (logEntry.trace) {
    // The trace field is typically in the format "projects/PROJECT_ID/traces/TRACE_ID"
    const match = logEntry.trace.match(/traces\/([a-f0-9]+)$/i);
    if (match && match[1]) {
      return match[1];
    }
  }
  
  // Check for trace in labels
  if (logEntry.labels && logEntry.labels['logging.googleapis.com/trace']) {
    const traceLabel = logEntry.labels['logging.googleapis.com/trace'];
    const match = traceLabel.match(/traces\/([a-f0-9]+)$/i);
    if (match && match[1]) {
      return match[1];
    }
  }
  
  // Check for trace in jsonPayload
  if (logEntry.jsonPayload) {
    if (logEntry.jsonPayload.traceId) {
      return logEntry.jsonPayload.traceId;
    }
    
    if (logEntry.jsonPayload['logging.googleapis.com/trace']) {
      const tracePayload = logEntry.jsonPayload['logging.googleapis.com/trace'];
      const match = tracePayload.match(/traces\/([a-f0-9]+)$/i);
      if (match && match[1]) {
        return match[1];
      }
    }
  }
  
  return undefined;
}
