/**
 * Card API error handling for Feishu.
 *
 * Provides error detection for rate limits (230020) and
 * table limit errors (230099/11310), plus text sanitization
 * to prevent card rendering failures.
 *
 * Adapted from openclaw-lark's card/card-error.ts.
 */

export const CARD_ERROR = {
  RATE_LIMITED: 230020,
  CARD_CONTENT_FAILED: 230099,
} as const

export const CARD_CONTENT_SUB_ERROR = {
  ELEMENT_LIMIT: 11310,
} as const

export const FEISHU_CARD_TABLE_LIMIT = 3

export interface CardKitResponse {
  code?: number
  msg?: string
  data?: Record<string, unknown>
  [key: string]: unknown
}

export class CardKitApiError extends Error {
  readonly code: number
  readonly msg: string

  constructor(params: { api: string; code: number; msg: string; context: string }) {
    const { api, code, msg, context } = params
    super(`cardkit ${api} FAILED: code=${code}, msg=${msg}, ${context}`)
    this.name = 'CardKitApiError'
    this.code = code
    this.msg = msg
  }
}

export function extractSubCode(msg: string): number | null {
  const match = /ErrCode:\s*(\d+)/.exec(msg)
  if (!match) return null
  const code = Number(match[1])
  return Number.isFinite(code) ? code : null
}

export function parseCardApiError(err: unknown): { code: number; subCode: number | null; errMsg: string } | null {
  const code = extractLarkApiCode(err)
  if (code === undefined) return null

  let errMsg = ''
  if (err && typeof err === 'object') {
    const e = err as {
      msg?: unknown
      message?: unknown
      response?: { data?: { msg?: unknown } }
    }
    if (typeof e.msg === 'string') {
      errMsg = e.msg
    } else if (typeof e.response?.data?.msg === 'string') {
      errMsg = e.response.data.msg
    } else if (typeof e.message === 'string') {
      errMsg = e.message
    }
  }

  const subCode = extractSubCode(errMsg)
  return { code, subCode, errMsg }
}

export function isCardTableLimitError(err: unknown): boolean {
  const parsed = parseCardApiError(err)
  if (!parsed) return false
  return (
    parsed.code === CARD_ERROR.CARD_CONTENT_FAILED &&
    parsed.subCode === CARD_CONTENT_SUB_ERROR.ELEMENT_LIMIT &&
    /table number over limit/i.test(parsed.errMsg)
  )
}

export function isCardRateLimitError(err: unknown): boolean {
  const parsed = parseCardApiError(err)
  if (!parsed) return false
  return parsed.code === CARD_ERROR.RATE_LIMITED
}

function extractLarkApiCode(err: unknown): number | undefined {
  if (!err || typeof err !== 'object') return undefined
  const e = err as { code?: number; status?: number; response?: { status?: number } }
  if (typeof e.code === 'number') return e.code
  if (typeof e.status === 'number') return e.status
  if (typeof e.response?.status === 'number') return e.response.status
  return undefined
}

export interface MarkdownTableMatch {
  index: number
  length: number
  raw: string
}

export function findMarkdownTablesOutsideCodeBlocks(text: string): MarkdownTableMatch[] {
  const codeBlockRanges: Array<{ start: number; end: number }> = []
  const codeBlockRegex = /```[\s\S]*?```/g
  let codeBlockMatch = codeBlockRegex.exec(text)
  while (codeBlockMatch != null) {
    codeBlockRanges.push({
      start: codeBlockMatch.index,
      end: codeBlockMatch.index + codeBlockMatch[0].length,
    })
    codeBlockMatch = codeBlockRegex.exec(text)
  }

  const isInsideCodeBlock = (idx: number): boolean =>
    codeBlockRanges.some((range) => idx >= range.start && idx < range.end)

  const tableRegex = /\|.+\|[\r\n]+\|[-:| ]+\|[\s\S]*?(?=\n\n|\n(?!\|)|$)/g
  const matches: MarkdownTableMatch[] = []
  let tableMatch = tableRegex.exec(text)
  while (tableMatch != null) {
    if (!isInsideCodeBlock(tableMatch.index)) {
      matches.push({
        index: tableMatch.index,
        length: tableMatch[0].length,
        raw: tableMatch[0],
      })
    }
    tableMatch = tableRegex.exec(text)
  }

  return matches
}

export function sanitizeTextSegmentsForCard(
  texts: readonly string[],
  tableLimit: number = FEISHU_CARD_TABLE_LIMIT,
): string[] {
  let remainingTableBudget = tableLimit

  return texts.map((text) => {
    const matches = findMarkdownTablesOutsideCodeBlocks(text)
    if (matches.length <= remainingTableBudget) {
      remainingTableBudget -= matches.length
      return text
    }

    const sanitizedText = wrapTablesBeyondLimit(text, matches, Math.max(remainingTableBudget, 0))
    remainingTableBudget = 0
    return sanitizedText
  })
}

export function sanitizeTextForCard(text: string, tableLimit: number = FEISHU_CARD_TABLE_LIMIT): string {
  return sanitizeTextSegmentsForCard([text], tableLimit)[0]
}

function wrapTablesBeyondLimit(
  text: string,
  matches: readonly MarkdownTableMatch[],
  keepCount: number,
): string {
  if (matches.length <= keepCount) return text

  let result = text
  for (let i = matches.length - 1; i >= keepCount; i--) {
    const { index, length, raw } = matches[i]
    const replacement = `\`\`\`\n${raw}\n\`\`\``
    result = result.slice(0, index) + replacement + result.slice(index + length)
  }

  return result
}