import { randomUUID } from 'node:crypto'
import {
  BrowserUrlPolicy,
  canonicalizeBrowserUrl,
  type ValidatedBrowserUrl
} from './browser-url-policy'
import { FilteringProxy } from './filtering-proxy'
import type { BrowserScreenshot } from './browser-screenshot'
import { encodeBoundedJpeg } from '../bounded-jpeg'

export type BrowserEventListener = (...argumentsValue: never[]) => void

export type BrowserDebugger = {
  attach(protocolVersion?: string): void
  detach(): void
  isAttached(): boolean
  sendCommand(
    method: string,
    commandParams?: Record<string, unknown>
  ): Promise<unknown>
  on(event: string, listener: BrowserEventListener): unknown
  off(event: string, listener: BrowserEventListener): unknown
}

export type BrowserCapturedImage = {
  getSize(): { width: number; height: number }
  resize(options: {
    width: number
    quality: 'good'
  }): BrowserCapturedImage
  toJPEG(quality: number): Buffer
}

export type BrowserWebContents = {
  debugger: BrowserDebugger
  on(event: string, listener: BrowserEventListener): unknown
  off(event: string, listener: BrowserEventListener): unknown
  setWindowOpenHandler(
    handler: (details: { url: string }) => { action: 'deny' }
  ): void
  capturePage?(): Promise<BrowserCapturedImage>
  getURL(): string
  stop(): void
  close?(options?: { waitForBeforeUnload?: boolean }): void
  destroy(): void
  isDestroyed(): boolean
}

export type BrowserWindowHandle = {
  webContents: BrowserWebContents
  loadURL(url: string): Promise<unknown>
  show(): void
  minimize(): void
  restore(): void
  isMinimized(): boolean
  focus(): void
  on(event: string, listener: BrowserEventListener): unknown
  off(event: string, listener: BrowserEventListener): unknown
  destroy(): void
  isDestroyed(): boolean
}

export type BrowserParentWindowHandle = {
  setEnabled?(enabled: boolean): void
  focus?(): void
  isDestroyed?(): boolean
}

export type BrowserPartitionSession = {
  setPermissionCheckHandler(
    handler: (...argumentsValue: never[]) => boolean
  ): void
  setPermissionRequestHandler(
    handler: (
      webContents: unknown,
      permission: string,
      callback: (granted: boolean) => void,
      details: unknown
    ) => void
  ): void
  setDisplayMediaRequestHandler(
    handler: (
      request: unknown,
      callback: (streams: Record<string, never>) => void
    ) => void
  ): void
  setProxy(configuration: {
    mode: 'fixed_servers'
    proxyRules: string
    proxyBypassRules: string
  }): Promise<void>
  setUserAgent?(
    userAgent: string,
    acceptLanguages?: string
  ): void
  on(event: string, listener: BrowserEventListener): unknown
  off(event: string, listener: BrowserEventListener): unknown
  clearData(): Promise<void>
  closeAllConnections(): Promise<void>
}

export type FilteringProxyLike = {
  start(): Promise<string>
  dispose(): Promise<void>
}

export type ElectronBrowserSessionOptions = {
  policy: BrowserUrlPolicy
  cleanupTimeoutMs?: number
  setupTimeoutMs?: number
  createPartition?: (partition: string) => Promise<BrowserPartitionSession>
  createWindow?: (
    options: Record<string, unknown>
  ) => Promise<BrowserWindowHandle>
  createProxy?: (policy: BrowserUrlPolicy) => FilteringProxyLike
  parentWindow?: BrowserParentWindowHandle
}

type Listener = {
  target: { off(event: string, listener: BrowserEventListener): unknown }
  event: string
  listener: BrowserEventListener
}

function managedBrowserUserAgent(): string {
  const platform =
    process.platform === 'win32'
      ? 'Windows NT 10.0; Win64; x64'
      : process.platform === 'darwin'
        ? 'Macintosh; Intel Mac OS X 10_15_7'
        : `X11; Linux ${process.arch === 'arm64' ? 'aarch64' : 'x86_64'}`
  return `Mozilla/5.0 (${platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${process.versions.chrome ?? '136.0.0.0'} Safari/537.36`
}

async function cleanupIsolatedState(
  partitionSession: BrowserPartitionSession | undefined,
  proxy: FilteringProxyLike,
  timeoutMs: number
): Promise<void> {
  const cleanup = Promise.allSettled([
    partitionSession?.closeAllConnections() ?? Promise.resolve(),
    partitionSession?.clearData() ?? Promise.resolve(),
    proxy.dispose()
  ])
  let timer: ReturnType<typeof setTimeout> | undefined
  const results = await Promise.race([
    cleanup,
    new Promise<never>((_resolve, reject) => {
      timer = setTimeout(
        () => reject(new Error('浏览器隔离数据清理超时')),
        timeoutMs
      )
    })
  ]).finally(() => {
    if (timer) {
      clearTimeout(timer)
    }
  })
  const failure = results.find((result) => result.status === 'rejected')
  if (failure?.status === 'rejected') {
    throw new Error('浏览器隔离数据清理失败', { cause: failure.reason })
  }
}

async function boundedSetup<T>(
  operation: Promise<T>,
  signal: AbortSignal,
  timeoutMs: number,
  cleanupLateValue?: (value: T) => void | Promise<void>
): Promise<T> {
  signal.throwIfAborted()
  const timeout = AbortSignal.timeout(timeoutMs)
  const effectiveSignal = AbortSignal.any([signal, timeout])
  return new Promise<T>((resolve, reject) => {
    const abort = (): void => {
      reject(
        signal.aborted
          ? signal.reason
          : new Error(`浏览器会话创建超时（${timeoutMs}ms）`)
      )
    }
    effectiveSignal.addEventListener('abort', abort, { once: true })
    void operation.then(
      (value) => {
        effectiveSignal.removeEventListener('abort', abort)
        if (effectiveSignal.aborted) {
          try {
            void Promise.resolve(cleanupLateValue?.(value)).catch(
              () => undefined
            )
          } catch {
            // Cleanup is best-effort after the caller has already timed out.
          }
          abort()
        } else {
          resolve(value)
        }
      },
      (error: unknown) => {
        effectiveSignal.removeEventListener('abort', abort)
        reject(error)
      }
    )
  })
}

async function defaultCreatePartition(
  partition: string
): Promise<BrowserPartitionSession> {
  const electron = await import('electron')
  return electron.session.fromPartition(
    partition
  ) as unknown as BrowserPartitionSession
}

async function defaultCreateWindow(
  options: Record<string, unknown>
): Promise<BrowserWindowHandle> {
  const electron = await import('electron')
  return new electron.BrowserWindow(options) as unknown as BrowserWindowHandle
}

export class ElectronBrowserSession {
  readonly partition: string
  readonly webContents: BrowserWebContents
  private approvedOrigin?: string
  private readonly listeners: Listener[] = []
  private interaction?: {
    promise: Promise<BrowserScreenshot | undefined>
    resolve(frame?: BrowserScreenshot): void
  }
  private interactionClosing?: Promise<void>
  private disposed = false

  private constructor(
    private readonly policy: BrowserUrlPolicy,
    private readonly partitionSession: BrowserPartitionSession,
    private readonly window: BrowserWindowHandle,
    private readonly proxy: FilteringProxyLike,
    partition: string,
    private readonly cleanupTimeoutMs: number,
    private readonly parentWindow?: BrowserParentWindowHandle
  ) {
    this.partition = partition
    this.webContents = window.webContents
  }

  static async create(
    options: ElectronBrowserSessionOptions,
    signal: AbortSignal = new AbortController().signal
  ): Promise<ElectronBrowserSession> {
    const partition = `browser-${randomUUID()}`
    const createPartition = options.createPartition ?? defaultCreatePartition
    const createWindow = options.createWindow ?? defaultCreateWindow
    const cleanupTimeoutMs = options.cleanupTimeoutMs ?? 5_000
    const setupTimeoutMs = options.setupTimeoutMs ?? 15_000
    if (!Number.isSafeInteger(cleanupTimeoutMs) || cleanupTimeoutMs < 1) {
      throw new Error('浏览器会话清理期限无效')
    }
    if (!Number.isSafeInteger(setupTimeoutMs) || setupTimeoutMs < 1) {
      throw new Error('浏览器会话创建期限无效')
    }
    const proxy =
      options.createProxy?.(options.policy) ??
      new FilteringProxy({ policy: options.policy })
    let proxyDisposal: Promise<void> | undefined
    const managedProxy: FilteringProxyLike = {
      start: () => proxy.start(),
      dispose: () => {
        proxyDisposal ??= proxy.dispose()
        return proxyDisposal
      }
    }
    let partitionSession: BrowserPartitionSession | undefined
    let window: BrowserWindowHandle | undefined
    let result: ElectronBrowserSession | undefined
    let setupStage = '启动代理'
    try {
      const proxyUrl = await boundedSetup(
        managedProxy.start(),
        signal,
        setupTimeoutMs,
        async () => managedProxy.dispose()
      )
      setupStage = '创建隔离会话'
      partitionSession = await boundedSetup(
        createPartition(partition),
        signal,
        setupTimeoutMs,
        async (latePartition) =>
          cleanupIsolatedState(
            latePartition,
            managedProxy,
            cleanupTimeoutMs
          )
      )
      partitionSession.setPermissionCheckHandler(() => false)
      partitionSession.setPermissionRequestHandler(
        (_contents, _permission, callback) => callback(false)
      )
      partitionSession.setDisplayMediaRequestHandler(
        (_request, callback) => callback({})
      )
      partitionSession.setUserAgent?.(
        managedBrowserUserAgent(),
        'zh-CN,zh,en'
      )
      setupStage = '配置网络代理'
      await boundedSetup(
        partitionSession.setProxy({
          mode: 'fixed_servers',
          proxyRules: proxyUrl,
          proxyBypassRules: '<-loopback>'
        }),
        signal,
        setupTimeoutMs
      )
      setupStage = '创建浏览器窗口'
      window = await boundedSetup(
        createWindow({
          show: false,
          width: 1280,
          height: 900,
          title: 'GoodBuddy 浏览器交互',
          autoHideMenuBar: true,
          ...(options.parentWindow
            ? {
                parent: options.parentWindow
              }
            : {}),
          webPreferences: {
            partition,
            sandbox: true,
            contextIsolation: true,
            nodeIntegration: false,
            nodeIntegrationInSubFrames: false,
            nodeIntegrationInWorker: false,
            backgroundThrottling: false,
            webSecurity: true,
            allowRunningInsecureContent: false,
            plugins: false,
            devTools: false,
            safeDialogs: true
          }
        }),
        signal,
        setupTimeoutMs,
        (lateWindow) => {
          if (!lateWindow.isDestroyed()) {
            lateWindow.destroy()
          } else if (!lateWindow.webContents.isDestroyed()) {
            lateWindow.webContents.destroy()
          }
        }
      )
      setupStage = '加载初始页面'
      await boundedSetup(
        window.loadURL('about:blank'),
        signal,
        setupTimeoutMs
      )
      result = new ElectronBrowserSession(
        options.policy,
        partitionSession,
        window,
        managedProxy,
        partition,
        cleanupTimeoutMs,
        options.parentWindow
      )
      setupStage = '初始化浏览器协议'
      await boundedSetup(result.initialize(), signal, setupTimeoutMs)
      return result
    } catch (error) {
      if (result) {
        await result.dispose().catch(() => undefined)
      } else if (window && !window.isDestroyed()) {
        window.destroy()
        await cleanupIsolatedState(
          partitionSession,
          managedProxy,
          cleanupTimeoutMs
        ).catch(() => undefined)
      } else {
        await cleanupIsolatedState(
          partitionSession,
          managedProxy,
          cleanupTimeoutMs
        ).catch(() => undefined)
      }
      const detail =
        error instanceof Error && error.message
          ? error.message.slice(0, 160)
          : '未知错误'
      throw new Error(
        `无法创建安全浏览器会话：${setupStage}失败（${detail}）`,
        { cause: error }
      )
    }
  }

  private listen(
    target: Listener['target'] & {
      on(event: string, listener: BrowserEventListener): unknown
    },
    event: string,
    listener: BrowserEventListener
  ): void {
    target.on(event, listener)
    this.listeners.push({ target, event, listener })
  }

  private async initialize(): Promise<void> {
    const contents = this.webContents
    this.listen(
      this.window,
      'close',
      (event: { preventDefault(): void }) => {
        if (this.disposed) {
          return
        }
        event.preventDefault()
        if (this.interaction) {
          void this.captureAndFinishInteraction()
        } else {
          this.window.minimize()
        }
      }
    )
    contents.setWindowOpenHandler(() => ({ action: 'deny' }))
    this.listen(contents, 'will-navigate', (event: { preventDefault(): void }, details: { url?: string } | string) => {
      const url = typeof details === 'string' ? details : details.url
      if (!url || !this.updateOriginFromUrl(url)) {
        event.preventDefault()
      }
    })
    this.listen(contents, 'will-redirect', (event: { preventDefault(): void }, details: { url?: string } | string) => {
      const url = typeof details === 'string' ? details : details.url
      if (!url || !this.updateOriginFromUrl(url)) {
        event.preventDefault()
      }
    })
    this.listen(contents, 'login', (
      event: { preventDefault(): void },
      _details: unknown,
      _authInfo: unknown,
      callback: () => void
    ) => {
      event.preventDefault()
      callback()
    })
    this.listen(contents, 'select-client-certificate', (
      event: { preventDefault(): void },
      _url: string,
      _certificates: unknown[],
      callback: (certificate?: unknown) => void
    ) => {
      event.preventDefault()
      callback()
    })
    this.listen(contents, 'did-navigate', (_event: unknown, url: string) => {
      if (url && !this.updateOriginFromUrl(url)) {
        contents.stop()
      }
    })
    this.listen(
      this.partitionSession,
      'will-download',
      (event: { preventDefault(): void }, item: { cancel?(): void }) => {
        event.preventDefault()
        item.cancel?.()
      }
    )
    contents.debugger.attach('1.3')
    await contents.debugger.sendCommand('Page.enable')
    this.assertOpen()
    await contents.debugger.sendCommand('Accessibility.enable')
    this.assertOpen()
    await contents.debugger.sendCommand('Page.setInterceptFileChooserDialog', {
      enabled: true
    })
    this.assertOpen()
    this.listen(
      contents.debugger,
      'message',
      (_event: unknown, method: string) => {
        if (method === 'Page.fileChooserOpened') {
          void contents.debugger
            .sendCommand('Page.handleFileChooser', { action: 'cancel' })
            .catch(() => undefined)
        }
      }
    )
  }

  private assertOpen(): void {
    if (this.disposed) {
      throw new Error('浏览器会话已关闭')
    }
  }

  private updateOriginFromUrl(input: string): boolean {
    try {
      this.approvedOrigin = canonicalizeBrowserUrl(input).origin
      return true
    } catch {
      return false
    }
  }

  approveNavigation(target: ValidatedBrowserUrl): void {
    if (this.disposed) {
      throw new Error('浏览器会话已关闭')
    }
    this.approvedOrigin = target.origin
  }

  getApprovedOrigin(): string | undefined {
    return this.approvedOrigin
  }

  getCurrentOrigin(): string | undefined {
    const current = this.webContents.getURL()
    if (!current) {
      return undefined
    }
    try {
      return canonicalizeBrowserUrl(current).origin
    } catch {
      return undefined
    }
  }

  async captureScreenshot(
    signal: AbortSignal
  ): Promise<BrowserScreenshot> {
    this.assertOpen()
    if (!this.webContents.capturePage) {
      throw new Error('浏览器原生画面捕获不可用')
    }
    const image = await boundedSetup(
      this.webContents.capturePage(),
      signal,
      2_000
    )
    this.assertOpen()
    const data = encodeBoundedJpeg(image)
    return {
      type: 'image',
      mimeType: 'image/jpeg',
      data: data.toString('base64')
    }
  }

  async validateRedirect(url: string, signal: AbortSignal): Promise<void> {
    const target = await this.policy.validateRedirect(url, signal)
    this.approvedOrigin = target.origin
  }

  openInteraction(): Promise<BrowserScreenshot | undefined> {
    this.assertOpen()
    if (this.interaction) {
      this.setParentEnabled(false)
      if (this.window.isMinimized()) {
        this.window.restore()
      }
      this.window.show()
      this.window.focus()
      return this.interaction.promise
    }
    let resolve!: (frame?: BrowserScreenshot) => void
    const promise = new Promise<BrowserScreenshot | undefined>(
      (resolvePromise) => {
        resolve = resolvePromise
      }
    )
    this.interaction = { promise, resolve }
    this.setParentEnabled(false)
    try {
      if (this.window.isMinimized()) {
        this.window.restore()
      }
      this.window.show()
      this.window.focus()
    } catch (error) {
      this.finishInteraction()
      throw error
    }
    return promise
  }

  private captureAndFinishInteraction(): Promise<void> {
    if (this.interactionClosing) {
      return this.interactionClosing
    }
    const operation = (async (): Promise<void> => {
      let frame: BrowserScreenshot | undefined
      try {
        frame = await this.captureScreenshot(AbortSignal.timeout(2_000))
      } catch {
        // The session remains usable even if the final visible frame fails.
      }
      try {
        if (!this.disposed && !this.window.isDestroyed()) {
          this.window.minimize()
        }
      } catch {
        // Resolving interaction must not depend on native minimize success.
      }
      this.finishInteraction(frame)
    })()
    this.interactionClosing = operation
    void operation.finally(() => {
      if (this.interactionClosing === operation) {
        this.interactionClosing = undefined
      }
    })
    return operation
  }

  private finishInteraction(frame?: BrowserScreenshot): void {
    const interaction = this.interaction
    this.interaction = undefined
    this.setParentEnabled(true)
    interaction?.resolve(frame)
  }

  private setParentEnabled(enabled: boolean): void {
    try {
      if (
        !this.parentWindow ||
        this.parentWindow.isDestroyed?.() === true
      ) {
        return
      }
      this.parentWindow.setEnabled?.(enabled)
      if (enabled) {
        this.parentWindow.focus?.()
      }
    } catch {
      // Parent-window state must not break browser-session cleanup.
    }
  }

  async dispose(): Promise<void> {
    if (this.disposed) {
      return
    }
    this.disposed = true
    this.approvedOrigin = undefined
    this.finishInteraction()
    for (const { target, event, listener } of this.listeners.splice(0)) {
      target.off(event, listener)
    }
    if (this.webContents.debugger.isAttached()) {
      this.webContents.debugger.detach()
    }
    this.webContents.stop()
    if (!this.window.isDestroyed()) {
      this.window.destroy()
    } else if (!this.webContents.isDestroyed()) {
      this.webContents.destroy()
    }
    await cleanupIsolatedState(
      this.partitionSession,
      this.proxy,
      this.cleanupTimeoutMs
    )
  }
}
