/**
 * MCP Apps Bridge - Lightweight postMessage bridge for MCP Apps protocol (SEP-1865)
 * Handles JSON-RPC communication between widget iframe and MCP Apps host
 */

import {
  createNotification,
  createRequest,
  type JsonRpcError,
  type JsonRpcNotification,
  type JsonRpcRequest,
  type JsonRpcResponse,
} from "../server/utils/jsonrpc-helpers.js";
import { MCP_APPS_BRIDGE_CONFIG } from "./constants.js";
import type {
  DisplayMode,
  MessageContentBlock,
  Theme,
} from "./widget-types.js";

// JSON-RPC message types (using imported types with compatible names)
type JSONRPCRequest = JsonRpcRequest;
type JSONRPCResponse = JsonRpcResponse | JsonRpcError;
type JSONRPCNotification = JsonRpcNotification;
type JSONRPCMessage = JSONRPCRequest | JSONRPCResponse | JSONRPCNotification;

// Host context from ui/initialize response
interface HostContext {
  theme?: Theme;
  displayMode?: DisplayMode;
  containerDimensions?: {
    width?: number;
    height?: number;
    maxWidth?: number;
    maxHeight?: number;
  };
  locale?: string;
  timeZone?: string;
  platform?: "web" | "desktop" | "mobile";
  userAgent?: string;
  deviceCapabilities?: {
    touch?: boolean;
    hover?: boolean;
  };
  safeAreaInsets?: {
    top: number;
    right: number;
    bottom: number;
    left: number;
  };
  styles?: {
    variables?: Record<string, string>;
    css?: {
      fonts?: string;
    };
  };
}

interface McpUiInitializeResult {
  protocolVersion: string;
  hostCapabilities?: unknown;
  hostInfo?: {
    name: string;
    version: string;
  };
  hostContext?: HostContext;
}

// Tool notification params
interface ToolInputNotification {
  arguments: Record<string, unknown>;
}

interface ToolInputPartialNotification {
  arguments: Record<string, unknown>;
}

interface ToolResultNotification {
  content?: Array<{
    type: string;
    text?: string;
    [key: string]: unknown;
  }>;
  structuredContent?: Record<string, unknown>;
  isError?: boolean;
  _meta?: Record<string, unknown>;
}

/**
 * Singleton bridge for MCP Apps postMessage communication
 */
class McpAppsBridge {
  private connected = false;
  private connectPromise: Promise<void> | null = null;
  private requestId = 1;
  private pendingRequests = new Map<
    number | string,
    { resolve: (value: unknown) => void; reject: (error: Error) => void }
  >();
  private listeners = new Set<(event: MessageEvent) => void>();

  // State
  private toolInput: Record<string, unknown> | null = null;
  private partialToolInput: Record<string, unknown> | null = null;
  private toolOutput: Record<string, unknown> | null = null;
  private toolResponseMetadata: Record<string, unknown> | null = null;
  private hostContext: HostContext | null = null;
  private hostInfo: { name: string; version: string } | null = null;
  private hostCapabilities: Record<string, unknown> | null = null;
  private initialized = false;

  // Event handlers
  private toolInputHandlers = new Set<
    (input: Record<string, unknown>) => void
  >();
  private toolInputPartialHandlers = new Set<
    (input: Record<string, unknown>) => void
  >();
  private toolResultHandlers = new Set<
    (result: Record<string, unknown>) => void
  >();
  private hostContextHandlers = new Set<(context: HostContext) => void>();

  constructor() {
    if (typeof window === "undefined") return;

    // Listen for postMessage from host
    const handleMessage = (event: MessageEvent) => {
      const message = event.data as JSONRPCMessage;
      if (!message || message.jsonrpc !== "2.0") return;

      // Handle responses
      if ("result" in message || "error" in message) {
        const response = message as any;
        if (response.id !== null && response.id !== undefined) {
          const pending = this.pendingRequests.get(response.id);
          if (pending) {
            this.pendingRequests.delete(response.id);
            if ("error" in response && response.error) {
              pending.reject(new Error(response.error.message));
            } else {
              pending.resolve(response.result);
            }
          }
        }
        return;
      }

      // Handle notifications
      if ("method" in message && !("id" in message)) {
        this.handleNotification(message);
      }
    };

    window.addEventListener("message", handleMessage);
    this.listeners.add(handleMessage);

    // Intercept console methods to send logs to host
    this.interceptConsole();
  }

  /**
   * Intercept console methods and proxy to MCP Apps host
   */
  private interceptConsole(): void {
    if (typeof window === "undefined" || typeof console === "undefined") return;

    // Save original console methods
    const originalConsole = {
      log: console.log.bind(console),
      warn: console.warn.bind(console),
      error: console.error.bind(console),
      info: console.info.bind(console),
      debug: console.debug.bind(console),
    };

    // Map JS console levels to RFC 5424 log levels
    const consoleLevelToRfc5424: Record<string, string> = {
      log: "info",
      warn: "warning",
      error: "error",
      info: "info",
      debug: "debug",
    };

    // Helper to serialize console arguments for postMessage
    const serializeForPostMessage = (value: any, seen = new WeakSet()): any => {
      // Handle primitives
      if (value === null || value === undefined) return value;
      if (typeof value !== "object") return value;

      // Handle circular references
      if (seen.has(value)) return "[Circular]";

      // Handle special object types that can't be cloned
      if (value instanceof Response) {
        return {
          __type: "Response",
          status: value.status,
          statusText: value.statusText,
          ok: value.ok,
          url: value.url,
          headers: Object.fromEntries(value.headers.entries()),
        };
      }
      if (value instanceof Request) {
        return {
          __type: "Request",
          method: value.method,
          url: value.url,
          headers: Object.fromEntries(value.headers.entries()),
        };
      }
      if (value instanceof Error) {
        return {
          __type: "Error",
          name: value.name,
          message: value.message,
          stack: value.stack,
        };
      }
      if (value instanceof Event) {
        return {
          __type: "Event",
          type: value.type,
          target: value.target?.constructor?.name,
        };
      }
      if (typeof HTMLElement !== "undefined" && value instanceof HTMLElement) {
        return {
          __type: "HTMLElement",
          tagName: value.tagName,
          id: value.id,
          className: value.className,
        };
      }
      if (typeof value === "function") {
        return `[Function: ${value.name || "anonymous"}]`;
      }

      // Handle arrays
      if (Array.isArray(value)) {
        seen.add(value);
        return value.map((item) => serializeForPostMessage(item, seen));
      }

      // Handle plain objects
      try {
        seen.add(value);
        const serialized: any = {};
        for (const key in value) {
          if (Object.prototype.hasOwnProperty.call(value, key)) {
            try {
              serialized[key] = serializeForPostMessage(value[key], seen);
            } catch {
              serialized[key] = "[Unserializable]";
            }
          }
        }
        return serialized;
      } catch {
        return "[Object]";
      }
    };

    // Helper to send logging notification
    const sendLog = (
      level: "log" | "warn" | "error" | "info" | "debug",
      args: any[]
    ) => {
      try {
        // Serialize arguments to prevent DataCloneError
        const serializedArgs = args.map((arg) => serializeForPostMessage(arg));

        // Send notification to host
        this.sendNotification("notifications/message", {
          level: consoleLevelToRfc5424[level] || "info",
          logger: "console",
          data:
            serializedArgs.length === 1 ? serializedArgs[0] : serializedArgs,
        });
      } catch (error) {
        // If serialization fails, send error message instead
        originalConsole.warn(
          "[MCP Apps Bridge] Failed to forward console message:",
          error
        );
      }
    };

    // Wrap console methods
    console.log = (...args: any[]) => {
      sendLog("log", args);
      originalConsole.log(...args);
    };

    console.warn = (...args: any[]) => {
      sendLog("warn", args);
      originalConsole.warn(...args);
    };

    console.error = (...args: any[]) => {
      sendLog("error", args);
      originalConsole.error(...args);
    };

    console.info = (...args: any[]) => {
      sendLog("info", args);
      originalConsole.info(...args);
    };

    console.debug = (...args: any[]) => {
      sendLog("debug", args);
      originalConsole.debug(...args);
    };
  }

  private handleNotification(notification: JSONRPCNotification) {
    console.log(
      "[MCP Apps Bridge] Received notification:",
      notification.method,
      notification.params
    );

    switch (notification.method) {
      case "ui/notifications/tool-input": {
        const params = notification.params as unknown as ToolInputNotification;
        console.log("[MCP Apps Bridge] Tool input received:", params.arguments);
        this.toolInput = params.arguments;
        this.toolInputHandlers.forEach((handler) => handler(params.arguments));
        break;
      }
      case "ui/notifications/tool-input-partial": {
        const params =
          notification.params as unknown as ToolInputPartialNotification;
        console.log(
          "[MCP Apps Bridge] Partial tool input received:",
          params.arguments
        );
        this.partialToolInput = params.arguments;
        this.toolInputPartialHandlers.forEach((handler) =>
          handler(params.arguments)
        );
        break;
      }
      case "ui/notifications/tool-result": {
        const params = notification.params as ToolResultNotification;
        const output =
          params.structuredContent || this.parseTextContent(params);
        const meta = (params._meta as Record<string, unknown>) || null;
        this.toolOutput = output;
        this.toolResponseMetadata = meta;
        this.partialToolInput = null;
        this.toolResultHandlers.forEach((handler) => handler(output));
        break;
      }
      case "ui/notifications/host-context-changed": {
        const context = notification.params as HostContext;
        console.log("[MCP Apps Bridge] Host context changed:", context);
        // Merge partial updates with existing context
        this.hostContext = { ...this.hostContext, ...context };
        console.log("[MCP Apps Bridge] Merged hostContext:", this.hostContext);
        console.log(
          "[MCP Apps Bridge] Calling handlers:",
          this.hostContextHandlers.size
        );
        this.hostContextHandlers.forEach((handler) =>
          handler(this.hostContext!)
        );
        break;
      }
      case "ui/notifications/initialized": {
        this.initialized = true;
        break;
      }
      default:
        // Unknown notification method, ignore
        break;
    }
  }

  private parseTextContent(
    result: ToolResultNotification
  ): Record<string, unknown> {
    // Try to extract structured data from text content
    if (result.content && Array.isArray(result.content)) {
      for (const block of result.content) {
        if (block.type === "text" && block.text) {
          try {
            const parsed = JSON.parse(block.text);
            if (
              parsed &&
              typeof parsed === "object" &&
              !Array.isArray(parsed)
            ) {
              return parsed as Record<string, unknown>;
            }
          } catch {
            // Not JSON, continue
          }
        }
      }
    }
    return {};
  }

  private sendRequest(method: string, params?: unknown): Promise<unknown> {
    if (typeof window === "undefined" || !window.parent) {
      return Promise.reject(new Error("Not in iframe context"));
    }

    const id = this.requestId++;
    const message = createRequest(
      id,
      method,
      params as Record<string, unknown> | undefined
    );

    return new Promise((resolve, reject) => {
      this.pendingRequests.set(id, { resolve, reject });
      window.parent.postMessage(message, "*");

      // Timeout
      setTimeout(() => {
        if (this.pendingRequests.has(id)) {
          this.pendingRequests.delete(id);
          reject(new Error(`Request timeout: ${method}`));
        }
      }, MCP_APPS_BRIDGE_CONFIG.REQUEST_TIMEOUT);
    });
  }

  private sendNotification(method: string, params?: unknown): void {
    if (typeof window === "undefined" || !window.parent) return;

    const message = createNotification(
      method,
      params as Record<string, unknown> | undefined
    );

    window.parent.postMessage(message, "*");
  }

  /**
   * Initialize connection with MCP Apps host.
   * Concurrent calls share the same in-flight connection attempt so that
   * React StrictMode double-invocations and multiple useWidget() hooks
   * only produce a single ui/initialize request.
   */
  async connect(): Promise<void> {
    if (this.connected) return;

    // Only connect if we're in an iframe (not top window)
    if (typeof window === "undefined" || window === window.parent) {
      console.log("[MCP Apps Bridge] Not in iframe, skipping connection");
      return;
    }

    if (!this.connectPromise) {
      this.connectPromise = this.doConnect();
      this.connectPromise.catch(() => {
        this.connectPromise = null;
      });
    }

    return this.connectPromise;
  }

  private async doConnect(): Promise<void> {
    console.log("[MCP Apps Bridge] Connecting to MCP Apps host...");

    try {
      // Send ui/initialize request
      const result = (await this.sendRequest("ui/initialize", {
        appCapabilities: {},
        appInfo: {
          name: MCP_APPS_BRIDGE_CONFIG.APP_NAME,
          version: MCP_APPS_BRIDGE_CONFIG.APP_VERSION,
        },
        protocolVersion: MCP_APPS_BRIDGE_CONFIG.PROTOCOL_VERSION,
      })) as McpUiInitializeResult;

      console.log("[MCP Apps Bridge] Initialize result:", result);

      // Store host context
      if (result.hostContext) {
        this.hostContext = result.hostContext;
        console.log("[MCP Apps Bridge] Host context:", this.hostContext);
      }

      // Store host info and capabilities
      if (result.hostInfo) {
        this.hostInfo = result.hostInfo;
      }
      if (result.hostCapabilities) {
        this.hostCapabilities = result.hostCapabilities as Record<
          string,
          unknown
        >;
      }

      // Send initialized notification
      this.sendNotification("ui/notifications/initialized", {});

      this.connected = true;
      console.log("[MCP Apps Bridge] Successfully connected");
    } catch (error) {
      console.error("[MCP Apps Bridge] Failed to connect:", error);
      throw error;
    }
  }

  /**
   * Check if connected to MCP Apps host
   */
  isConnected(): boolean {
    return this.connected;
  }

  /**
   * Get current tool input (props)
   */
  getToolInput(): Record<string, unknown> | null {
    return this.toolInput;
  }

  /**
   * Get current partial/streaming tool input
   */
  getPartialToolInput(): Record<string, unknown> | null {
    return this.partialToolInput;
  }

  /**
   * Get current tool output (structuredContent from tool result)
   */
  getToolOutput(): Record<string, unknown> | null {
    return this.toolOutput;
  }

  /**
   * Get tool response metadata (_meta from tool result)
   */
  getToolResponseMetadata(): Record<string, unknown> | null {
    return this.toolResponseMetadata;
  }

  /**
   * Get host context
   */
  getHostContext(): HostContext | null {
    return this.hostContext;
  }

  /**
   * Get host info (name and version of the MCP Apps host)
   */
  getHostInfo(): { name: string; version: string } | null {
    return this.hostInfo;
  }

  /**
   * Get host capabilities advertised during the ui/initialize handshake
   */
  getHostCapabilities(): Record<string, unknown> | null {
    return this.hostCapabilities;
  }

  /**
   * Subscribe to tool input changes
   */
  onToolInput(handler: (input: Record<string, unknown>) => void): () => void {
    this.toolInputHandlers.add(handler);
    return () => this.toolInputHandlers.delete(handler);
  }

  /**
   * Subscribe to partial/streaming tool input changes
   */
  onToolInputPartial(
    handler: (input: Record<string, unknown>) => void
  ): () => void {
    this.toolInputPartialHandlers.add(handler);
    return () => this.toolInputPartialHandlers.delete(handler);
  }

  /**
   * Subscribe to tool result changes
   */
  onToolResult(handler: (result: Record<string, unknown>) => void): () => void {
    this.toolResultHandlers.add(handler);
    return () => this.toolResultHandlers.delete(handler);
  }

  /**
   * Subscribe to host context changes
   */
  onHostContextChange(handler: (context: HostContext) => void): () => void {
    this.hostContextHandlers.add(handler);
    return () => this.hostContextHandlers.delete(handler);
  }

  /**
   * Call a tool on the server
   */
  async callTool(
    name: string,
    args: Record<string, unknown>
  ): Promise<unknown> {
    return this.sendRequest("tools/call", { name, arguments: args });
  }

  /**
   * Send a message to the conversation.
   * Accepts a single content block or an array of blocks per the SEP-1865 ui/message spec.
   */
  async sendMessage(
    content: MessageContentBlock | MessageContentBlock[]
  ): Promise<void> {
    const contentArray = Array.isArray(content) ? content : [content];
    await this.sendRequest("ui/message", {
      role: "user",
      content: contentArray,
    });
  }

  /**
   * Open an external link
   */
  async openLink(url: string): Promise<void> {
    await this.sendRequest("ui/open-link", { url });
  }

  /**
   * Request display mode change
   */
  async requestDisplayMode(mode: DisplayMode): Promise<{ mode: DisplayMode }> {
    const result = await this.sendRequest("ui/request-display-mode", { mode });
    return result as { mode: DisplayMode };
  }

  /**
   * Update the host's model context (SEP-1865 ui/update-model-context).
   * The host will include this data in the model's context on future turns.
   * Each call overwrites the previous context.
   */
  async updateModelContext(params: {
    content?: Array<{ type: string; text: string }>;
    structuredContent?: Record<string, unknown>;
  }): Promise<void> {
    await this.sendRequest("ui/update-model-context", params);
  }

  /**
   * Notify host about size changes for auto-sizing
   * Sends ui/notifications/size-changed notification per SEP-1865
   */
  sendSizeChanged(params: { width?: number; height?: number }): void {
    this.sendNotification("ui/notifications/size-changed", params);
  }

  /**
   * Cleanup
   */
  disconnect(): void {
    this.listeners.forEach((listener) => {
      if (typeof window !== "undefined") {
        window.removeEventListener("message", listener);
      }
    });
    this.listeners.clear();
    this.pendingRequests.clear();
    this.toolInputHandlers.clear();
    this.toolInputPartialHandlers.clear();
    this.toolResultHandlers.clear();
    this.hostContextHandlers.clear();
    this.connected = false;
    this.connectPromise = null;
  }
}

// Singleton instance
let bridgeInstance: McpAppsBridge | null = null;

/**
 * Get or create the MCP Apps bridge singleton
 */
export function getMcpAppsBridge(): McpAppsBridge {
  if (!bridgeInstance) {
    bridgeInstance = new McpAppsBridge();
  }
  return bridgeInstance;
}

/**
 * Type exports
 */
export type {
  HostContext,
  ToolInputNotification,
  ToolInputPartialNotification,
  ToolResultNotification,
};
