import puppeteer, { Browser, BrowserContext, ConnectOptions, ElementHandle, GoToOptions, Page } from 'puppeteer-core'
import { Context, h, hyphenate, Schema, Service } from 'koishi'
import { SVG, SVGOptions } from './svg'
import find from 'puppeteer-finder'
import Canvas from './canvas'

import { tmpdir } from 'node:os'
import { pathToFileURL } from 'node:url'
import { resolve, join } from 'node:path'
import { existsSync, mkdirSync } from 'node:fs'

import type { } from '@cordisjs/plugin-proxy-agent'
import type { } from 'koishi-plugin-glyph'

export * from './svg'

declare module 'koishi' {
  interface Context {
    puppeteer: Puppeteer
  }
}

type RenderCallback = (page: Page, next: (handle?: ElementHandle) => Promise<string>) => Promise<string>

export async function injectDefaultFont(page: Page, ctx: Context, config: Puppeteer.Config, fontDataUrl?: string) {
  if (!config.enableFont) {
    return
  }

  if (!fontDataUrl) {
    return
  }

  try {
    // 使用 Data URL 注入字体，无需 Service Worker 缓存
    await page.addStyleTag({
      content: `
        @font-face {
          font-family: "KoishiDefaultFont";
          src: url("${fontDataUrl}");
          font-display: swap;
        }
      `
    })

    // 根据注入模式决定是否需要检查页面字体
    let shouldInject = true

    if (config.fontInjectMode === 'smart') {
      const hasAnyFontFamily = await page.evaluate(() => {
        for (let i = 0; i < document.styleSheets.length; i++) {
          try {
            const styleSheet = document.styleSheets[i]
            if (styleSheet.cssRules) {
              for (let j = 0; j < styleSheet.cssRules.length; j++) {
                const rule = styleSheet.cssRules[j]
                if (rule instanceof CSSStyleRule && rule.style.fontFamily) {
                  return true
                }
              }
            }
          } catch (e) {
            // 跨域样式表可能无法访问，忽略错误
          }
        }

        const elementsWithInlineFont = document.querySelectorAll('[style*="font-family"]')
        if (elementsWithInlineFont.length > 0) {
          return true
        }

        return false
      })

      // 判断模式：只有在页面没有设置字体时才注入
      shouldInject = !hasAnyFontFamily
    }

    // force 模式：shouldInject 保持为 true，无条件注入

    // 根据判断结果决定是否注入字体样式
    if (shouldInject) {
      await page.addStyleTag({
        content: `
          /* 全局应用默认字体 */
          *, *::before, *::after {
            font-family: "KoishiDefaultFont" !important;
          }

          html, body, div, span, p, h1, h2, h3, h4, h5, h6,
          input, textarea, button, select, option, canvas {
            font-family: "KoishiDefaultFont" !important;
          }
        `
      })

      await page.evaluate(`
        new Promise((resolve) => {
          let resolved = false;

          // 设置超时保护，最多等待 3 秒
          setTimeout(() => {
            if (!resolved) {
              resolved = true;
              resolve();
            }
          }, 3000);

          // 检查字体加载
          (function() {
            if (document.fonts && document.fonts.check) {
              try {
                const fontLoaded = document.fonts.check('16px "KoishiDefaultFont"');
                if (fontLoaded) {
                  if (!resolved) {
                    resolved = true;
                    resolve();
                  }
                  return;
                }
              } catch (e) {
                // 如果 check 方法失败，继续使用其他方法
              }
            }

            // 备用方法：等待 document.fonts.ready
            if (document.fonts && document.fonts.ready) {
              document.fonts.ready.then(() => {
                setTimeout(() => {
                  if (!resolved) {
                    resolved = true;
                    resolve();
                  }
                }, 100);
              }).catch(() => {
                setTimeout(() => {
                  if (!resolved) {
                    resolved = true;
                    resolve();
                  }
                }, 500);
              });
            } else {
              setTimeout(() => {
                if (!resolved) {
                  resolved = true;
                  resolve();
                }
              }, 500);
            }
          })();
        })
      `)
    }
    // 如果页面已经设置了 font-family，则不注入任何字体样式
  } catch (error) {
    ctx.logger.error('默认字体注入失败:', error.message)
  }
}

class Puppeteer extends Service {
  static [Service.provide] = 'puppeteer'
  static inject = {
    required: ['http'],
    optional: ['glyph']
  }

  browser: Browser
  executable: string
  private browserWSEndpoint: string
  private activePageCount: number = 0
  private activeContextCount: number = 0
  private isRestarting: boolean = false // 是否正在重启
  private disposeKeepAlive: (() => void) | null = null // 保活定时器销毁函数

  private originalBrowserMethods = new WeakMap<Browser, {
    newPage: Browser['newPage']
    createBrowserContext: Browser['createBrowserContext']
  }>()
  private patchedBrowsers = new WeakSet<Browser>()
  private trackedPages = new WeakSet<Page>()
  private trackedContexts = new WeakSet<BrowserContext>()
  constructor(ctx: Context, public config: Puppeteer.Config) {
    super(ctx, 'puppeteer')
    if (this.config.enableCanvas !== false) {
      ctx.plugin(Canvas)
    }

    // 根据配置注册 HTML 组件
    if (this.config.registerHtmlComponent) {
      this.registerHtmlComponent()
    }

    // 根据配置决定是否注册重启指令
    if (this.config.enableRestartCommand !== false && !this.config.immediateClose) {
      ctx.command('puppeteer.restart', '重启 Puppeteer 浏览器服务')
        .action(async ({ session }) => {
          try {
            await session?.send('正在重启 Puppeteer 服务...')

            // 停止当前浏览器实例
            await this.stopBrowser()

            // 重新启动浏览器
            await this.startBrowser()

            return '✅ Puppeteer 服务重启成功'
          } catch (error) {
            ctx.logger.error('Puppeteer 服务重启失败:', error)
            return `❌ Puppeteer 服务重启失败: ${error.message}`
          }
        })
    } else if (this.config.immediateClose && this.config.enableRestartCommand !== false) {
      ctx.logger.warn('immediateClose 模式下 puppeteer.restart 指令已被禁用')
    }
  }

  private patchBrowser(browser: Browser) {
    if (this.patchedBrowsers.has(browser)) {
      return
    }

    this.patchedBrowsers.add(browser)

    this.originalBrowserMethods.set(browser, {
      newPage: browser.newPage.bind(browser),
      createBrowserContext: browser.createBrowserContext.bind(browser),
    })

    browser.newPage = async (...args: Parameters<Browser['newPage']>) => {
      await this.ensureConnected()
      const currentBrowser = this.browser
      if (!currentBrowser) {
        throw new Error('浏览器尚未启动')
      }
      const originalMethods = this.originalBrowserMethods.get(currentBrowser)
      if (!originalMethods) {
        throw new Error('浏览器实例尚未完成方法补丁注册')
      }
      const page = await originalMethods.newPage(...args)
      return this.trackPage(page)
    }

    browser.createBrowserContext = async (...args: Parameters<Browser['createBrowserContext']>) => {
      await this.ensureConnected()
      const currentBrowser = this.browser
      if (!currentBrowser) {
        throw new Error('浏览器尚未启动')
      }
      const originalMethods = this.originalBrowserMethods.get(currentBrowser)
      if (!originalMethods) {
        throw new Error('浏览器实例尚未完成方法补丁注册')
      }
      const context = await originalMethods.createBrowserContext(...args)
      return this.trackBrowserContext(context)
    }
  }

  private trackBrowserContext(context: BrowserContext) {
    if (this.trackedContexts.has(context)) {
      return context
    }

    this.trackedContexts.add(context)

    if (this.config.immediateClose) {
      this.activeContextCount++
    }

    const originalNewPage = context.newPage.bind(context)
    context.newPage = async (...args: Parameters<BrowserContext['newPage']>) => {
      const page = await originalNewPage(...args)
      return this.trackPage(page)
    }

    const originalClose = context.close.bind(context)
    let released = false

    const release = async () => {
      if (released || !this.config.immediateClose) {
        return
      }

      released = true
      this.activeContextCount = Math.max(0, this.activeContextCount - 1)
      await this.checkAndCloseBrowser()
    }

    context.close = async () => {
      try {
        await originalClose()
      } finally {
        await release()
      }
    }

    return context
  }

  private trackPage(page: Page) {
    if (this.trackedPages.has(page)) {
      return page
    }

    this.trackedPages.add(page)

    if (this.config.immediateClose) {
      this.activePageCount++
    }

    const originalClose = page.close.bind(page)
    let released = false

    const release = async () => {
      if (released || !this.config.immediateClose) {
        return
      }

      released = true
      this.activePageCount = Math.max(0, this.activePageCount - 1)
      await this.checkAndCloseBrowser()
    }

    page.once('close', () => {
      void release()
    })

    page.close = async (...args: Parameters<Page['close']>) => {
      try {
        await originalClose(...args)
      } finally {
        await release()
      }
    }

    return page
  }

  private getFontCacheDir(customDir?: string): string {
    if (customDir && customDir.trim()) {
      // 使用用户指定的目录
      const dir = resolve(customDir.trim())
      if (!existsSync(dir)) {
        mkdirSync(dir, { recursive: true })
      }
      return dir
    }

    // 使用默认目录
    const defaultDir = join(tmpdir(), '.koishi-puppeteer-userDataDir');
    if (!existsSync(defaultDir)) {
      mkdirSync(defaultDir, { recursive: true })
    }
    return defaultDir
  }

  private registerHtmlComponent() {
    const transformStyle = (source: {}, base = {}) => {
      return Object.entries({ ...base, ...source }).map(([key, value]) => {
        return `${hyphenate(key)}: ${Array.isArray(value) ? value.join(', ') : value}`
      }).join('; ')
    }

    this.ctx.component('html', async (attrs, children) => {
      const head: h[] = []

      const transform = (element: h) => {
        if (element.type === 'head') {
          head.push(...element.children)
          return
        }
        const attrs = { ...element.attrs }
        if (typeof attrs.style === 'object') {
          attrs.style = transformStyle(attrs.style)
        }
        return h(element.type, attrs, element.children.map(transform).filter(Boolean))
      }

      // 确保浏览器已连接
      await this.ensureConnected()

      const page = await this.page()
      try {
        if (attrs.src) {
          await page.goto(attrs.src)
        } else {
          await page.goto(pathToFileURL(resolve(__dirname, '../index.html')).href)
          const bodyStyle = typeof attrs.style === 'object'
            ? transformStyle({ display: 'inline-block' }, attrs.style)
            : ['display: inline-block', attrs.style].filter(Boolean).join('; ')
          const content = children.map(transform).filter(Boolean).join('')
          const lang = attrs.lang ? ` lang="${attrs.lang}"` : ''
          await page.setContent(`<html${lang}>
            <head>${head.join('')}</head>
            <body style="${bodyStyle}">${content}</body>
          </html>`)
          // setContent 后重新注入默认字体
          const fontDataUrl = this.getFontDataUrl()
          await injectDefaultFont(page, this.ctx, this.config, fontDataUrl)
        }
        await page.waitForNetworkIdle({
          timeout: attrs.timeout ? +attrs.timeout : undefined,
        })
        const body = await page.$(attrs.selector || 'body')
        const clip = await body.boundingBox()
        const screenshot = await page.screenshot({ clip }) as Buffer
        return h.image(screenshot, 'image/png')
      } finally {
        await page?.close()
      }
    })
  }

  async start() {
    // 如果启用了立即关闭模式，则不在启动时初始化浏览器
    if (!this.config.immediateClose) {
      await this.startBrowser()
      // 启动保活机制
      this.startKeepAlive()
    }
  }

  private async startBrowser() {
    const { remote, endpoint, executablePath, headers, headless, args = [], enableTempUserDataDir, TempUserDataDir, ...config } = this.config

    try {
      if (remote) {
        if (!endpoint) {
          throw new Error('远程浏览器模式下必须提供 endpoint 参数')
        }

        const connectOptions: ConnectOptions = { headers, ...config }

        try {
          const endpointURL = new URL(endpoint)

          if (['ws:', 'wss:'].includes(endpointURL.protocol)) {
            // 不再检查路径格式，接受任何有效的 WebSocket URL
            connectOptions.browserWSEndpoint = endpoint
          } else if (['http:', 'https:'].includes(endpointURL.protocol)) {
            connectOptions.browserURL = endpoint
          } else {
            throw new Error(`不支持的协议: ${endpointURL.protocol}，endpoint 必须以 ws://, wss://, http:// 或 https:// 开头`)
          }
        } catch (e) {
          if (e instanceof TypeError) {
            throw new Error(`无效的 endpoint URL: ${endpoint}，请检查格式是否正确`)
          }
          throw e
        }

        try {
          this.ctx.logger.info('正在连接远程浏览器: %c', endpoint)
          this.browser = await puppeteer.connect(connectOptions)
          this.patchBrowser(this.browser)
          this.ctx.logger.info('远程浏览器连接成功。')

          // 保存 WebSocket 端点，无论是直接提供的还是从 HTTP URL 获取的
          if (connectOptions.browserWSEndpoint) {
            this.browserWSEndpoint = connectOptions.browserWSEndpoint
          } else if (this.browser.wsEndpoint) {
            // 如果使用 HTTP URL，从浏览器实例获取 WebSocket 端点
            this.browserWSEndpoint = this.browser.wsEndpoint()
          }
        } catch (e) {
          // 处理连接错误
          if (e.message?.includes('ECONNREFUSED')) {
            throw new Error(`无法连接到远程浏览器 ${endpoint}，请确保远程浏览器已启动并且端口可访问`)
          } else if (e.message?.includes('not opened')) {
            throw new Error(`远程浏览器连接被拒绝，请确保提供的 endpoint 是正确的并且浏览器已启动调试模式`)
          } else {
            // 确保包含原始错误信息
            throw new Error(`连接远程浏览器失败: ${e.message || e}`)
          }
        }
      } else {
        // 当配置路径存在且可访问时直接使用，否则（未配置或路径不可用）从环境自动查找
        if (executablePath && existsSync(executablePath)) {
          this.executable = executablePath
          this.ctx.logger.info('使用配置指定的浏览器路径: %c', this.executable)
        } else {
          if (executablePath) {
            // 用户填写了路径但文件不存在，给出警告后回退到自动查找
            this.ctx.logger.warn('配置的可执行文件路径不可用: %c，将从环境自动查找', executablePath)
          }

          // 调用 puppeteer-finder 从环境自动发现
          this.executable = find()

          // 找不到时尝试 Termux 特殊路径
          if (!this.executable) {
            const termuxChromiumPath = '/data/data/com.termux/files/usr/bin/chromium-browser'
            if (existsSync(termuxChromiumPath)) {
              this.executable = termuxChromiumPath
              this.ctx.logger.info('在 Termux 环境中找到浏览器: %c', termuxChromiumPath)
            }
          }

          if (!this.executable) {
            throw new Error('未找到 Chrome 可执行文件，请手动指定 executablePath 参数')
          }

          this.ctx.logger.info('找到 Chrome 可执行文件: %c', this.executable)
        }

        // 处理代理设置
        const localArgs = [...args]
        const { proxyAgent } = this.ctx.http.config
        if (proxyAgent && !localArgs.some(arg => arg.startsWith('--proxy-server'))) {
          localArgs.push(`--proxy-server=${proxyAgent}`)
        }

        try {
          // 启动浏览器
          this.ctx.logger.info('正在启动本地浏览器...')

          // 准备启动选项
          const launchOptions: any = {
            executablePath: this.executable,
            headless,
            args: localArgs,
            ...config
          }

          // 固定用户数据目录
          if (enableTempUserDataDir) {
            const userDataDir = this.getFontCacheDir(TempUserDataDir)
            launchOptions.userDataDir = userDataDir
            this.ctx.logger.info('用户数据目录: %c', userDataDir)
          }

          // 最多重试 3 次，应对端口短暂占用或进程未完全释放等偶发问题
          const maxLaunchRetries = 3
          let launched = false
          for (let attempt = 1; attempt <= maxLaunchRetries; attempt++) {
            try {
              this.browser = await puppeteer.launch(launchOptions)
              this.patchBrowser(this.browser)
              this.browserWSEndpoint = this.browser.wsEndpoint()
              this.ctx.logger.info('本地浏览器启动成功。')
              launched = true
              break
            } catch (e) {
              if (attempt < maxLaunchRetries) {
                // 遇到端口占用或启动失败时，稍等片刻后重试（Chrome 会自动换端口）
                this.ctx.logger.warn(`浏览器启动失败，1 秒后重试 (${attempt}/${maxLaunchRetries}): ${e.message}`)
                await this.ctx.sleep(1000)
              } else {
                // 已达最大重试次数，抛出详细错误
                if (e.message?.includes('Failed to launch') || e.message?.includes('EADDRINUSE')) {
                  throw new Error(`启动浏览器失败（已重试 ${maxLaunchRetries} 次），请检查 Chrome 是否已安装或端口是否被其他进程持续占用: ${e.message}`)
                } else {
                  throw new Error(`启动浏览器失败: ${e.message || e}`)
                }
              }
            }
          }
          if (!launched) {
            throw new Error('浏览器启动失败：未知原因导致重试循环退出')
          }
        } catch (e) {
          // 重新抛出，由外层统一处理
          throw e
        }
      }
    } catch (error) {
      this.ctx.logger.error(`Puppeteer 初始化失败: `, error)
      throw error
    }
  }

  async stop() {
    // 停止保活定时器
    this.stopKeepAlive()
    await this.stopBrowser()
  }

  private async stopBrowser() {
    try {
      if (this.browser) {
        if (this.config.remote) {
          await this.browser.disconnect()
        } else {
          await this.browser.close()
        }
        this.browser = null
        this.browserWSEndpoint = null
        this.activePageCount = 0
        this.activeContextCount = 0
      }
    } catch (error) {
      this.ctx.logger.warn('停止浏览器时出现错误:', error.message)
    }
  }

  // 启动保活机制
  private startKeepAlive() {
    // 如果未启用保活或启用了立即关闭模式，则不启动保活
    if (!this.config.enableKeepAlive || this.config.immediateClose) {
      return
    }

    // 清除已有的定时器
    this.stopKeepAlive()

    const interval = this.config.keepAliveInterval || 30000 // 默认30秒检查一次

    this.disposeKeepAlive = this.ctx.setInterval(async () => {
      await this.checkBrowserHealth()
    }, interval)
  }

  // 停止保活机制
  private stopKeepAlive() {
    if (this.disposeKeepAlive) {
      this.disposeKeepAlive()
      this.disposeKeepAlive = null
    }
  }

  // 检查浏览器健康状态
  private async checkBrowserHealth() {
    // 如果正在重启，跳过本次检查
    if (this.isRestarting) {
      return
    }

    // 如果浏览器未初始化，跳过检查
    if (!this.browser) {
      return
    }

    try {
      // 检查浏览器连接状态
      if (!this.browser.connected) {
        this.ctx.logger.warn('检测到浏览器连接已断开，尝试重新连接...')
        await this.restartBrowserSafely()
        return
      }

      // 尝试获取浏览器版本来验证浏览器是否真正存活
      await this.browser.version()

      // 如果能成功获取版本，说明浏览器正常
    } catch (error) {
      // 如果获取版本失败，说明浏览器进程可能已被杀死
      this.ctx.logger.warn('浏览器健康检查失败: %s，尝试重启...', error.message)
      await this.restartBrowserSafely()
    }
  }

  // 安全地重启浏览器
  private async restartBrowserSafely() {
    // 防止并发重启
    if (this.isRestarting) {
      return
    }

    this.isRestarting = true

    try {
      this.ctx.logger.info('开始重启浏览器...')

      // 停止当前浏览器
      await this.stopBrowser()

      // 等待一小段时间确保资源释放
      await this.ctx.sleep(1000)

      // 重新启动浏览器
      await this.startBrowser()

      this.ctx.logger.info('浏览器重启成功')
    } catch (error) {
      this.ctx.logger.error('浏览器重启失败: %s', error.message)
    } finally {
      this.isRestarting = false
    }
  }

  // 检查并关闭浏览器
  private async checkAndCloseBrowser() {
    if (!this.config.immediateClose) return

    // 只有页面和上下文都已释放后，才允许关闭整个浏览器。
    if (this.activePageCount <= 0 && this.activeContextCount <= 0 && this.browser) {
      try {
        // 获取所有页面
        const pages = await this.browser.pages()

        // 检查是否只剩下空白页（about:blank）
        const nonBlankPages = pages.filter(page => page.url() !== 'about:blank')

        if (nonBlankPages.length === 0) {
          await this.stopBrowser()
        }
      } catch (error) {
        this.ctx.logger.warn('检查浏览器状态时出现错误:', error.message)
      }
    }
  }

  // 检查浏览器连接状态并尝试重连
  private async ensureConnected() {
    // 如果启用了立即关闭模式，且浏览器未连接，则启动浏览器
    if (this.config.immediateClose && (!this.browser || !this.browser.connected)) {
      await this.startBrowser()
      return
    }

    // 如果浏览器已连接，直接返回
    if (this.browser && this.browser.connected) return

    // 如果未启用重连，则抛出异常
    if (this.config.enableReconnect === false) {
      throw new Error('浏览器连接已断开，且未启用自动重连')
    }

    // 本地模式：浏览器进程重启后端点（含随机端口）会失效，不应尝试 connect() 旧端点
    // 直接重新启动一个新的浏览器进程，避免因旧端口被其他进程占用导致连接失败
    if (!this.config.remote) {
      this.ctx.logger.warn('本地浏览器连接已断开，将重新启动浏览器...')
      await this.restartBrowserSafely()
      return
    }

    // 远程模式：检查是否有可用的重连端点
    const hasReconnectEndpoint = this.browserWSEndpoint || this.config.endpoint
    if (!hasReconnectEndpoint) {
      throw new Error('远程浏览器连接已断开，且没有可用的重连端点')
    }

    // 尝试重连，使用配置的最大重试次数
    let retryCount = 0
    const maxRetries = this.config.maxReconnectRetries ?? 3

    while (retryCount < maxRetries) {
      try {
        this.ctx.logger.info(`浏览器连接已断开，尝试重新连接... (尝试 ${retryCount + 1}/${maxRetries})`)

        // 如果设置了重连间隔，则等待指定时间
        if (this.config.reconnectInterval > 0) {
          await this.ctx.sleep(this.config.reconnectInterval)
        }

        // 准备连接选项
        const connectOptions: ConnectOptions = { ...this.config }

        // 优先使用保存的 WebSocket 端点，如果没有则使用配置的端点
        if (this.browserWSEndpoint) {
          connectOptions.browserWSEndpoint = this.browserWSEndpoint
        } else if (this.config.endpoint) {
          // 检查端点类型
          try {
            const endpointURL = new URL(this.config.endpoint)
            if (['ws:', 'wss:'].includes(endpointURL.protocol)) {
              connectOptions.browserWSEndpoint = this.config.endpoint
            } else if (['http:', 'https:'].includes(endpointURL.protocol)) {
              connectOptions.browserURL = this.config.endpoint
            }
          } catch (e) {
            this.ctx.logger.warn('解析端点 URL 失败: %c', e.message)
          }
        }

        // 尝试重新连接
        this.browser = await puppeteer.connect(connectOptions)
        this.patchBrowser(this.browser)

        // 检查连接是否成功
        if (this.browser.connected) {
          this.ctx.logger.info('浏览器重新连接成功')

          // 保存新的 WebSocket 端点
          if (this.browser.wsEndpoint) {
            this.browserWSEndpoint = this.browser.wsEndpoint()
          }

          return
        } else {
          throw new Error('连接后浏览器状态仍为断开')
        }
      } catch (e) {
        retryCount++

        // 检查是否是 404 错误，如果是，则尝试使用原始配置重新连接
        if (e.message?.includes('404') || e.message?.includes('Unexpected server response: 404')) {
          this.ctx.logger.warn('检测到 404 错误，尝试使用原始配置重新连接')

          try {
            // 重置端点信息
            this.browserWSEndpoint = null

            // 使用原始配置重新连接
            const originalOptions: ConnectOptions = { ...this.config }
            if (this.config.endpoint) {
              const endpointURL = new URL(this.config.endpoint)
              if (['ws:', 'wss:'].includes(endpointURL.protocol)) {
                originalOptions.browserWSEndpoint = this.config.endpoint
              } else if (['http:', 'https:'].includes(endpointURL.protocol)) {
                originalOptions.browserURL = this.config.endpoint
              }
            }

            this.browser = await puppeteer.connect(originalOptions)
            this.patchBrowser(this.browser)

            // 检查连接是否成功
            if (this.browser.connected) {
              this.ctx.logger.info('使用原始配置重新连接成功')

              // 保存新的 WebSocket 端点
              if (this.browser.wsEndpoint) {
                this.browserWSEndpoint = this.browser.wsEndpoint()
              }

              return
            }
          } catch (reconnectError) {
            this.ctx.logger.warn('使用原始配置重新连接失败:', reconnectError.message)
          }
        }

        if (retryCount >= maxRetries) {
          this.ctx.logger.error(`浏览器重新连接失败 (${retryCount}/${maxRetries}):`, e.message)
          throw new Error(`浏览器重新连接失败: ${e.message}`)
        } else {
          this.ctx.logger.warn(`浏览器重新连接失败，将重试 (${retryCount}/${maxRetries}):`, e.message)
          // 增加重试间隔
          await this.ctx.sleep(this.config.reconnectInterval * retryCount)
        }
      }
    }
  }

  // 获取字体 Data URL（从 glyph 插件）
  private getFontDataUrl(): string | null {
    if (!this.config.enableFont || !this.config.fontName) {
      return null
    }

    // 从 glyph 服务获取字体 Data URL
    if (this.ctx.glyph) {
      try {
        const fontDataUrl = this.ctx.glyph.getFontDataUrl(this.config.fontName)
        if (!fontDataUrl) {
          this.ctx.logger.warn(`未找到字体: ${this.config.fontName}`)
          return null
        }
        return fontDataUrl
      } catch (error) {
        this.ctx.logger.error('获取字体 Data URL 失败:', error.message)
        return null
      }
    }

    return null
  }

  page = async (options?: Puppeteer.PageOptions) => {
    let page: Page | undefined
    try {
      // 确保浏览器已连接
      await this.ensureConnected()

      // 创建新页面
      // 创建新页面，并统一纳入生命周期跟踪。
      page = await this.browser.newPage()
      page = this.trackPage(page)

      // 设置默认超时时间
      if (this.config.defaultTimeout !== undefined) {
        page.setDefaultTimeout(this.config.defaultTimeout)
      }

      // 注入默认字体
      const fontDataUrl = this.getFontDataUrl()
      await injectDefaultFont(page, this.ctx, this.config, fontDataUrl)

      const originalSetContent = page.setContent.bind(page)
      page.setContent = async (html: string, options?: any) => {
        const result = await originalSetContent(html, options)
        // setContent 后重新注入默认字体
        const fontDataUrl = this.getFontDataUrl()
        await injectDefaultFont(page, this.ctx, this.config, fontDataUrl)
        return result
      }

      if (options) {
        if (options?.beforeGotoPage) {
          await options.beforeGotoPage(page)
        }
        await page.goto(`${pathToFileURL(options.url)}`, options?.gotoOptions)
        if (options?.content) {
          await page.setContent(options.content)
          // setContent 后重新注入默认字体
          const fontDataUrl = this.getFontDataUrl()
          await injectDefaultFont(page, this.ctx, this.config, fontDataUrl)
        }
      }
    } catch (err) {
      if (page) {
        await page.close()
      }
      this.ctx.logger.error('failed to create page: %s', err)
      throw err
    }
    return page
  }

  svg = async (options?: SVGOptions) => {
    // 确保浏览器已连接
    await this.ensureConnected()
    return new SVG(options)
  }

  render = async (content: string, callback?: RenderCallback) => {
    // 确保浏览器已连接
    await this.ensureConnected()

    const url = resolve(__dirname, '../index.html')
    const page = await this.page({ url, content })
    const renderConfig = this.config.render || {}

    callback ||= async (_, next) => page.$('body').then(next)
    const output = await callback(page, async (handle) => {
      const clip = handle ? await handle.boundingBox() : null
      const screenshotOptions = { clip, ...renderConfig }
      const buffer = await page.screenshot(screenshotOptions) as Buffer
      const imageType = renderConfig.type || 'png'
      return h.image(buffer, `image/${imageType}`).toString()
    })

    await page.close()
    return output
  }
}

namespace Puppeteer {
  export interface PageOptions {
    beforeGotoPage?: (page: Page) => Promise<void>
    url: string
    gotoOptions?: GoToOptions
    content?: string
  }

  export const filter = false;

  export const usage = `
---

本插件提供浏览器 API 服务，主要用于网页截图、生成图片等功能。

**重要提示：**

1.  **浏览器环境要求：** 为确保插件正常运行，请确保您的系统已安装 Chromium 浏览器，或已配置远程浏览器服务。
2.  **版本匹配：** 建议保持 Chromium/Chrome 浏览器与本插件同步更新，以避免因版本不匹配导致的功能异常。
3.  **Windows 系统兼容性：** 如果您在 Windows 服务器上运行，为支持最新版 Puppeteer 及其捆绑的 Chromium，您的操作系统至少需要是 **Windows Server 2016 或更高版本**。

注意： Windows Server 2012 R2 及更早版本已无法满足最新 Chrome 的运行要求。

---


**服务依赖：**

- **必需服务：** http（自带服务，无需额外安装）
- **可选服务：** [glyph](/market?keyword=glyph) - 用于字体注入功能（启用字体注入时需要）

---

<p>➣ <a href="https://github.com/shangxueink/koishi-plugin-puppeteer-without-canvas" target="_blank">点我前往项目地址</a></p>

---
  `;

  type LaunchOptions = Parameters<typeof puppeteer.launch>[0] & {}
  export type ImageType = 'png' | 'jpeg' | 'webp'

  export interface Config extends LaunchOptions, ConnectOptions {
    enablePuppeteer?: boolean
    enableCanvas?: boolean
    registerHtmlComponent?: boolean
    enableRestartCommand?: boolean
    remote?: boolean
    endpoint?: string
    headers?: Record<string, string>
    enableReconnect?: boolean
    reconnectInterval?: number
    maxReconnectRetries?: number
    immediateClose?: boolean
    enableKeepAlive?: boolean
    keepAliveInterval?: number
    enableFont?: boolean
    fontName?: string
    fontInjectMode?: 'force' | 'smart'
    enableTempUserDataDir?: boolean
    TempUserDataDir?: string
    defaultTimeout?: number
    render?: {
      type?: ImageType
      quality?: number
    }
  }

  export const Config = Schema.intersect([
    Schema.object({
      remote: Schema.boolean().description('是否连接到远程浏览器。').default(false),
    }).description('连接设置'),
    Schema.union([
      Schema.object({
        remote: Schema.const(true).required(),
        endpoint: Schema.string().description(
          '远程浏览器的端点。<br>' +
          '例：<br>' +
          'WebSocket URL:  `ws://localhost:14550/devtools/browser/[id]`<br>' +
          'HTTP URL:  `http://localhost:14550`<br>'
        ).required(),
        headers: Schema.dict(String).role('table').description(
          '连接到远程浏览器时使用的 HTTP 请求头。<br>' +
          '注意：这与本地模式的 `args` 不同，headers 只影响连接请求，不影响浏览器本身的行为。<br>' +
          '常用于设置身份验证、API密钥或自定义标识等。详细示例请参考 README。'
        ),
      }).description('远程浏览器设置'),
      Schema.object({
        remote: Schema.const(false).default(false),
        executablePath: Schema.string().description(
          '`Chrome/Chromium 可执行文件`的路径。一般无需指定。<br>' +
          '**缺省或路径不可用时**，将自动从系统环境中查找。<br>' +
          '仅当自动查找失败时，才需要手动指定此路径。'
        ),
        headless: Schema.boolean().description('是否开启[无头模式](https://developer.chrome.com/blog/headless-chrome/)。无头模式下浏览器不会显示界面。').default(true),
        immediateClose: Schema.boolean().description('是否在渲染完成后 立即关闭浏览器连接。<br>启用后 会增加每次渲染的启动时间。适用于低频率渲染场景。').default(false).experimental(),
        args: Schema.array(String)
          .description(
            '启动 Chrome/Chromium 浏览器时传递的命令行参数。<br>' +
            '常用参数：<br>' +
            '`--no-sandbox`: 禁用沙箱（在 Docker 或 root 用户下常用）<br>' +
            '`--disable-gpu`: 禁用 GPU 加速<br>' +
            '更多 [Chromium 参数请参考这个页面](https://peter.sh/experiments/chromium-command-line-switches/)。'
          )
          .default(process.getuid?.() === 0 ? ["--no-sandbox", "--disable-gpu", "--disable-web-security"] : ["--disable-web-security"]),
      }).description('本地浏览器设置'),
    ]),

    Schema.object({
      enablePuppeteer: Schema.boolean().description('是否注册 puppeteer 服务。').default(true),
      enableCanvas: Schema.boolean().description('是否注册 canvas 服务。（默认关闭。）<br>注意: 这与[`koishi-plugin-canvas`](/market?keyword=koishi-plugin-canvas+email:shigma10826@gmail.com+email:void@anillc.cn+email:i.dlist@outlook.com)的`canvas`服务同名 但API不一致。').default(false),
      registerHtmlComponent: Schema.boolean().description('是否注册 `component:html` 服务。<br>注意: 启用后会覆盖 Koishi 的默认 `html` 组件行为。').default(true),
    }).description('服务注册'),

    Schema.object({
      enableRestartCommand: Schema.boolean().description('是否注册 `puppeteer.restart` 重启指令。启用后可通过指令重启浏览器服务。').default(false),
      enableReconnect: Schema.boolean().description('是否启用浏览器自动重连功能。当浏览器连接断开时，会尝试重新连接。').default(true),
      reconnectInterval: Schema.number().description('浏览器重连尝试的间隔时间（毫秒）。').default(1000),
      maxReconnectRetries: Schema.number().description('浏览器重连最大尝试次数。').default(3),
    }).description('重连功能设置'),

    Schema.object({
      enableKeepAlive: Schema.boolean().description('是否启用浏览器保活机制。定期检查浏览器健康状态，自动重启被杀死的浏览器进程。<br>**注意**: 仅在非立即关闭模式下有效（即 `immediateClose` 配置项关闭时）。').default(false),
      keepAliveInterval: Schema.number().description('浏览器健康检查的间隔时间（毫秒）。').default(30000).min(5000),
    }).description('保活功能设置'),

    Schema.object({
      render: Schema.intersect([
        Schema.object({
          type: Schema.union(['png', 'jpeg', 'webp'] as const).description('默认渲染的图片类型。').default('png'),
        }),
        Schema.union([
          Schema.object({
            type: Schema.const('png'),
          }),
          Schema.object({
            quality: Schema.number().min(0).max(100).step(1).description('默认渲染的图片质量。').default(80),
          }),
        ]),
      ]),
    }).description('渲染设置'),

    Schema.object({
      defaultTimeout: Schema.number().description('页面渲染的默认超时时间（毫秒）。<br>设置为 0 表示禁用超时。').default(30000).min(0),
      defaultViewport: Schema.object({
        width: Schema.natural().description('默认的视图宽度。').default(1280),
        height: Schema.natural().description('默认的视图高度。').default(768),
        deviceScaleFactor: Schema.number().min(0).description('默认的设备缩放比率。').default(2),
      }),
      ignoreHTTPSErrors: Schema.boolean().description('在导航时忽略 HTTPS 错误。').default(false),
      enableTempUserDataDir: Schema.boolean().description('是否固定用户数据目录。<br>- 需要使用本地浏览器。远程浏览器无效。').default(false).experimental(),
    }).description('浏览器设置'),
    Schema.union([
      Schema.object({
        enableTempUserDataDir: Schema.const(false)
      }),
      Schema.object({
        enableTempUserDataDir: Schema.const(true).required(),
        TempUserDataDir: Schema.string().experimental().default(null)
          .description('用户数据目录路径。建议保持默认值。<br>默认目录:系统`temp`目录下的 `.koishi-puppeteer-userDataDir`。'),
      }),
    ]),

    Schema.object({
      enableFont: Schema.boolean().description('是否为页面注入字体。<br>需要安装 `glyph` 插件并配置字体。').default(false).experimental(),
    }).description('字体注入设置'),
    Schema.union([
      Schema.object({
        enableFont: Schema.const(true).required(),
        fontName: Schema.dynamic('glyph.fonts').description('选择要注入的字体。<br>需要先安装并配置 `glyph` 插件。<br>**注意**: 动态配置项在开发模式下不显示选项，请在生产模式下查看。').experimental(),
        fontInjectMode: Schema.union([
          Schema.const('smart').description('1.判断注入'),
          Schema.const('force').description('2.强制注入'),
        ]).default('smart').experimental()
          .description('字体注入模式。<br>1.判断注入，仅在页面未设置字体时注入<br>2.强制注入，无论页面是否已设置字体都会注入'),
      }),
      Schema.object({
        enableFont: Schema.const(false)
      }),
    ]),

  ]) as Schema<Config>
}

export default Puppeteer
