/// <reference lib="dom" />

import fs from 'node:fs/promises'

type JsonObject = Record<string, any>

type BrowserResponseLike = {
  url: () => string
  request: () => { method: () => string }
  text: () => Promise<string>
  status: () => number
}

type BrowserLocatorLike = {
  screenshot: (options: { path: string }) => Promise<void>
  getAttribute: (name: string) => Promise<string | null>
  click: (options?: JsonObject) => Promise<void>
  evaluate: <T = unknown>(callback: (element: Element) => T | Promise<T>) => Promise<T>
}

type BrowserPageLike = {
  waitForFunction: (callback: any, argOrOptions?: any, options?: JsonObject) => Promise<unknown>
  waitForResponse: (
    predicate: (response: BrowserResponseLike) => boolean,
    options?: JsonObject,
  ) => Promise<BrowserResponseLike>
  waitForTimeout: (milliseconds: number) => Promise<void>
  locator: (selector: string) => BrowserLocatorLike
  evaluate: <T = any>(callback: any, arg?: any) => Promise<T>
}

type ActivityInfo = {
  role?: JsonObject & {
    ready?: boolean
    area?: string
  }
  form?: {
    cdkeyInputId?: string
    verifyInputId?: string
    verifyImgId?: string
    submitId?: string
  }
}

type TencentRedeemPopup = {
  visible?: boolean
  text?: string
  detail?: string
}

type TencentRedeemResult = JsonObject & {
  iRet?: number | string
  sMsg?: string
  msg?: string
  popup?: TencentRedeemPopup | null
}

type TencentRedeemOutcome = 'success' | 'captcha_rejected' | 'code_used' | 'code_invalid' | 'failed'

type TencentRedeemClassification = {
  outcome: TencentRedeemOutcome
  success: boolean
  retryWithSameCode: boolean
  retryWithReplacementCode: boolean
  retCode: number | null
  message: string
}

type RedeemAttemptRecord = {
  attempt: number
  verifyCode: string
  verifysession: string
  ocrSample: unknown
  redeem: TencentRedeemResult
  classification: TencentRedeemClassification
  role: ActivityInfo['role']
}

type TencentBrowserRedeemSession = JsonObject & {
  status: string
  notice: string
  updatedAt: string
  lastError?: string
  lastRedeem?: JsonObject
  page: BrowserPageLike
  browserContext: unknown
  sessionDir: string
  sessionId: string
}

type RunTencentBrowserRedeemOptions = {
  session: TencentBrowserRedeemSession
  code: string
  maxAttempts: number
  ensureLoggedInPresentation: (session: TencentBrowserRedeemSession, options?: JsonObject) => Promise<unknown>
  ensureActivityInfoReady: (page: BrowserPageLike, options?: JsonObject) => Promise<ActivityInfo>
  fillRedeemCodeInBrowser: (page: BrowserPageLike, code: string, options?: JsonObject) => Promise<unknown>
  capturePageCaptchaForOcr: (
    page: BrowserPageLike,
    options: { sessionDir: string; attempt: number },
  ) => Promise<{
    imageBuffer: Buffer
    imagePath: string
    imageExtension: string
    contentType: string
    verifyImgId: string
    verifysession: string
  }>
  recognizeTencentCaptcha: (payload: JsonObject) => Promise<JsonObject>
  submitRedeemInBrowser: (
    page: BrowserPageLike,
    payload: { code: string; verifyCode: string },
  ) => Promise<TencentRedeemResult>
  isCaptchaRejectedResult: (result: TencentRedeemResult) => boolean
  refreshPageCaptcha: (page: BrowserPageLike, verifyImgId?: string) => Promise<void>
  persistSessionState: (session: TencentBrowserRedeemSession) => Promise<unknown>
  buildSessionPayload: (session: TencentBrowserRedeemSession) => JsonObject
  saveRedeemArtifacts: (payload: JsonObject) => Promise<JsonObject>
  activityUrl: string
}

type ActivityInfoProvider = {
  ensureActivityInfoReady: (page: BrowserPageLike, options?: JsonObject) => Promise<ActivityInfo>
}

declare global {
  interface Window {
    Milo?: {
      get?: (key: string) => unknown
    }
    closeDialog?: () => void
  }
}

export async function runTencentBrowserRedeem({
  session,
  code,
  maxAttempts,
  ensureLoggedInPresentation,
  ensureActivityInfoReady,
  fillRedeemCodeInBrowser,
  capturePageCaptchaForOcr,
  recognizeTencentCaptcha,
  submitRedeemInBrowser,
  isCaptchaRejectedResult,
  refreshPageCaptcha,
  persistSessionState,
  buildSessionPayload,
  saveRedeemArtifacts,
  activityUrl,
}: RunTencentBrowserRedeemOptions): Promise<JsonObject> {
  session.status = 'redeeming'
  session.notice = '正在识别验证码并提交兑换'
  session.updatedAt = new Date().toISOString()
  await persistSessionState(session)

  const attempts: RedeemAttemptRecord[] = []
  let finalResult: RedeemAttemptRecord | null = null

  try {
    await ensureLoggedInPresentation(session, { credentialReady: true })
    const activityInfo = await ensureActivityInfoReady(session.page, { triggerRender: true })

    if (!activityInfo?.role?.ready) {
      throw new Error('浏览器会话尚未拿到角色信息，请稍后重试')
    }

    for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
      await fillRedeemCodeInBrowser(session.page, code)

      const captcha = await capturePageCaptchaForOcr(session.page, {
        sessionDir: session.sessionDir,
        attempt,
      })
      const ocr = await recognizeTencentCaptcha({
        imageBase64: captcha.imageBuffer.toString('base64'),
        imageExtension: captcha.imageExtension,
        imageContentType: captcha.contentType,
        finalUrl: captcha.imagePath,
        saveSample: true,
        tag: `browser-session-${session.sessionId}-attempt-${attempt}`,
      })

      if (ocr?.code !== 0) {
        throw new Error(ocr?.msg || 'OCR 识别失败')
      }

      const verifyCode = String(ocr?.data?.text || ocr?.data?.recognizedText || '').trim()

      if (!verifyCode) {
        throw new Error('OCR 没有识别出验证码')
      }

      const redeem = await submitRedeemInBrowser(session.page, {
        code,
        verifyCode,
      })
      const classification = classifyTencentRedeemResult(redeem)

      const attemptRecord = {
        attempt,
        verifyCode,
        verifysession: captcha.verifysession,
        ocrSample: ocr?.data?.saved || null,
        redeem,
        classification,
        role: activityInfo.role,
      }

      attempts.push(attemptRecord)
      finalResult = attemptRecord

      if (!isCaptchaRejectedResult(redeem)) {
        break
      }

      await dismissRedeemRetryPopup(session.page)
      await refreshPageCaptcha(session.page, captcha.verifyImgId)
    }

    if (!finalResult) {
      throw new Error('浏览器会话兑换没有拿到结果')
    }

    const artifacts = await saveRedeemArtifacts({
      browserContext: session.browserContext,
      page: session.page,
      sessionDir: session.sessionDir,
      sessionId: session.sessionId,
      activityUrl,
      code,
      finalResult,
      attempts,
    })

    session.lastRedeem = {
      code,
      area: finalResult?.role?.area || '',
      attempts,
      final: finalResult,
      proofMode: artifacts.proofMode,
      screenshotPath: artifacts.screenshotPath,
      htmlPath: artifacts.htmlPath,
      resultPath: artifacts.resultPath,
      finishedAt: new Date().toISOString(),
    }
    session.status = 'redeemed'
    session.notice = resolveTencentRedeemMessage(finalResult?.redeem)
    session.updatedAt = new Date().toISOString()
    await persistSessionState(session)

    return {
      ...buildSessionPayload(session),
      redeem: session.lastRedeem,
    }
  } catch (error) {
    session.status = 'failed'
    session.notice = error instanceof Error ? error.message : '浏览器会话兑换失败'
    session.lastError = session.notice
    session.updatedAt = new Date().toISOString()
    await persistSessionState(session)
    throw error
  }
}

export async function capturePageCaptchaForOcr(
  page: BrowserPageLike,
  { sessionDir, attempt, ensureActivityInfoReady }: { sessionDir: string; attempt: number } & ActivityInfoProvider,
) {
  const activityInfo = await ensureActivityInfoReady(page, { timeoutMs: 5_000 })

  if (!activityInfo?.form?.verifyImgId) {
    throw new Error('页面里没有找到验证码图片节点')
  }

  const verifySelector = `#${activityInfo.form.verifyImgId}`

  await page.waitForFunction(
    (selector) => {
      const element = document.querySelector(selector)

      if (!(element instanceof HTMLImageElement)) {
        return false
      }

      const style = window.getComputedStyle(element)
      return (
        style.display !== 'none' &&
        style.visibility !== 'hidden' &&
        style.opacity !== '0' &&
        element.naturalWidth > 0
      )
    },
    verifySelector,
    { timeout: 8_000 },
  )

  const imagePath = `${sessionDir}/captcha-attempt-${attempt}.png`
  await page.locator(verifySelector).screenshot({ path: imagePath })

  const verifysession = await page.evaluate(() => {
    try {
      if (window.Milo && typeof window.Milo.get === 'function') {
        return String(window.Milo.get('verifysession') || '')
      }
    } catch {
      // ignore Milo access failures
    }

    return ''
  })

  return {
    imageBuffer: await fs.readFile(imagePath),
    imagePath,
    imageExtension: '.png',
    contentType: 'image/png',
    verifyImgId: activityInfo.form.verifyImgId,
    verifysession,
  }
}

export async function submitRedeemInBrowser(
  page: BrowserPageLike,
  payload: { code: string; verifyCode: string },
  { ensureActivityInfoReady }: ActivityInfoProvider,
): Promise<TencentRedeemResult> {
  const activityInfo = await ensureActivityInfoReady(page, { timeoutMs: 2_000 })

  if (!activityInfo?.form?.verifyInputId || !activityInfo?.form?.submitId) {
    throw new Error('页面里没有找到兑换输入框或提交按钮')
  }

  const verifySelector = `#${activityInfo.form.verifyInputId}`
  const submitSelector = `#${activityInfo.form.submitId}`

  await setFormControlValue(page, verifySelector, payload.verifyCode)
  await resetRedeemPopup(page)
  await normalizeRedeemOverlay(page)

  const responsePromise = page
    .waitForResponse(
      (response) =>
        response.url().includes('dfm.ams.game.qq.com/ide/') &&
        response.request().method().toUpperCase() === 'POST',
      { timeout: 10_000 },
    )
    .catch(() => null)
  const popupPromise = waitForRedeemPopup(page).catch(() => null)

  await clickRedeemSubmit(page, submitSelector)

  const networkResponse = await responsePromise
  const popup = await popupPromise
  const parsedNetwork = networkResponse ? tryParseJson(await networkResponse.text()) : null

  if (parsedNetwork && typeof parsedNetwork === 'object') {
    return {
      httpStatus: networkResponse.status(),
      popup: popup || null,
      ...parsedNetwork,
    }
  }

  if (!popup) {
    throw new Error('页面内兑换没有等到结果弹窗或接口响应')
  }

  return {
    httpStatus: networkResponse?.status?.() || 0,
    iRet: inferRedeemCodeFromPopup(popup),
    sMsg: popup.text || popup.detail || '兑换完成',
    popup,
  }
}

export function fillRedeemCodeInBrowser(
  page: BrowserPageLike,
  code: string,
  { activityInfo }: { activityInfo?: ActivityInfo } = {},
) {
  const cdkeySelector = activityInfo?.form?.cdkeyInputId
    ? `#${activityInfo.form.cdkeyInputId}`
    : '[id^="milo_cdkeyInfo_"]'

  const verifySelector = activityInfo?.form?.verifyInputId
    ? `#${activityInfo.form.verifyInputId}`
    : '[id^="milo_verifyInput_"]'

  return Promise.all([
    setFormControlValue(page, cdkeySelector, code),
    setFormControlValue(page, verifySelector, ''),
  ])
}

export async function prepareRedeemCodeFill(
  page: BrowserPageLike,
  code: string,
  { ensureActivityInfoReady }: ActivityInfoProvider,
) {
  const activityInfo = await ensureActivityInfoReady(page, { triggerRender: true, timeoutMs: 2_000 })
  await fillRedeemCodeInBrowser(page, code, { activityInfo })
}

export async function refreshPageCaptcha(page: BrowserPageLike, verifyImgId?: string) {
  if (!verifyImgId) {
    return
  }

  const selector = `#${verifyImgId}`
  const currentSrc = await page.locator(selector).getAttribute('src').catch(() => '')
  await page.locator(selector).click({ timeout: 3_000 }).catch(() => null)
  await page.waitForFunction(
    ({ selector: targetSelector, previousSrc }) => {
      const element = document.querySelector(targetSelector)

      if (!(element instanceof HTMLImageElement)) {
        return false
      }

      return element.naturalWidth > 0 && String(element.getAttribute('src') || '') !== String(previousSrc || '')
    },
    { selector, previousSrc: currentSrc || '' },
    { timeout: 5_000 },
  ).catch(() => null)
}

export function isCaptchaRejectedResult(result: TencentRedeemResult): boolean {
  if (classifyTencentRedeemResult(result).outcome === 'captcha_rejected') {
    return true
  }

  return false
}

export function classifyTencentRedeemResult(result: TencentRedeemResult): TencentRedeemClassification {
  const retCode = Number(result?.iRet)
  const message = resolveTencentRedeemMessage(result)
  const normalized = normalizeTencentRedeemText(message)

  if (Number.isFinite(retCode) && retCode === -100) {
    return buildTencentRedeemClassification('captcha_rejected', retCode, message)
  }

  if (/兑换码已使用|cdk已使用|cdkey已使用|已被使用/.test(normalized)) {
    return buildTencentRedeemClassification('code_used', retCode, message || '兑换码已使用')
  }

  if (
    retCode === -165 ||
    retCode === -183 ||
    /兑换码错误|cdk错误|cdkey错误|请确认兑换码信息是否准确|兑换码信息是否准确|兑换码不存在|cdk不存在|cdkey不存在|不存在请您确认后输入/.test(normalized)
  ) {
    return buildTencentRedeemClassification('code_invalid', retCode, message || '兑换码错误，请确认兑换码信息是否准确')
  }

  if (/验证码|校验码/.test(normalized)) {
    return buildTencentRedeemClassification('captcha_rejected', retCode, message || '验证码错误，请稍后重试')
  }

  if (
    retCode === 0 ||
    /成功|已兑换|领取成功|恭喜您获得了礼包|查看邮件|到账/.test(normalized)
  ) {
    return buildTencentRedeemClassification('success', retCode, message || '兑换成功')
  }

  return buildTencentRedeemClassification('failed', retCode, message || '兑换失败')
}

function resetRedeemPopup(page: BrowserPageLike) {
  return page.evaluate(() => {
    const popup = document.querySelector('#pop2')
    const popupText = document.querySelector('#PopText')
    const popupDetail = document.querySelector('#PopText2')

    if (popup instanceof HTMLElement) {
      popup.style.display = 'none'
    }

    if (popupText instanceof HTMLElement) {
      popupText.textContent = ''
    }

    if (popupDetail instanceof HTMLElement) {
      popupDetail.classList.add('hide')
      popupDetail.textContent = ''
    }
  })
}

async function dismissRedeemRetryPopup(page: BrowserPageLike) {
  await page.evaluate(() => {
    try {
      if (typeof window.closeDialog === 'function') {
        window.closeDialog()
      }
    } catch {
      // ignore page close hook failures
    }

    const closeButton = document.querySelector('#pop2 .pop_close')

    if (closeButton instanceof HTMLElement) {
      closeButton.click()
    }

    const popup = document.querySelector('#pop2')

    if (popup instanceof HTMLElement) {
      popup.style.display = 'none'
      popup.style.visibility = 'hidden'
    }
  })

  await normalizeRedeemOverlay(page)
  await page.waitForTimeout(150)
}

async function normalizeRedeemOverlay(page: BrowserPageLike) {
  await page.evaluate(() => {
    const overlayIds = ['_overlay_', 'overlay_mask', 'overlay']

    for (const id of overlayIds) {
      const element = document.getElementById(id)

      if (!(element instanceof HTMLElement)) {
        continue
      }

      element.style.pointerEvents = 'none'
      element.style.display = 'none'
      element.style.opacity = '0'
    }
  })
}

async function clickRedeemSubmit(page: BrowserPageLike, submitSelector: string) {
  const locator = page.locator(submitSelector)

  try {
    await locator.click({ timeout: 3_000 })
    return
  } catch {
    await normalizeRedeemOverlay(page)
  }

  try {
    await locator.click({ timeout: 3_000, force: true })
    return
  } catch {
    await locator.evaluate((element) => {
      if (element instanceof HTMLElement) {
        element.click()
      }
    })
  }
}

async function setFormControlValue(page: BrowserPageLike, selector: string, value: unknown) {
  const nextValue = String(value ?? '')

  const appliedValue = await page.evaluate(
    ({ targetSelector, targetValue }) => {
      const isVisible = (element: Element) => {
        if (!(element instanceof HTMLElement)) {
          return false
        }

        const style = window.getComputedStyle(element)
        return (
          style.display !== 'none' &&
          style.visibility !== 'hidden' &&
          style.opacity !== '0' &&
          element.getClientRects().length > 0
        )
      }

      const matches = [...document.querySelectorAll(targetSelector)]
      const element =
        matches.find(
          (node) =>
            (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) &&
            !node.disabled &&
            node.type !== 'hidden' &&
            isVisible(node),
        ) ||
        matches.find(
          (node) =>
            (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) &&
            !node.disabled &&
            node.type !== 'hidden',
        ) ||
        null

      if (!(element instanceof HTMLInputElement) && !(element instanceof HTMLTextAreaElement)) {
        return {
          found: false,
          value: '',
        }
      }

      const prototype =
        element instanceof HTMLTextAreaElement
          ? window.HTMLTextAreaElement.prototype
          : window.HTMLInputElement.prototype
      const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value')

      if (descriptor?.set) {
        descriptor.set.call(element, targetValue)
      } else {
        element.value = targetValue
      }

      element.setAttribute('value', targetValue)
      element.focus()
      element.dispatchEvent(new Event('input', { bubbles: true }))
      element.dispatchEvent(new Event('change', { bubbles: true }))
      element.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: 'Enter' }))
      element.blur()

      return {
        found: true,
        value: String(element.value || ''),
      }
    },
    { targetSelector: selector, targetValue: nextValue },
  )

  if (!appliedValue?.found) {
    throw new Error(`页面里没有找到可填写的表单节点: ${selector}`)
  }

  if (String(appliedValue.value || '') !== nextValue) {
    throw new Error(`页面表单写值失败: ${selector}`)
  }
}

async function waitForRedeemPopup(page: BrowserPageLike): Promise<TencentRedeemPopup> {
  await page.waitForFunction(() => {
    const popup = document.querySelector('#pop2')
    const popupText = document.querySelector('#PopText')

    if (!(popup instanceof HTMLElement) || !(popupText instanceof HTMLElement)) {
      return false
    }

    const style = window.getComputedStyle(popup)
    return style.display !== 'none' && String(popupText.textContent || '').trim().length > 0
  }, { timeout: 10_000 })

  return page.evaluate(() => ({
    visible: true,
    text: String(document.querySelector('#PopText')?.textContent || '').trim(),
    detail: String(document.querySelector('#PopText2')?.textContent || '').trim(),
  }))
}

function tryParseJson(text: string): any {
  try {
    return JSON.parse(text)
  } catch {
    return null
  }
}

function inferRedeemCodeFromPopup(popup: TencentRedeemPopup) {
  const text = `${String(popup?.text || '')} ${String(popup?.detail || '')}`.trim()

  if (!text) {
    return 1
  }

  if (/验证码|校验码/.test(text)) {
    return -100
  }

  if (/成功|已兑换|领取成功/.test(text)) {
    return 0
  }

  return 1
}

export function resolveTencentRedeemMessage(result: TencentRedeemResult): string {
  const candidates = [
    result?.popup?.detail,
    result?.popup?.text,
    result?.sMsg,
    result?.msg,
  ]
    .map((item) => String(item || '').trim())
    .filter(Boolean)

  return candidates[0] || '兑换完成'
}

function buildTencentRedeemClassification(
  outcome: TencentRedeemOutcome,
  retCode: number,
  message: string,
): TencentRedeemClassification {
  return {
    outcome,
    success: outcome === 'success',
    retryWithSameCode: outcome === 'captcha_rejected',
    retryWithReplacementCode: outcome === 'code_used' || outcome === 'code_invalid',
    retCode: Number.isFinite(retCode) ? retCode : null,
    message: String(message || '').trim(),
  }
}

function normalizeTencentRedeemText(value: unknown): string {
  return String(value || '')
    .replace(/\s+/g, '')
    .trim()
    .toLowerCase()
}
