import { Workspace,WorkspaceNode,WorkspaceViewMode } from './models';

export type SplitDirection = 'horizontal' | 'vertical';
type SplitPosition = 'left' | 'right' | 'top' | 'bottom';

export type SplitHint = {
  direction: SplitDirection;
  position: SplitPosition;
  targetSessionId?: string;
};

export const pruneWorkspaceNode = (node: WorkspaceNode, targetSessionId: string): WorkspaceNode | null => {
  if (node.type === 'pane') {
    return node.sessionId === targetSessionId ? null : node;
  }

  const nextChildren: WorkspaceNode[] = [];
  const nextSizes: number[] = [];
  const sizeList = node.sizes && node.sizes.length === node.children.length
    ? node.sizes
    : node.children.map(() => 1 / node.children.length);
  let removedDirectChild = false;

  node.children.forEach((child, idx) => {
    const pruned = pruneWorkspaceNode(child, targetSessionId);
    if (pruned) {
      nextChildren.push(pruned);
      nextSizes.push(sizeList[idx] ?? 1 / node.children.length);
    } else {
      removedDirectChild = true;
    }
  });

  if (nextChildren.length === 0) return null;
  if (nextChildren.length === 1) return nextChildren[0];

  // Only rebalance siblings to equal sizes when this level actually
  // lost one of its direct children. If the prune happened deeper in
  // one branch, this split's direct children are unchanged and their
  // original ratios must be preserved (otherwise e.g. a root 0.8/0.2
  // split gets rewritten to 0.5/0.5 when a grand-child pane closes).
  if (removedDirectChild) {
    const equalSize = 1 / nextChildren.length;
    return { ...node, children: nextChildren, sizes: nextChildren.map(() => equalSize) };
  }

  // Preserve existing ratios; normalise defensively in case sibling
  // subtrees changed shape (e.g. a split collapsed to a single pane).
  const total = nextSizes.reduce((acc, n) => acc + n, 0) || 1;
  const normalized = nextSizes.map(n => n / total);
  return { ...node, children: nextChildren, sizes: normalized };
};

/**
 * Append a new pane containing `sessionId` to the end of the workspace
 * root's split. If the root already splits in the requested direction,
 * the new pane becomes its last sibling and all sibling sizes are reset
 * to equal. Otherwise the root is wrapped in a new split (same behaviour
 * as the existing `insertPaneIntoWorkspace(root, id, { targetSessionId:
 * undefined })` path) with two equal children.
 */
export const appendPaneToWorkspaceRoot = (
  root: WorkspaceNode,
  sessionId: string,
  direction: SplitDirection = 'vertical',
): WorkspaceNode => {
  const newPane: WorkspaceNode = { id: crypto.randomUUID(), type: 'pane', sessionId };

  if (root.type === 'split' && root.direction === direction) {
    const nextChildren = [...root.children, newPane];
    const equalSize = 1 / nextChildren.length;
    return {
      ...root,
      children: nextChildren,
      sizes: nextChildren.map(() => equalSize),
    };
  }

  return {
    id: crypto.randomUUID(),
    type: 'split',
    direction,
    children: [root, newPane],
    sizes: [0.5, 0.5],
  };
};

const createSplitFromPane = (
  existingPane: WorkspaceNode,
  newPane: WorkspaceNode,
  hint: SplitHint
): WorkspaceNode => {
  const children = (hint.position === 'left' || hint.position === 'top') ? [newPane, existingPane] : [existingPane, newPane];
  return {
    id: crypto.randomUUID(),
    type: 'split',
    direction: hint.direction,
    children,
    sizes: [1, 1],
  };
};

export const insertPaneIntoWorkspace = (
  root: WorkspaceNode,
  sessionId: string,
  hint: SplitHint
): WorkspaceNode => {
  const pane: WorkspaceNode = { id: crypto.randomUUID(), type: 'pane', sessionId };

  if (!hint.targetSessionId) {
    const children = (hint.position === 'left' || hint.position === 'top') ? [pane, root] : [root, pane];
    return {
      id: crypto.randomUUID(),
      type: 'split',
      direction: hint.direction,
      children,
      sizes: [1, 1],
    };
  }

  const insertPane = (node: WorkspaceNode): WorkspaceNode => {
    if (node.type === 'pane' && node.sessionId === hint.targetSessionId) {
      return createSplitFromPane(node, pane, hint);
    }
    if (node.type === 'split') {
      return { ...node, children: node.children.map(child => insertPane(child)) };
    }
    return node;
  };

  return insertPane(root);
};

export const createWorkspaceFromSessions = (
  baseSessionId: string,
  joiningSessionId: string,
  hint: SplitHint
): Workspace => {
  const basePane: WorkspaceNode = { id: crypto.randomUUID(), type: 'pane', sessionId: baseSessionId };
  const newPane: WorkspaceNode = { id: crypto.randomUUID(), type: 'pane', sessionId: joiningSessionId };
  const children = (hint.position === 'left' || hint.position === 'top') ? [newPane, basePane] : [basePane, newPane];

  return {
    id: `ws-${crypto.randomUUID()}`,
    title: 'Workspace',
    focusedSessionId: baseSessionId, // Initialize with the base session focused
    focusSessionOrder: [baseSessionId, joiningSessionId],
    root: {
      id: crypto.randomUUID(),
      type: 'split',
      direction: hint.direction,
      children,
      sizes: [1, 1],
    },
  };
};

export const updateWorkspaceSplitSizes = (
  root: WorkspaceNode,
  splitId: string,
  sizes: number[]
): WorkspaceNode => {
  const patch = (node: WorkspaceNode): WorkspaceNode => {
    if (node.type === 'split') {
      if (node.id === splitId) {
        return { ...node, sizes };
      }
      return { ...node, children: node.children.map(child => patch(child)) };
    }
    return node;
  };
  return patch(root);
};

export const resolveWorkspaceFocusSessionOrder = (
  root: WorkspaceNode,
  savedOrder?: string[],
): string[] => {
  const sessionIds = collectSessionIds(root);
  if (!savedOrder?.length) return sessionIds;

  const sessionIdSet = new Set(sessionIds);
  const ordered = savedOrder.filter((id, index) => (
    sessionIdSet.has(id) && savedOrder.indexOf(id) === index
  ));
  const orderedSet = new Set(ordered);
  return [...ordered, ...sessionIds.filter((id) => !orderedSet.has(id))];
};

export const reorderWorkspaceFocusSessionOrder = (
  root: WorkspaceNode,
  savedOrder: string[] | undefined,
  draggedSessionId: string,
  targetSessionId: string,
  position: 'before' | 'after' = 'before',
): string[] => {
  if (draggedSessionId === targetSessionId) {
    return resolveWorkspaceFocusSessionOrder(root, savedOrder);
  }

  const currentOrder = resolveWorkspaceFocusSessionOrder(root, savedOrder);
  const draggedIndex = currentOrder.indexOf(draggedSessionId);
  const targetIndex = currentOrder.indexOf(targetSessionId);

  if (draggedIndex === -1 || targetIndex === -1) return currentOrder;

  currentOrder.splice(draggedIndex, 1);
  let insertIndex = targetIndex;
  if (draggedIndex < targetIndex) insertIndex -= 1;
  if (position === 'after') insertIndex += 1;
  currentOrder.splice(insertIndex, 0, draggedSessionId);

  return currentOrder;
};

/**
 * Create a workspace from multiple session IDs.
 * Used for snippet runner - creates a workspace with all sessions in a horizontal split.
 */
export const createWorkspaceFromSessionIds = (
  sessionIds: string[],
  options: {
    title: string;
    viewMode?: WorkspaceViewMode;
    snippetId?: string;
  }
): Workspace => {
  if (sessionIds.length === 0) {
    throw new Error('Cannot create workspace with no sessions');
  }

  if (sessionIds.length === 1) {
    // Single pane workspace
    return {
      id: `ws-${crypto.randomUUID()}`,
      title: options.title,
      viewMode: options.viewMode,
      snippetId: options.snippetId,
      focusedSessionId: sessionIds[0],
      focusSessionOrder: [sessionIds[0]],
      root: {
        id: crypto.randomUUID(),
        type: 'pane',
        sessionId: sessionIds[0],
      },
    };
  }

  // Multiple sessions - create a horizontal split
  const children: WorkspaceNode[] = sessionIds.map(sessionId => ({
    id: crypto.randomUUID(),
    type: 'pane' as const,
    sessionId,
  }));

  return {
    id: `ws-${crypto.randomUUID()}`,
    title: options.title,
    viewMode: options.viewMode,
    snippetId: options.snippetId,
    focusedSessionId: sessionIds[0],
    focusSessionOrder: sessionIds,
    root: {
      id: crypto.randomUUID(),
      type: 'split',
      direction: 'vertical', // Side by side
      children,
      sizes: children.map(() => 1),
    },
  };
};

/**
 * Collect all session IDs from a workspace node tree.
 */
export const collectSessionIds = (node: WorkspaceNode): string[] => {
  if (node.type === 'pane') {
    return [node.sessionId];
  }
  return node.children.flatMap(child => collectSessionIds(child));
};

/**
 * Find a pane node by session ID in the workspace tree.
 */
const _findPaneBySessionId = (node: WorkspaceNode, sessionId: string): WorkspaceNode | null => {
  if (node.type === 'pane') {
    return node.sessionId === sessionId ? node : null;
  }
  for (const child of node.children) {
    const found = _findPaneBySessionId(child, sessionId);
    if (found) return found;
  }
  return null;
};

/**
 * Get the path to a session in the workspace tree.
 * Returns an array of indices representing the path from root to the pane.
 */
const _getPathToSession = (node: WorkspaceNode, sessionId: string, path: number[] = []): number[] | null => {
  if (node.type === 'pane') {
    return node.sessionId === sessionId ? path : null;
  }
  for (let i = 0; i < node.children.length; i++) {
    const result = _getPathToSession(node.children[i], sessionId, [...path, i]);
    if (result) return result;
  }
  return null;
};

/**
 * Get all panes with their positions for navigation.
 */
interface PanePosition {
  sessionId: string;
  path: number[];
  // Calculated bounds (normalized 0-1)
  x: number;
  y: number;
  width: number;
  height: number;
}

const collectPanePositions = (
  node: WorkspaceNode,
  path: number[] = [],
  bounds: { x: number; y: number; width: number; height: number } = { x: 0, y: 0, width: 1, height: 1 }
): PanePosition[] => {
  if (node.type === 'pane') {
    return [{
      sessionId: node.sessionId,
      path,
      ...bounds,
    }];
  }

  const positions: PanePosition[] = [];
  const sizes = node.sizes || node.children.map(() => 1 / node.children.length);
  const totalSize = sizes.reduce((a, b) => a + b, 0) || 1;
  
  let offset = 0;
  for (let i = 0; i < node.children.length; i++) {
    const sizeRatio = (sizes[i] || 1 / node.children.length) / totalSize;
    
    let childBounds: { x: number; y: number; width: number; height: number };
    if (node.direction === 'horizontal') {
      // Top/bottom split
      childBounds = {
        x: bounds.x,
        y: bounds.y + bounds.height * offset,
        width: bounds.width,
        height: bounds.height * sizeRatio,
      };
    } else {
      // Left/right split
      childBounds = {
        x: bounds.x + bounds.width * offset,
        y: bounds.y,
        width: bounds.width * sizeRatio,
        height: bounds.height,
      };
    }
    
    positions.push(...collectPanePositions(node.children[i], [...path, i], childBounds));
    offset += sizeRatio;
  }
  
  return positions;
};

export type FocusDirection = 'up' | 'down' | 'left' | 'right';

/**
 * Find the next session to focus when moving in a direction.
 * Returns the session ID to focus, or null if no valid target.
 */
export const getNextFocusSessionId = (
  root: WorkspaceNode,
  currentSessionId: string,
  direction: FocusDirection
): string | null => {
  const positions = collectPanePositions(root);
  
  const current = positions.find(p => p.sessionId === currentSessionId);
  if (!current) {
    return null;
  }

  // Filter candidates based on direction
  let candidates: PanePosition[] = [];
  const otherPanes = positions.filter(p => p.sessionId !== currentSessionId);
  
  switch (direction) {
    case 'left':
      // Find panes to the left
      candidates = otherPanes.filter(p => 
        p.x + p.width <= current.x + 0.001 // Allow small epsilon for floating point
      );
      // Wraparound: if no pane to the left, find the rightmost pane
      if (candidates.length === 0 && otherPanes.length > 0) {
        const maxX = Math.max(...otherPanes.map(p => p.x));
        candidates = otherPanes.filter(p => p.x >= maxX - 0.001);
      }
      break;
    case 'right':
      // Find panes to the right
      candidates = otherPanes.filter(p => 
        p.x >= current.x + current.width - 0.001
      );
      // Wraparound: if no pane to the right, find the leftmost pane
      if (candidates.length === 0 && otherPanes.length > 0) {
        const minX = Math.min(...otherPanes.map(p => p.x));
        candidates = otherPanes.filter(p => p.x <= minX + 0.001);
      }
      break;
    case 'up':
      // Find panes above
      candidates = otherPanes.filter(p => 
        p.y + p.height <= current.y + 0.001
      );
      // Wraparound: if no pane above, find the bottommost pane
      if (candidates.length === 0 && otherPanes.length > 0) {
        const maxY = Math.max(...otherPanes.map(p => p.y));
        candidates = otherPanes.filter(p => p.y >= maxY - 0.001);
      }
      break;
    case 'down':
      // Find panes below
      candidates = otherPanes.filter(p => 
        p.y >= current.y + current.height - 0.001
      );
      // Wraparound: if no pane below, find the topmost pane
      if (candidates.length === 0 && otherPanes.length > 0) {
        const minY = Math.min(...otherPanes.map(p => p.y));
        candidates = otherPanes.filter(p => p.y <= minY + 0.001);
      }
      break;
  }

  if (candidates.length === 0) return null;

  // Calculate center point of current pane for scoring
  const currentCenterX = current.x + current.width / 2;
  const currentCenterY = current.y + current.height / 2;

  // Find the closest candidate
  // For left/right, prefer candidates that overlap vertically
  // For up/down, prefer candidates that overlap horizontally
  let best: PanePosition | null = null;
  let bestScore = Infinity;

  for (const candidate of candidates) {
    const candidateCenterX = candidate.x + candidate.width / 2;
    const candidateCenterY = candidate.y + candidate.height / 2;
    
    let score: number;
    
    if (direction === 'left' || direction === 'right') {
      // Check vertical overlap
      const overlapTop = Math.max(current.y, candidate.y);
      const overlapBottom = Math.min(current.y + current.height, candidate.y + candidate.height);
      const hasOverlap = overlapBottom > overlapTop;
      
      // Distance is horizontal distance, but penalize if no overlap
      const distance = Math.abs(candidateCenterX - currentCenterX);
      const verticalPenalty = hasOverlap ? 0 : Math.abs(candidateCenterY - currentCenterY) * 2;
      score = distance + verticalPenalty;
    } else {
      // Check horizontal overlap
      const overlapLeft = Math.max(current.x, candidate.x);
      const overlapRight = Math.min(current.x + current.width, candidate.x + candidate.width);
      const hasOverlap = overlapRight > overlapLeft;
      
      // Distance is vertical distance, but penalize if no overlap
      const distance = Math.abs(candidateCenterY - currentCenterY);
      const horizontalPenalty = hasOverlap ? 0 : Math.abs(candidateCenterX - currentCenterX) * 2;
      score = distance + horizontalPenalty;
    }

    if (score < bestScore) {
      bestScore = score;
      best = candidate;
    }
  }

  return best?.sessionId || null;
};
