/**
 * WebSocket Figma Connector
 *
 * Implements IFigmaConnector using the WebSocket Desktop Bridge transport.
 * Each method sends a command to the plugin UI via WebSocket and waits
 * for the response — same window.* functions, different transport.
 *
 * Data flow: MCP Server ←WebSocket→ ui.html ←postMessage→ code.js ←figma.*→ Figma
 */

import type { IFigmaConnector } from './figma-connector.js';
import type { FigmaWebSocketServer } from './websocket-server.js';
import { createChildLogger } from './logger.js';

const logger = createChildLogger({ component: 'websocket-connector' });

/**
 * Number of variants a CREATE_COMPONENT_SET request will produce:
 * Mode B = componentIds.length; Mode A = the cartesian product of the
 * property axes ({State:[a,b,c], Size:[s,l]} → 6).
 */
export function componentSetVariantCount(params: {
  properties?: Record<string, string[]>;
  componentIds?: string[];
}): number {
  if (params.componentIds?.length) return params.componentIds.length;
  if (params.properties) {
    let count = 1;
    for (const values of Object.values(params.properties)) {
      count *= Math.max(1, values?.length ?? 1);
    }
    return count;
  }
  return 1;
}

/**
 * Server-hop timeout for CREATE_COMPONENT_SET, scaled by variant count.
 * The plugin builds all variants in ONE uncancellable pass, so timing out
 * early doesn't stop the work — it just makes the report contradict the
 * file state (set gets created after the "failure") and invites duplicate
 * retries. Budget ~1.2s per variant with a 30s floor and 120s cap, plus a
 * 5s buffer over the ui.html hop (which uses the same base formula) so the
 * plugin-side result or error always wins the race.
 *
 * NOTE: ui.html's CREATE_COMPONENT_SET route and
 * cloud-websocket-connector.ts mirror the base formula — keep all three in
 * sync.
 */
export function componentSetTimeoutMs(params: {
  properties?: Record<string, string[]>;
  componentIds?: string[];
}): number {
  const count = componentSetVariantCount(params);
  return Math.min(120000, Math.max(30000, count * 1200)) + 5000;
}

export class WebSocketConnector implements IFigmaConnector {
  private wsServer: FigmaWebSocketServer;

  constructor(wsServer: FigmaWebSocketServer) {
    this.wsServer = wsServer;
  }

  async initialize(): Promise<void> {
    if (!this.wsServer.isClientConnected()) {
      throw new Error(
        'No WebSocket client connected. Make sure the Desktop Bridge plugin is open in Figma.'
      );
    }
    logger.info('WebSocket connector initialized');
  }

  getTransportType(): 'websocket' {
    return 'websocket';
  }

  // ============================================================================
  // Core execution
  // ============================================================================

  async executeInPluginContext<T = any>(code: string): Promise<T> {
    return this.wsServer.sendCommand('EXECUTE_CODE', { code, timeout: 5000 }, 7000);
  }

  async getVariablesFromPluginUI(fileKey?: string): Promise<any> {
    // Request the cached variables data that the plugin UI holds in window.__figmaVariablesData
    return this.wsServer.sendCommand('GET_VARIABLES_DATA', {}, 10000, fileKey);
  }

  async getVariables(fileKey?: string): Promise<any> {
    // Execute the same variables-fetching code in the plugin worker context.
    //
    // IMPORTANT: Do NOT wrap this in an inner `(async () => { ... })()` IIFE.
    // figma-desktop-bridge/code.js already wraps every EXECUTE_CODE payload in
    // `(async function() { <code> })()`. An inner IIFE turns `return X` into a
    // statement-expression that builds (but doesn't return) a Promise — the
    // outer async returns undefined, and the result is silently dropped. See
    // issue #68. The bare try/catch with top-level `return` is the contract
    // code.js expects.
    const code = `
      try {
        if (typeof figma === 'undefined') {
          throw new Error('Figma API not available in this context');
        }
        const variables = await figma.variables.getLocalVariablesAsync();
        const collections = await figma.variables.getLocalVariableCollectionsAsync();
        return {
          success: true,
          timestamp: Date.now(),
          fileMetadata: { fileName: figma.root.name, fileKey: figma.fileKey || null },
          variables: variables.map(function(v) { return { id: v.id, name: v.name, key: v.key, resolvedType: v.resolvedType, valuesByMode: v.valuesByMode, variableCollectionId: v.variableCollectionId, scopes: v.scopes, description: v.description, hiddenFromPublishing: v.hiddenFromPublishing }; }),
          variableCollections: collections.map(function(c) { return { id: c.id, name: c.name, key: c.key, modes: c.modes, defaultModeId: c.defaultModeId, variableIds: c.variableIds }; })
        };
      } catch (error) {
        return { success: false, error: error.message };
      }
    `;
    return this.wsServer.sendCommand('EXECUTE_CODE', { code, timeout: 30000 }, 32000, fileKey);
  }

  async executeCodeViaUI(code: string, timeoutMs = 5000): Promise<any> {
    return this.wsServer.sendCommand('EXECUTE_CODE', { code, timeout: timeoutMs }, timeoutMs + 2000);
  }

  // ============================================================================
  // Variable operations
  // ============================================================================

  async updateVariable(variableId: string, modeId: string, value: any): Promise<any> {
    return this.wsServer.sendCommand('UPDATE_VARIABLE', { variableId, modeId, value });
  }

  async createVariable(
    name: string,
    collectionId: string,
    resolvedType: string,
    options?: any
  ): Promise<any> {
    const params: any = { name, collectionId, resolvedType };
    if (options) {
      if (options.valuesByMode) params.valuesByMode = options.valuesByMode;
      if (options.description) params.description = options.description;
      if (options.scopes) params.scopes = options.scopes;
    }
    return this.wsServer.sendCommand('CREATE_VARIABLE', params);
  }

  async deleteVariable(variableId: string): Promise<any> {
    return this.wsServer.sendCommand('DELETE_VARIABLE', { variableId });
  }

  async refreshVariables(): Promise<any> {
    return this.wsServer.sendCommand('REFRESH_VARIABLES', {}, 300000);
  }

  async renameVariable(variableId: string, newName: string): Promise<any> {
    const result = await this.wsServer.sendCommand('RENAME_VARIABLE', { variableId, newName });
    // oldName may be embedded in variable data if ui.html handleResult doesn't pass it through
    if (!result.oldName && result.variable?.oldName) result.oldName = result.variable.oldName;
    return result;
  }

  async setVariableDescription(variableId: string, description: string): Promise<any> {
    return this.wsServer.sendCommand('SET_VARIABLE_DESCRIPTION', { variableId, description });
  }

  // ============================================================================
  // Mode operations
  // ============================================================================

  async addMode(collectionId: string, modeName: string): Promise<any> {
    return this.wsServer.sendCommand('ADD_MODE', { collectionId, modeName });
  }

  async renameMode(collectionId: string, modeId: string, newName: string): Promise<any> {
    const result = await this.wsServer.sendCommand('RENAME_MODE', { collectionId, modeId, newName });
    // oldName may be embedded in collection data if ui.html handleResult doesn't pass it through
    if (!result.oldName && result.collection?.oldName) result.oldName = result.collection.oldName;
    return result;
  }

  // ============================================================================
  // Collection operations
  // ============================================================================

  async createVariableCollection(name: string, options?: any): Promise<any> {
    const params: any = { name };
    if (options) {
      if (options.initialModeName) params.initialModeName = options.initialModeName;
      if (options.additionalModes) params.additionalModes = options.additionalModes;
    }
    return this.wsServer.sendCommand('CREATE_VARIABLE_COLLECTION', params);
  }

  async deleteVariableCollection(collectionId: string): Promise<any> {
    return this.wsServer.sendCommand('DELETE_VARIABLE_COLLECTION', { collectionId });
  }

  // ============================================================================
  // Component operations
  // ============================================================================

  async getComponentFromPluginUI(nodeId: string): Promise<any> {
    return this.wsServer.sendCommand('GET_COMPONENT', { nodeId }, 10000);
  }

  async getLocalComponents(): Promise<any> {
    return this.wsServer.sendCommand('GET_LOCAL_COMPONENTS', {}, 300000);
  }

  async setNodeDescription(nodeId: string, description: string, descriptionMarkdown?: string): Promise<any> {
    return this.wsServer.sendCommand('SET_NODE_DESCRIPTION', { nodeId, description, descriptionMarkdown });
  }

  // ============================================================================
  // Annotation operations
  // ============================================================================

  async getAnnotations(nodeId: string, includeChildren?: boolean, depth?: number): Promise<any> {
    return this.wsServer.sendCommand('GET_ANNOTATIONS', { nodeId, includeChildren, depth }, 10000);
  }

  async setAnnotations(nodeId: string, annotations: any[], mode?: 'replace' | 'append'): Promise<any> {
    return this.wsServer.sendCommand('SET_ANNOTATIONS', { nodeId, annotations, mode: mode || 'replace' });
  }

  async getAnnotationCategories(): Promise<any> {
    return this.wsServer.sendCommand('GET_ANNOTATION_CATEGORIES', {}, 5000);
  }

  async deepGetComponent(nodeId: string, depth?: number): Promise<any> {
    return this.wsServer.sendCommand('DEEP_GET_COMPONENT', { nodeId, depth: depth || 10 }, 30000);
  }

  async analyzeComponentSet(nodeId: string): Promise<any> {
    return this.wsServer.sendCommand('ANALYZE_COMPONENT_SET', { nodeId }, 30000);
  }

  async addComponentProperty(
    nodeId: string,
    propertyName: string,
    type: string,
    defaultValue: any,
    options?: any
  ): Promise<any> {
    const params: any = { nodeId, propertyName, propertyType: type, defaultValue };
    if (options?.preferredValues) params.preferredValues = options.preferredValues;
    return this.wsServer.sendCommand('ADD_COMPONENT_PROPERTY', params);
  }

  async editComponentProperty(nodeId: string, propertyName: string, newValue: any): Promise<any> {
    return this.wsServer.sendCommand('EDIT_COMPONENT_PROPERTY', { nodeId, propertyName, newValue });
  }

  async deleteComponentProperty(nodeId: string, propertyName: string): Promise<any> {
    return this.wsServer.sendCommand('DELETE_COMPONENT_PROPERTY', { nodeId, propertyName });
  }

  async instantiateComponent(componentKey: string, options?: any): Promise<any> {
    const params: any = { componentKey };
    if (options) {
      if (options.nodeId) params.nodeId = options.nodeId;
      if (options.position) params.position = options.position;
      if (options.size) params.size = options.size;
      if (options.overrides) params.overrides = options.overrides;
      if (options.variant) params.variant = options.variant;
      if (options.parentId) params.parentId = options.parentId;
    }
    return this.wsServer.sendCommand('INSTANTIATE_COMPONENT', params);
  }

  async createComponentSet(params: {
    baseComponentId?: string;
    properties?: Record<string, string[]>;
    componentIds?: string[];
    variantProperties?: Array<Record<string, string>>;
    name?: string;
    parentId?: string;
    position?: { x: number; y: number };
  }): Promise<any> {
    // Cloning + combining large variant matrices is slow on heavy components
    // and the plugin-side pass is uncancellable — a fixed 30s ceiling made a
    // 48-variant set REPORT failure while the plugin kept going and created
    // the set anyway (retry → duplicates). Scale the timeout with variant
    // count instead (precedent: the token-import create phase).
    return this.wsServer.sendCommand(
      'CREATE_COMPONENT_SET',
      params,
      componentSetTimeoutMs(params),
    );
  }

  // ============================================================================
  // Node manipulation
  // ============================================================================

  async resizeNode(nodeId: string, width: number, height: number, withConstraints = true): Promise<any> {
    return this.wsServer.sendCommand('RESIZE_NODE', { nodeId, width, height, withConstraints });
  }

  async moveNode(nodeId: string, x: number, y: number): Promise<any> {
    return this.wsServer.sendCommand('MOVE_NODE', { nodeId, x, y });
  }

  async setNodeFills(nodeId: string, fills: any[]): Promise<any> {
    return this.wsServer.sendCommand('SET_NODE_FILLS', { nodeId, fills });
  }

  async setNodeStrokes(nodeId: string, strokes: any[], strokeWeight?: number): Promise<any> {
    const params: any = { nodeId, strokes };
    if (strokeWeight !== undefined) params.strokeWeight = strokeWeight;
    return this.wsServer.sendCommand('SET_NODE_STROKES', params);
  }

  async setNodeOpacity(nodeId: string, opacity: number): Promise<any> {
    return this.wsServer.sendCommand('SET_NODE_OPACITY', { nodeId, opacity });
  }

  async setNodeCornerRadius(nodeId: string, radius: number): Promise<any> {
    return this.wsServer.sendCommand('SET_NODE_CORNER_RADIUS', { nodeId, radius });
  }

  async cloneNode(nodeId: string): Promise<any> {
    return this.wsServer.sendCommand('CLONE_NODE', { nodeId });
  }

  async deleteNode(nodeId: string): Promise<any> {
    return this.wsServer.sendCommand('DELETE_NODE', { nodeId });
  }

  async renameNode(nodeId: string, newName: string): Promise<any> {
    return this.wsServer.sendCommand('RENAME_NODE', { nodeId, newName });
  }

  async setTextContent(nodeId: string, characters: string, options?: any): Promise<any> {
    const params: any = { nodeId, text: characters };
    if (options) {
      if (options.fontSize) params.fontSize = options.fontSize;
      if (options.fontWeight) params.fontWeight = options.fontWeight;
      if (options.fontFamily) params.fontFamily = options.fontFamily;
      if (options.fontStyle) params.fontStyle = options.fontStyle;
    }
    return this.wsServer.sendCommand('SET_TEXT_CONTENT', params);
  }

  async createChildNode(parentId: string, nodeType: string, properties?: any): Promise<any> {
    return this.wsServer.sendCommand('CREATE_CHILD_NODE', { parentId, nodeType, properties: properties || {} });
  }

  // ============================================================================
  // Screenshot & instance properties
  // ============================================================================

  async captureScreenshot(nodeId: string, options?: any): Promise<any> {
    const params: any = { nodeId };
    if (options?.format) params.format = options.format;
    if (options?.scale) params.scale = options.scale;
    return this.wsServer.sendCommand('CAPTURE_SCREENSHOT', params, 30000);
  }

  async setInstanceProperties(nodeId: string, properties: any): Promise<any> {
    return this.wsServer.sendCommand('SET_INSTANCE_PROPERTIES', { nodeId, properties });
  }

  // ============================================================================
  // Image fill
  // ============================================================================

  async setImageFill(nodeIds: string[], imageData: string, scaleMode = 'FILL'): Promise<any> {
    return this.wsServer.sendCommand('SET_IMAGE_FILL', { nodeIds, imageData, scaleMode }, 60000);
  }

  // ============================================================================
  // Design lint
  // ============================================================================

  async lintDesign(nodeId?: string, rules?: string[], maxDepth?: number, maxFindings?: number): Promise<any> {
    const params: any = {};
    if (nodeId) params.nodeId = nodeId;
    if (rules) params.rules = rules;
    if (maxDepth !== undefined) params.maxDepth = maxDepth;
    if (maxFindings !== undefined) params.maxFindings = maxFindings;
    return this.wsServer.sendCommand('LINT_DESIGN', params, 120000);
  }

  // ============================================================================
  // Component accessibility audit
  // ============================================================================

  async auditComponentAccessibility(nodeId?: string, targetSize?: number): Promise<any> {
    const params: any = {};
    if (nodeId) params.nodeId = nodeId;
    if (targetSize !== undefined) params.targetSize = targetSize;
    return this.wsServer.sendCommand('AUDIT_COMPONENT_ACCESSIBILITY', params, 120000);
  }

  // ============================================================================
  // FigJam operations
  // ============================================================================

  async createSticky(params: { text: string; color?: string; x?: number; y?: number }): Promise<any> {
    return this.wsServer.sendCommand('CREATE_STICKY', params);
  }

  async createStickies(params: { stickies: Array<{ text: string; color?: string; x?: number; y?: number }> }): Promise<any> {
    return this.wsServer.sendCommand('CREATE_STICKIES', params, 30000);
  }

  async createConnector(params: { startNodeId: string; endNodeId: string; label?: string; startMagnet?: string; endMagnet?: string }): Promise<any> {
    return this.wsServer.sendCommand('CREATE_CONNECTOR', params);
  }

  async createShapeWithText(params: { text?: string; shapeType?: string; x?: number; y?: number; width?: number; height?: number; fillColor?: string; strokeColor?: string; fontSize?: number; strokeDashPattern?: string }): Promise<any> {
    return this.wsServer.sendCommand('CREATE_SHAPE_WITH_TEXT', params);
  }

  async createSection(params: { name?: string; x?: number; y?: number; width?: number; height?: number; fillColor?: string }): Promise<any> {
    return this.wsServer.sendCommand('CREATE_SECTION', params);
  }

  async createTable(params: { rows: number; columns: number; data?: string[][]; x?: number; y?: number }): Promise<any> {
    return this.wsServer.sendCommand('CREATE_TABLE', params, 30000);
  }

  async createCodeBlock(params: { code: string; language?: string; x?: number; y?: number }): Promise<any> {
    return this.wsServer.sendCommand('CREATE_CODE_BLOCK', params);
  }

  async getBoardContents(params: { nodeTypes?: string[]; maxNodes?: number }): Promise<any> {
    return this.wsServer.sendCommand('GET_BOARD_CONTENTS', params, 30000);
  }

  async getConnections(): Promise<any> {
    return this.wsServer.sendCommand('GET_CONNECTIONS', {}, 15000);
  }

  // ============================================================================
  // Slides operations
  // ============================================================================

  async listSlides(): Promise<any> {
    return this.wsServer.sendCommand('LIST_SLIDES', {}, 10000);
  }

  async getSlideContent(params: { slideId: string }): Promise<any> {
    return this.wsServer.sendCommand('GET_SLIDE_CONTENT', params, 10000);
  }

  async createSlide(params: { row?: number; col?: number }): Promise<any> {
    return this.wsServer.sendCommand('CREATE_SLIDE', params, 10000);
  }

  async deleteSlide(params: { slideId: string }): Promise<any> {
    return this.wsServer.sendCommand('DELETE_SLIDE', params, 5000);
  }

  async duplicateSlide(params: { slideId: string }): Promise<any> {
    return this.wsServer.sendCommand('DUPLICATE_SLIDE', params, 5000);
  }

  async getSlideGrid(): Promise<any> {
    return this.wsServer.sendCommand('GET_SLIDE_GRID', {}, 10000);
  }

  async reorderSlides(params: { grid: string[][] }): Promise<any> {
    return this.wsServer.sendCommand('REORDER_SLIDES', params, 15000);
  }

  async setSlideTransition(params: { slideId: string; style: string; duration: number; curve: string }): Promise<any> {
    return this.wsServer.sendCommand('SET_SLIDE_TRANSITION', params, 5000);
  }

  async getSlideTransition(params: { slideId: string }): Promise<any> {
    return this.wsServer.sendCommand('GET_SLIDE_TRANSITION', params, 5000);
  }

  async setSlidesViewMode(params: { mode: string }): Promise<any> {
    return this.wsServer.sendCommand('SET_SLIDES_VIEW_MODE', params, 5000);
  }

  async getFocusedSlide(): Promise<any> {
    return this.wsServer.sendCommand('GET_FOCUSED_SLIDE', {}, 5000);
  }

  async focusSlide(params: { slideId: string }): Promise<any> {
    return this.wsServer.sendCommand('FOCUS_SLIDE', params, 5000);
  }

  async skipSlide(params: { slideId: string; skip: boolean }): Promise<any> {
    return this.wsServer.sendCommand('SKIP_SLIDE', params, 5000);
  }

  async addTextToSlide(params: { slideId: string; text: string; x?: number; y?: number; fontSize?: number; fontFamily?: string; fontStyle?: string; color?: string; textAlign?: string; width?: number; lineHeight?: number; letterSpacing?: number; textCase?: string }): Promise<any> {
    return this.wsServer.sendCommand('ADD_TEXT_TO_SLIDE', params, 10000);
  }

  async addShapeToSlide(params: { slideId: string; shapeType: string; x: number; y: number; width: number; height: number; fillColor?: string }): Promise<any> {
    return this.wsServer.sendCommand('ADD_SHAPE_TO_SLIDE', params, 5000);
  }

  async setSlideBackground(params: { slideId: string; color: string }): Promise<any> {
    return this.wsServer.sendCommand('SET_SLIDE_BACKGROUND', params, 5000);
  }

  async getTextStyles(): Promise<any> {
    return this.wsServer.sendCommand('GET_TEXT_STYLES', {}, 5000);
  }

  // ============================================================================
  // Cache management (no-op for WebSocket — no frame cache)
  // ============================================================================

  clearFrameCache(): void {
    // No frame cache in WebSocket mode
  }
}
