/**
 * ClientErrorReporter - Captures client-side errors and reports them to a server endpoint.
 *
 * USAGE:
 * 1. Import and instantiate with your error collection endpoint
 * 2. Call start() to begin capturing errors:
 *    reporter.start()
 * 3. Call stop() when done (e.g., on page unload or component unmount):
 *    reporter.stop()
 *
 * ENDPOINT:
 * The default endpoint for this project is hosted at:
 *   https://scoreboard-tailuge.vercel.app/api/client-error
 *
 * CORS:
 * The endpoint must include appropriate CORS headers (Access-Control-Allow-Origin)
 * to allow cross-origin requests from client applications.
 *
 * CAPTURED SOURCES:
 * - console.error and console.warn calls
 * - console.log calls (only when captureAll is true, or on Apple devices)
 * - window.onerror (uncaught JavaScript errors)
 * - window.onunhandledrejection (unhandled Promise rejections)
 * @example
 * // Basic usage with default settings
 * const reporter = new ClientErrorReporter("https://scoreboard-tailuge.vercel.app/api/client-error")
 * reporter.start()
 *
 */

import { getUID } from "../../utils/uid"
import { VERSION } from "../../utils/version"

export interface ErrorReport {
  type: string
  message: string
  stack?: string
  url: string
  ts: number
  sid: string
  version: string
  origin: string
}

export class ClientErrorReporter {
  private readonly sid: string
  private queue: ErrorReport[] = []
  private readonly seen = new Map<string, number>()

  private readonly maxPerKey: number
  private readonly flushIntervalMs: number
  private readonly maxQueueSize: number
  public readonly captureAll: boolean

  private intervalId: ReturnType<typeof setInterval> | undefined
  private readonly boundFlush: () => void
  private originalConsoleError?: typeof console.error
  private originalConsoleWarn?: typeof console.warn
  private originalConsoleLog?: typeof console.log

  constructor(
    _endpoint: string,
    options?: {
      maxPerKey?: number
      flushIntervalMs?: number
      maxQueueSize?: number
      captureAll?: boolean
    }
  ) {
    this.sid = this.generateSid()
    this.maxPerKey = options?.maxPerKey ?? 3
    this.flushIntervalMs = options?.flushIntervalMs ?? 5000
    this.maxQueueSize = options?.maxQueueSize ?? 20
    this.captureAll = false

    this.boundFlush = this.flush.bind(this)
  }

  private generateSid(): string {
    return "B_" + getUID()
  }

  start() {
    if (!(globalThis.window !== undefined && process.env.NODE_ENV !== "test"))
      return

    this.patchConsole()
    this.patchGlobalErrors()

    this.intervalId = setInterval(this.boundFlush, this.flushIntervalMs)

    globalThis.addEventListener("pagehide", this.boundFlush)
  }

  stop() {
    this.flush(true)

    if (this.intervalId) {
      clearInterval(this.intervalId)
      this.intervalId = undefined
    }

    globalThis.removeEventListener("pagehide", this.boundFlush)

    if (this.originalConsoleError) {
      console.error = this.originalConsoleError
    }
    if (this.originalConsoleWarn) {
      console.warn = this.originalConsoleWarn
    }
    if (this.originalConsoleLog) {
      console.log = this.originalConsoleLog
    }
  }

  private patchConsole() {
    this.originalConsoleError = console.error
    this.originalConsoleWarn = console.warn

    console.error = (...args: unknown[]) => {
      this.capture("error", args)
      this.originalConsoleError!.apply(console, args)
    }

    console.warn = (...args: unknown[]) => {
      this.capture("warn", args)
      this.originalConsoleWarn!.apply(console, args)
    }

    if (this.captureAll) {
      this.originalConsoleLog = console.log
      console.log = (...args: unknown[]) => {
        this.capture("log", args)
        this.originalConsoleLog!.apply(console, args)
      }
    }
  }

  private patchGlobalErrors() {
    globalThis.addEventListener("error", (e) => {
      this.capture("uncaught", [e.error || e.message])
    })

    globalThis.addEventListener("unhandledrejection", (e) => {
      this.capture("promise", [e.reason])
    })
  }

  private capture(type: string, args: unknown[]) {
    try {
      let message = args.map(String).join(" ")
      let stack: string | undefined

      if (args[0] instanceof Error) {
        message = args[0].message
        stack = args[0].stack
      }

      if (message.includes("Backpack couldn't override `window.et")) {
        return
      }

      const key = type + ":" + message
      const count = (this.seen.get(key) ?? 0) + 1
      this.seen.set(key, count)

      if (count > this.maxPerKey) return

      this.queue.push({
        type,
        message,
        ...(stack ? { stack } : {}),
        url: globalThis.location?.href ?? "",
        ts: Date.now(),
        sid: this.sid,
        version: VERSION,
        origin: globalThis.location?.origin ?? "",
      })

      if (this.queue.length > this.maxQueueSize) this.flush()
    } catch {
      // do nothing
    }
  }

  private flush(_useBeacon = true) {
    try {
      if (!this.queue.length) return

      this.queue = []
    } catch {
      // do nothing
    }
  }
}
