/**
 * @sandcode/browser-mcp — vendor pkg.
 *
 * Bridges the SANDCODE CLI to the SANDCODE browser extension over MCP.
 * The extension implements the actual tool handlers (chrome.tabs API,
 * chrome.debugger console reads, chrome.scripting JS eval, etc.). This
 * package exposes them as MCP tools and relays CallTool requests through
 * the chrome native messaging host's Unix-socket / Win-named-pipe end
 * (created by src/utils/browserIntegration/chromeNativeHost.ts).
 *
 * v0.2: real CallTool relay. Connects to the native host socket, frames
 * each tool_request with a generated request_id, awaits the matching
 * tool_response. Response correlation requires the extension to echo
 * request_id (already implemented in chrome-extension/src/background.js)
 * AND the native host to forward it (added in chromeNativeHost.ts:type
 * ToolRequest + the sendChromeMessage call in handleMcpClient).
 *
 * Layout note: this is a vendor pkg installed via `file:./src/vendor/...`
 * in package.json — there is no separate publish flow yet. To upgrade
 * the tool list, edit BROWSER_TOOLS below and rebuild the CLI.
 */

import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
import { connect } from 'net'
import { randomUUID } from 'crypto'

// ─── Tool surface ─────────────────────────────────────────────────────────

/**
 * The browser-tool surface the agent sees. Each entry mirrors the MCP
 * tool definition shape: { name, description, inputSchema }.
 *
 * Tool names are namespaced via the MCP server name `sandcode-browser`
 * (set in src/utils/browserIntegration/common.ts), so the agent calls
 * them as `mcp__sandcode-browser__<name>`.
 */
export const BROWSER_TOOLS = [
  {
    name: 'tabs_context_mcp',
    description:
      'List the currently open browser tabs (URL, title, active state). Call this at the start of any browser session before creating new tabs so you reuse what is already open.',
    inputSchema: {
      type: 'object',
      properties: {},
      additionalProperties: false,
    },
  },
  {
    name: 'tabs_create_mcp',
    description: 'Open a new browser tab at the given URL. Returns the new tabId.',
    inputSchema: {
      type: 'object',
      properties: {
        url: { type: 'string', description: 'URL to open.' },
        active: { type: 'boolean', description: 'Focus the new tab. Default true.' },
      },
      required: ['url'],
      additionalProperties: false,
    },
  },
  {
    name: 'tabs_close_mcp',
    description: 'Close a browser tab by tabId.',
    inputSchema: {
      type: 'object',
      properties: { tabId: { type: 'number', description: 'Tab to close.' } },
      required: ['tabId'],
      additionalProperties: false,
    },
  },
  {
    name: 'read_console_messages',
    description:
      "Read the JavaScript console messages from a tab. Use the `pattern` regex to filter when output is verbose. Returns up to 200 messages by default.",
    inputSchema: {
      type: 'object',
      properties: {
        tabId: { type: 'number' },
        pattern: { type: 'string', description: 'Optional regex pattern to filter messages.' },
        sinceTimestamp: {
          type: 'number',
          description: 'Only return messages with timestamp >= this value.',
        },
        max: { type: 'number', description: 'Max messages. Default 200.' },
      },
      required: ['tabId'],
      additionalProperties: false,
    },
  },
  {
    name: 'take_screenshot',
    description:
      "Capture a base64-encoded PNG of a tab's viewport. If no tabId is provided, captures the active tab.",
    inputSchema: {
      type: 'object',
      properties: {
        tabId: { type: 'number' },
        format: { type: 'string', enum: ['png', 'jpeg'] },
        quality: { type: 'number', minimum: 1, maximum: 100 },
      },
      additionalProperties: false,
    },
  },
  {
    name: 'javascript_tool',
    description:
      "Run a JavaScript snippet in the MAIN world of a tab and return the result. The snippet runs as a function body with structured-cloneable args bound to arg0..argN.",
    inputSchema: {
      type: 'object',
      properties: {
        tabId: { type: 'number' },
        code: { type: 'string', description: 'JavaScript function body.' },
        args: {
          type: 'array',
          description: 'Arguments passed to the function as arg0..argN.',
        },
      },
      required: ['tabId', 'code'],
      additionalProperties: false,
    },
  },
]

const TOOL_NAMES = new Set(BROWSER_TOOLS.map((t) => t.name))

// ─── Native host socket bridge ────────────────────────────────────────────

const REQUEST_TIMEOUT_MS = 30_000
const MAX_MESSAGE_SIZE = 1024 * 1024 // matches chromeNativeHost.ts

/**
 * Persistent connection to the chromeNativeHost Unix socket / Win named
 * pipe. Created lazily on first CallTool, reused across calls. Auto-
 * reconnects on disconnect; pending requests reject with a connection
 * error if the socket dies before their response arrives.
 */
class NativeHostBridge {
  constructor(socketPath, getSocketPaths, logger) {
    this.socketPath = socketPath
    this.getSocketPaths = getSocketPaths
    this.logger = logger
    this.socket = null
    this.connectPromise = null
    this.buffer = Buffer.alloc(0)
    /** request_id → { resolve, reject, timeoutHandle } */
    this.pending = new Map()
  }

  async ensureConnected() {
    if (this.socket && !this.socket.destroyed) return
    if (this.connectPromise) return this.connectPromise

    this.connectPromise = new Promise((resolve, reject) => {
      // Try the primary socket path first; if it fails, sweep alternates
      // returned by getSocketPaths (multiple sandcode CLIs may be running).
      const candidates = [this.socketPath, ...((this.getSocketPaths?.() ?? []).filter(
        (p) => p !== this.socketPath,
      ))]
      const tryNext = (i) => {
        if (i >= candidates.length) {
          reject(new Error('No reachable sandcode native host socket. Is the browser extension installed and running?'))
          return
        }
        const path = candidates[i]
        const sock = connect(path)
        sock.once('connect', () => {
          this.socket = sock
          this.attachHandlers(sock)
          this.logger?.info?.(`[browser-mcp] connected to native host: ${path}`)
          resolve()
        })
        sock.once('error', () => {
          sock.destroy()
          tryNext(i + 1)
        })
      }
      tryNext(0)
    }).finally(() => {
      this.connectPromise = null
    })

    return this.connectPromise
  }

  attachHandlers(sock) {
    sock.on('data', (chunk) => {
      this.buffer = Buffer.concat([this.buffer, chunk])
      while (this.buffer.length >= 4) {
        const length = this.buffer.readUInt32LE(0)
        if (length === 0 || length > MAX_MESSAGE_SIZE) {
          this.logger?.warn?.(`[browser-mcp] invalid frame length: ${length}, destroying socket`)
          sock.destroy()
          return
        }
        if (this.buffer.length < 4 + length) break
        const body = this.buffer.subarray(4, 4 + length).toString('utf-8')
        this.buffer = this.buffer.subarray(4 + length)
        let parsed
        try {
          parsed = JSON.parse(body)
        } catch (e) {
          this.logger?.warn?.(`[browser-mcp] non-JSON response from native host`)
          continue
        }
        const requestId = parsed.request_id
        if (typeof requestId !== 'string') continue // not for us (broadcast)
        const entry = this.pending.get(requestId)
        if (!entry) continue
        clearTimeout(entry.timeoutHandle)
        this.pending.delete(requestId)
        if (parsed.error !== undefined) {
          entry.reject(new Error(String(parsed.error)))
        } else {
          entry.resolve(parsed.result ?? parsed)
        }
      }
    })
    sock.on('close', () => {
      this.logger?.info?.(`[browser-mcp] native host socket closed`)
      this.socket = null
      this.buffer = Buffer.alloc(0)
      // Reject any pending — caller can decide to retry.
      for (const [id, entry] of this.pending) {
        clearTimeout(entry.timeoutHandle)
        entry.reject(new Error('Native host socket closed before response arrived'))
        this.pending.delete(id)
      }
    })
    sock.on('error', (err) => {
      this.logger?.warn?.(`[browser-mcp] native host socket error: ${err.message}`)
    })
  }

  async callTool(method, params) {
    await this.ensureConnected()
    const requestId = randomUUID()
    const messageBody = Buffer.from(
      JSON.stringify({ method, params: params ?? {}, request_id: requestId }),
      'utf-8',
    )
    const lengthPrefix = Buffer.alloc(4)
    lengthPrefix.writeUInt32LE(messageBody.length, 0)
    return new Promise((resolve, reject) => {
      const timeoutHandle = setTimeout(() => {
        this.pending.delete(requestId)
        reject(new Error(`Browser tool '${method}' timed out after ${REQUEST_TIMEOUT_MS}ms (extension not connected?)`))
      }, REQUEST_TIMEOUT_MS)
      this.pending.set(requestId, { resolve, reject, timeoutHandle })
      try {
        this.socket.write(lengthPrefix)
        this.socket.write(messageBody)
      } catch (err) {
        clearTimeout(timeoutHandle)
        this.pending.delete(requestId)
        reject(err)
      }
    })
  }
}

// ─── MCP server factory ───────────────────────────────────────────────────

/**
 * Create an MCP server that exposes BROWSER_TOOLS via ListTools and
 * relays CallTool through the chromeNativeHost socket bridge.
 *
 * @param {object} context BrowserIntegrationContext from
 *   src/utils/browserIntegration/mcpServer.ts. Required fields:
 *   - socketPath: string — primary native-host socket path
 *   - getSocketPaths: () => string[] — fallback sockets when primary fails
 *   - logger: { info, warn, error } — optional structured logger
 */
export function createBrowserIntegrationMcpServer(context) {
  const server = new Server(
    {
      name: 'sandcode-browser',
      version: '0.2.0',
    },
    {
      capabilities: { tools: {} },
    },
  )

  const bridge = new NativeHostBridge(
    context?.socketPath,
    context?.getSocketPaths,
    context?.logger,
  )

  server.setRequestHandler(ListToolsRequestSchema, async () => ({
    tools: BROWSER_TOOLS,
  }))

  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params
    if (!TOOL_NAMES.has(name)) {
      return {
        content: [{ type: 'text', text: `Unknown browser tool: ${name}` }],
        isError: true,
      }
    }
    try {
      const result = await bridge.callTool(name, args ?? {})
      return {
        content: [
          {
            type: 'text',
            text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
          },
        ],
      }
    } catch (err) {
      return {
        content: [
          {
            type: 'text',
            text: `Browser tool error: ${err instanceof Error ? err.message : String(err)}`,
          },
        ],
        isError: true,
      }
    }
  })

  return server
}

export default {
  BROWSER_TOOLS,
  createBrowserIntegrationMcpServer,
}
