import { v4 as uuidv4 } from 'uuid';
import { Node, Edge, ModelData } from '../store/store';
import { EDGE_STYLING, LAYOUT, NODE_DIMENSIONS, VIEWPORT } from '../constants/dimensions';
import {
  classifyNodes,
  findStartNodes,
  calculateLockedBounds,
  createNodeCollisionBounds,
  buildGraphData,
  determineStartPosition,
} from './layoutHelpers';
import {
  processFlowLayout,
  convertPositionsToCoordinates,
  groupNodesByRow,
  optimizeRowAlignment,
} from './layoutStrategies';

// Type definitions for React Flow
interface Position {
  x: number;
  y: number;
}

interface Viewport {
  x: number;
  y: number;
  zoom: number;
}

interface BoundingBox {
  x: number;
  y: number;
  width: number;
  height: number;
}

interface ComponentSuccessor {
  id: string;
  conditionId?: string;
  [key: string]: any;
}

interface _Component {
  id: string;
  type: string;
  plugin?: string;
  label?: string;
  configuration?: { [key: string]: any };
  successors?: ComponentSuccessor[];
  position?: Position;
  locked?: boolean;
  annotation?: string;
  [key: string]: any;
}

/**
 * Map node type back to component type constant
 */
function getComponentTypeFromNodeType(nodeType: string): string {
  switch (nodeType) {
    case 'start':
      return '1'; // Api::COMPONENT_TYPE_START
    case 'subprocess':
      return '2'; // Api::COMPONENT_TYPE_SUBPROCESS  
    case 'element':
      return '4'; // Api::COMPONENT_TYPE_ELEMENT
    case 'gateway':
      return '6'; // Api::COMPONENT_TYPE_GATEWAY
    default:
      return '4'; // Default to element
  }
}

/**
 * Map component category to component type constant
 */
export function getComponentTypeFromCategory(category: string): string {
  switch (category) {
    case 'Events':
    case 'Triggers':
      return '1'; // Api::COMPONENT_TYPE_START
    case 'Actions':
    case 'Tasks':
      return '4'; // Api::COMPONENT_TYPE_ELEMENT  
    case 'Conditions':
    case 'Decisions':
      return '5'; // Api::COMPONENT_TYPE_LINK
    case 'Gateways':
      return '6'; // Api::COMPONENT_TYPE_GATEWAY
    default:
      return '4'; // Default to element
  }
}

/**
 * Parse model data from Drupal JSON format to React Flow format
 */
export function parseModelData(modelData: ModelData | string | null): { nodes: Node[], edges: Edge[], modelData: ModelData | null } {
  if (!modelData) {
    return { nodes: [], edges: [], modelData: null };
  }

  const data = typeof modelData === 'string' ? JSON.parse(modelData) : modelData;
  
  const nodes = (data.nodes || []).map((node: any) => ({
    id: String(node.id), // Ensure ID is string
    type: node.type || 'element',
    position: node.position || { x: LAYOUT.DEFAULT_POSITION_X, y: LAYOUT.DEFAULT_POSITION_Y },
    data: {
      label: node.label || node.id,
      plugin: node.plugin,
      configuration: node.configuration || {},
      configured: !!node.configuration && Object.keys(node.configuration).length > 0,
      componentType: node.category ? getComponentTypeFromCategory(node.category) : getComponentTypeFromNodeType(node.type),
      locked: node.locked || false, // Add locked property, default to false
      category: node.category, // Preserve category information
      ...node,
    },
  }));

  // Create a set of valid node IDs for edge validation
  const nodeIds = new Set(nodes.map((n: any) => n.id));
  
  // Track gateway outputs to assign handles properly
  const gatewayOutputs = new Map();
  
  // Filter edges to only include those that reference existing nodes
  const edges = (data.edges || [])
    .filter((edge: any, _index: number) => {
      const sourceExists = nodeIds.has(String(edge.source));
      const targetExists = nodeIds.has(String(edge.target));
      return sourceExists && targetExists;
    })
    .map((edge: any, index: number) => {
      // Ensure unique edge ID
      const edgeId = edge.id || `${edge.source}-${edge.target}-${index}`;
      
      // Find the source node to determine correct handle
      const sourceNode = nodes.find((n: any) => n.id === String(edge.source));
      let sourceHandle = 'output'; // Default for element/start nodes
      
      // Gateway nodes have different handles
      if (sourceNode && sourceNode.type === 'gateway') {
        const sourceId = String(edge.source);
        
        // Track how many edges we've seen from this gateway
        if (!gatewayOutputs.has(sourceId)) {
          gatewayOutputs.set(sourceId, 0);
        }
        
        const outputIndex = gatewayOutputs.get(sourceId);
        gatewayOutputs.set(sourceId, outputIndex + 1);
        
        // Assign handle based on output index: first = true, second = false
        sourceHandle = outputIndex === 0 ? 'true' : 'false';
      }
      
      // Determine edge type based on content
      let edgeType = 'default';
      const hasCondition = edge.condition || edge.conditionLabel || edge.conditionConfiguration;
      const hasAnnotation = edge.annotation;
      
      if (hasCondition && hasAnnotation) {
        edgeType = 'condition'; // Condition edge can also display annotations
      } else if (hasCondition) {
        edgeType = 'condition';
      } else if (hasAnnotation) {
        edgeType = 'annotation';
      }
      
      return {
        id: edgeId,
        source: String(edge.source),
        target: String(edge.target),
        sourceHandle: sourceHandle,
        targetHandle: 'input',
        type: edgeType,
        animated: false,
        label: edge.conditionLabel || edge.condition || '',
        markerEnd: {
          type: 'arrow',
          width: EDGE_STYLING.ARROW_WIDTH,
          height: EDGE_STYLING.ARROW_HEIGHT,
          color: 'var(--modeler-color-edge-stroke)',
          strokeWidth: EDGE_STYLING.STROKE_WIDTH,
        },
        style: {
          stroke: 'var(--modeler-color-edge-stroke)',
          strokeWidth: EDGE_STYLING.STROKE_WIDTH,
        },
        pathOptions: {
          offset: EDGE_STYLING.CONTROL_OFFSET,
          borderRadius: EDGE_STYLING.BORDER_RADIUS,
        },
        data: {
          condition: edge.condition,
          locked: edge.locked || false, // Add locked property, default to false
          controlOffset: edge.controlOffset || { x: 0, y: 0 }, // Add control point offset
          ...edge,
        },
      };
    });

  // Apply auto-layout if all unlocked nodes have the same position (indicating they need layout)
  const unlockedNodes = nodes.filter((node: any) => node.data?.locked !== true);
  const needsLayout = unlockedNodes.length > 1 && unlockedNodes.every((n: any) => n.position.x === LAYOUT.DEFAULT_POSITION_X && n.position.y === LAYOUT.DEFAULT_POSITION_Y);
  if (needsLayout) {
    const layoutedNodes = autoLayout(nodes, edges);
    return { nodes: layoutedNodes || nodes, edges, modelData: data };
  }
  
  return { nodes, edges, modelData: data };
}

/**
 * Filter out internal properties (starting with _) from configuration
 */
function filterInternalProperties(config: Record<string, any> | undefined): Record<string, any> {
  if (!config) return {};
  return Object.fromEntries(
    Object.entries(config).filter(([key]) => !key.startsWith('_'))
  );
}

/**
 * Convert a label to snake_case for use as model ID
 */
export function labelToSnakeCase(label: string): string {
  return label
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9\s_-]/g, '') // Remove special characters except spaces, underscores, hyphens
    .replace(/[\s-]+/g, '_')       // Replace spaces and hyphens with underscores
    .replace(/_+/g, '_')           // Collapse multiple underscores
    .replace(/^_|_$/g, '');        // Remove leading/trailing underscores
}

/**
 * Generate a model ID from label or create a unique fallback
 */
function generateModelId(metadata: any): string {
  // If ID is already provided, use it
  if (metadata.id) {
    return metadata.id;
  }

  // If label is provided, generate ID from it
  if (metadata.label && metadata.label !== 'Untitled Model') {
    const snakeCaseId = labelToSnakeCase(metadata.label);
    if (snakeCaseId) {
      return snakeCaseId;
    }
  }

  // Fallback to UUID
  return uuidv4();
}

/**
 * Export model data from React Flow format to Drupal JSON format
 */
export function exportModelData(nodes: any[], edges: any[], metadata: any = {}) {
  const modelData = {
    id: generateModelId(metadata),
    version: metadata.version || '1.0.0',
    metadata: {
      label: metadata.label || 'Untitled Model',
      description: metadata.description || '',
      executable: metadata.executable !== false,
      tags: metadata.tags || [],
      changelog: metadata.changelog || '',
      ...metadata,
    },
    nodes: nodes.map(node => ({
      id: node.id,
      type: node.type,
      plugin: node.data.plugin,
      label: node.data.label,
      position: {
        x: Math.round(node.position.x),
        y: Math.round(node.position.y),
      },
      configuration: filterInternalProperties(node.data.configuration),
      locked: node.data.locked || false, // Include locked property in export
      annotation: node.data.annotation || '', // Include annotation in export
    })),
    edges: edges.map(edge => ({
      id: edge.id,
      source: edge.source,
      target: edge.target,
      sourceHandle: edge.sourceHandle,
      targetHandle: edge.targetHandle,
      condition: edge.data?.condition || edge.label || '',
      conditionLabel: edge.data?.conditionLabel || '',
      conditionConfiguration: edge.data?.conditionConfiguration || {},
      locked: edge.data?.locked || false, // Include locked property in export
      annotation: edge.data?.annotation || '', // Include annotation in export
      controlOffset: edge.data?.controlOffset || { x: 0, y: 0 }, // Include control point offset
    })),
  };

  return modelData;
}

/**
 * Generate automatic layout for nodes based on edge flow
 * Refactored into smaller, maintainable functions
 */
export function autoLayout(nodes: Node[], edges: Edge[]): Node[] | null {
  // Safety checks
  if (!nodes || nodes.length === 0) return null;
  if (!edges) edges = []; // Default to empty array if edges is undefined
  
  const layoutNodes = [...nodes];
  
  // Step 1: Classify nodes into locked and unlocked groups
  const { lockedNodes, unlockedNodes } = classifyNodes(layoutNodes);
  
  // Step 2: Calculate bounds for locked nodes
  const lockedBounds = calculateLockedBounds(lockedNodes);
  
  // Step 3: Determine starting position for layout
  const startPos = determineStartPosition(lockedBounds);
  
  // Step 4: Create collision bounds for locked nodes
  const lockedNodeBounds = createNodeCollisionBounds(lockedNodes);

  // Step 5: Build graph data structure
  const graphData = buildGraphData(unlockedNodes, edges);

  // Step 6: Find start nodes for layout
  const actualStartNodes = findStartNodes(unlockedNodes, graphData.inDegree);

  // Step 7: Process flow layout to determine row/column positions
  const nodePositions = processFlowLayout(actualStartNodes, unlockedNodes, graphData);
  
  // Step 8: Convert positions to actual coordinates
  const nodesWithCoordinates = convertPositionsToCoordinates(
    layoutNodes,
    nodePositions,
    startPos,
    lockedNodeBounds
  );
  
  // Step 9: Group nodes by row for optimization
  const rowNodes = groupNodesByRow(nodePositions, nodesWithCoordinates);
  
  // Step 10: Optimize alignment within rows
  optimizeRowAlignment(nodesWithCoordinates, edges, rowNodes, lockedNodeBounds);
  
  return nodesWithCoordinates;
}

/**
 * Calculate bounds for a subset of nodes
 * @param {Array} nodes - Array of nodes to calculate bounds for
 * @returns {Object} Bounds object with x, y, width, height
 */
function getNodesBounds(nodes: Node[]): BoundingBox {
  if (!nodes || nodes.length === 0) {
    return { x: 0, y: 0, width: 0, height: 0 };
  }

  const nodeWidth = NODE_DIMENSIONS.DEFAULT_WIDTH; // Estimated width
  const nodeHeight = 80; // Estimated height (no specific constant for this)

  let minX = Infinity;
  let minY = Infinity;
  let maxX = -Infinity;
  let maxY = -Infinity;

  nodes.forEach((node: Node) => {
    const x = node.position?.x || 0;
    const y = node.position?.y || 0;
    const width = node.width || nodeWidth;
    const height = node.height || nodeHeight;
    
    minX = Math.min(minX, x);
    minY = Math.min(minY, y);
    maxX = Math.max(maxX, x + width);
    maxY = Math.max(maxY, y + height);
  });
  
  return {
    x: minX,
    y: minY,
    width: maxX - minX,
    height: maxY - minY
  };
}

/**
 * Calculate viewport for fitting specific nodes
 * @param {Array} nodes - Nodes to fit
 * @param {number} viewportWidth - Width of viewport
 * @param {number} viewportHeight - Height of viewport
 * @param {number} padding - Padding factor (0.1 = 10% padding)
 * @returns {Object} Viewport object with x, y, zoom
 */
export function getFitViewport(nodes: Node[], viewportWidth: number, viewportHeight: number, padding = 0.1): Viewport {
  const bounds = getNodesBounds(nodes);
  
  if (bounds.width === 0 || bounds.height === 0) {
    return { x: 0, y: 0, zoom: 1 };
  }
  
  // Calculate zoom to fit bounds with padding
  const paddingX = bounds.width * padding;
  const paddingY = bounds.height * padding;
  const zoomX = viewportWidth / (bounds.width + paddingX * 2);
  const zoomY = viewportHeight / (bounds.height + paddingY * 2);
  const zoom = Math.min(zoomX, zoomY, VIEWPORT.MAX_ZOOM);
  
  // Calculate position to center the bounds
  const x = -bounds.x * zoom + (viewportWidth - bounds.width * zoom) / 2;
  const y = -bounds.y * zoom + (viewportHeight - bounds.height * zoom) / 2;
  
  return { x, y, zoom };
}