import { Database, eq, and, sql, inArray } from "@opencode-ai/console-core/drizzle/index.js"
import { IpRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
import { FreeUsageLimitError } from "./error"
import { logger } from "./logger"
import { ZenData } from "@opencode-ai/console-core/model.js"

export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string, headers: Headers) {
  if (!limit) return

  const limitValue = limit.checkHeader && !headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value

  const ip = !rawIp.length ? "unknown" : rawIp
  const now = Date.now()
  const intervals =
    limit.period === "day"
      ? [buildYYYYMMDD(now)]
      : [buildYYYYMMDDHH(now), buildYYYYMMDDHH(now - 3_600_000), buildYYYYMMDDHH(now - 7_200_000)]

  return {
    track: async () => {
      await Database.use((tx) =>
        tx
          .insert(IpRateLimitTable)
          .values({ ip, interval: intervals[0], count: 1 })
          .onDuplicateKeyUpdate({ set: { count: sql`${IpRateLimitTable.count} + 1` } }),
      )
    },
    check: async () => {
      const rows = await Database.use((tx) =>
        tx
          .select({ interval: IpRateLimitTable.interval, count: IpRateLimitTable.count })
          .from(IpRateLimitTable)
          .where(and(eq(IpRateLimitTable.ip, ip), inArray(IpRateLimitTable.interval, intervals))),
      )
      const total = rows.reduce((sum, r) => sum + r.count, 0)
      logger.debug(`rate limit total: ${total}`)
      if (total >= limitValue)
        throw new FreeUsageLimitError(
          `Rate limit exceeded. Please try again later.`,
          limit.period === "day" ? getRetryAfterDay(now) : getRetryAfterHour(rows, intervals, limitValue, now),
        )
    },
  }
}

export function getRetryAfterDay(now: number) {
  return Math.ceil((86_400_000 - (now % 86_400_000)) / 1000)
}

export function getRetryAfterHour(
  rows: { interval: string; count: number }[],
  intervals: string[],
  limit: number,
  now: number,
) {
  const counts = new Map(rows.map((r) => [r.interval, r.count]))
  // intervals are ordered newest to oldest: [current, -1h, -2h]
  // simulate dropping oldest intervals one at a time
  let running = intervals.reduce((sum, i) => sum + (counts.get(i) ?? 0), 0)
  for (let i = intervals.length - 1; i >= 0; i--) {
    running -= counts.get(intervals[i]) ?? 0
    if (running < limit) {
      // interval at index i rolls out of the window (intervals.length - i) hours from the current hour start
      const hours = intervals.length - i
      return Math.ceil((hours * 3_600_000 - (now % 3_600_000)) / 1000)
    }
  }
  return Math.ceil((3_600_000 - (now % 3_600_000)) / 1000)
}

function buildYYYYMMDD(timestamp: number) {
  return new Date(timestamp)
    .toISOString()
    .replace(/[^0-9]/g, "")
    .substring(0, 8)
}

function buildYYYYMMDDHH(timestamp: number) {
  return new Date(timestamp)
    .toISOString()
    .replace(/[^0-9]/g, "")
    .substring(0, 10)
}
