/**
 * MCP Client Service for Anything Copilot
 * Handles n8n Native HTTP Streamable MCP correctly.
 */

export interface McpTool {
  name: string
  description: string
  inputSchema?: Record<string, any>
}

export interface McpExecutionResult {
  id: string
  toolName: string
  args: Record<string, any>
  status: "pending" | "success" | "error"
  result?: any
  error?: string
  timestamp: number
  duration?: number
}

export type McpConnectionStatus = "disconnected" | "connecting" | "connected" | "error"

type StatusCallback = (status: McpConnectionStatus) => void
type ToolsCallback = (tools: McpTool[]) => void
type ExecutionCallback = (result: McpExecutionResult) => void

export class McpService {
  private serverUrl: string
  private status: McpConnectionStatus = "disconnected"
  private tools: McpTool[] = []
  private executions: McpExecutionResult[] = []

  private statusListeners: StatusCallback[] = []
  private toolsListeners: ToolsCallback[] = []
  private executionListeners: ExecutionCallback[] = []

  private requestId = 0
  private n8nApiUrl: string = ""
  private n8nApiKey: string = ""
  private mcpAuthToken: string = ""

  private static readonly STORAGE_KEY = "mcp_n8n_config"

  constructor(serverUrl = "http://localhost:3006") {
    this.serverUrl = serverUrl
  }

  async loadConfig(): Promise<void> {
    try {
      const data = await chrome.storage.local.get(McpService.STORAGE_KEY)
      const cfg = data[McpService.STORAGE_KEY]
      if (cfg) {
        if (cfg.proxyUrl) this.serverUrl = cfg.proxyUrl
        if (cfg.n8nApiUrl) this.n8nApiUrl = cfg.n8nApiUrl
        if (cfg.n8nApiKey) this.n8nApiKey = cfg.n8nApiKey
        if (cfg.mcpAuthToken !== undefined) this.mcpAuthToken = cfg.mcpAuthToken
        console.log("[McpService] Config loaded:", this.serverUrl)
      }
    } catch (e) {}
  }

  onStatusChange(callback: StatusCallback) {
    this.statusListeners.push(callback)
    return () => {
      this.statusListeners = this.statusListeners.filter((cb) => cb !== callback)
    }
  }

  onToolsUpdate(callback: ToolsCallback) {
    this.toolsListeners.push(callback)
    return () => {
      this.toolsListeners = this.toolsListeners.filter((cb) => cb !== callback)
    }
  }

  onExecution(callback: ExecutionCallback) {
    this.executionListeners.push(callback)
    return () => {
      this.executionListeners = this.executionListeners.filter((cb) => cb !== callback)
    }
  }

  private setStatus(status: McpConnectionStatus) {
    this.status = status
    this.statusListeners.forEach((cb) => cb(status))
  }

  getStatus(): McpConnectionStatus {
    return this.status
  }

  getTools(): McpTool[] {
    return this.tools
  }

  getExecutions(): McpExecutionResult[] {
    return this.executions
  }

  // --- Connection ---
  async connect(): Promise<void> {
    if (this.status === "connected" || this.status === "connecting") return
    this.setStatus("connecting")

    try {
      await this.loadConfig()

      // The 'initialize' call is special in HTTP Streamable.
      // We send it via POST and we expect the server to just say OK (200/204) 
      // or start a stream.
      const result = await this.sendRequest("initialize", {
        protocolVersion: "2024-11-05",
        capabilities: {},
        clientInfo: { name: "AnythingCopilot", version: "1.0.0" }
      })

      console.log("[McpService] Initialized successfully")
      this.setStatus("connected")
      this.fetchTools()
    } catch (error) {
      console.error("[McpService] Connection failed:", error)
      this.setStatus("error")
    }
  }

  async disconnect(): Promise<void> {
    this.setStatus("disconnected")
  }

  // --- JSON-RPC Messaging ---
  private async sendRequest(method: string, params?: any): Promise<any> {
    const id = ++this.requestId
    const message = { jsonrpc: "2.0", id, method, params: params || {} }

    let endpoint = this.serverUrl
    // Fallback for old local proxy
    if (!this.serverUrl.includes("/mcp-server/http") && !this.serverUrl.includes("localhost")) {
      if (method === "tools/list" || method === "tools/call") {
        endpoint = `${this.serverUrl}/message`
      }
    }

    try {
      const headers: Record<string, string> = { 
        "Content-Type": "application/json",
        "Accept": "application/json, text/event-stream"
      }
      if (this.mcpAuthToken) {
        headers["Authorization"] = `Bearer ${this.mcpAuthToken}`
      }
      if (this.n8nApiUrl) headers["x-n8n-url"] = this.n8nApiUrl
      if (this.n8nApiKey) headers["x-n8n-key"] = this.n8nApiKey

      const response = await fetch(endpoint, {
        method: "POST",
        headers,
        body: JSON.stringify(message),
      })

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`)
      }

      // n8n cloud often returns a stream even for a single RPC call.
      // We need to try parsing as JSON, but if it fails (because it's SSE),
      // we assume it succeeded if status is 200.
      const text = await response.text()
      try {
        const data = JSON.parse(text)
        if (data.error) throw data.error
        return data.result || data
      } catch (e) {
        // If it looks like SSE data, it's actually success for n8n
        if (text.includes("event:") || text.includes("data:")) {
          return { success: true } 
        }
        // If it's empty but 200 OK
        if (text.trim() === "" && response.status === 200) {
          return { success: true }
        }
        throw new Error("Invalid response format")
      }
    } catch (error) {
      console.error(`[McpService] Request ${method} failed:`, error)
      throw error
    }
  }

  // --- Tool Operations ---
  async fetchTools(): Promise<McpTool[]> {
    try {
      const result = await this.sendRequest("tools/list")
      const tools = result.tools || result || []
      if (Array.isArray(tools)) {
        this.tools = tools.map((t: any) => ({
          name: t.name,
          description: t.description || "",
          inputSchema: t.inputSchema || t.input_schema,
        }))
        this.toolsListeners.forEach((cb) => cb(this.tools))
      }
      return this.tools
    } catch (error) {
      return this.tools
    }
  }

  async callTool(toolName: string, args: Record<string, any> = {}): Promise<McpExecutionResult> {
    const execution: McpExecutionResult = {
      id: `exec_${Date.now()}`,
      toolName,
      args,
      status: "pending",
      timestamp: Date.now(),
    }

    this.executions.unshift(execution)
    this.executionListeners.forEach((cb) => cb(execution))

    try {
      const result = await this.sendRequest("tools/call", {
        name: toolName,
        arguments: args,
      })

      execution.status = "success"
      execution.result = result
      this.executionListeners.forEach((cb) => cb(execution))
      return execution
    } catch (error: any) {
      execution.status = "error"
      execution.error = error?.message || String(error)
      this.executionListeners.forEach((cb) => cb(execution))
      return execution
    }
  }

  clearExecutions(): void {
    this.executions = []
  }
}

let _instance: McpService | null = null
export function getMcpService(url?: string): McpService {
  if (!_instance) _instance = new McpService(url)
  return _instance
}
