import { randomUUID } from 'crypto'
import { NextRequest, NextResponse } from 'next/server'
import { getRedisClient, isRedisAvailable, setRedisAvailable } from '../redis'
import { requireCronAuth } from './cron-auth'
import { recordSecurityEvent } from './security-observability'

interface CronExecutionOptions {
  endpoint: string
  lockTtlMs?: number
  idempotencyTtlMs?: number
}

const DEFAULT_LOCK_TTL_MS = parseInt(process.env.CRON_LOCK_TTL_MS || '600000', 10)
const DEFAULT_IDEMPOTENCY_TTL_MS = parseInt(process.env.CRON_IDEMPOTENCY_TTL_MS || '900000', 10)

const memoryLocks = new Map<string, { token: string; expiresAt: number }>()
const memoryIdempotency = new Map<string, number>()

function getLockTtlMs(): number {
  return Number.isFinite(DEFAULT_LOCK_TTL_MS) && DEFAULT_LOCK_TTL_MS > 0
    ? DEFAULT_LOCK_TTL_MS
    : 600000
}

function getIdempotencyTtlMs(): number {
  return Number.isFinite(DEFAULT_IDEMPOTENCY_TTL_MS) && DEFAULT_IDEMPOTENCY_TTL_MS > 0
    ? DEFAULT_IDEMPOTENCY_TTL_MS
    : 900000
}

function cleanupMemoryStores(now: number): void {
  for (const [key, entry] of memoryLocks.entries()) {
    if (entry.expiresAt <= now) {
      memoryLocks.delete(key)
    }
  }

  for (const [key, expiresAt] of memoryIdempotency.entries()) {
    if (expiresAt <= now) {
      memoryIdempotency.delete(key)
    }
  }
}

async function acquireExecutionLock(lockKey: string, token: string, ttlMs: number): Promise<boolean> {
  const now = Date.now()
  cleanupMemoryStores(now)

  if (isRedisAvailable()) {
    try {
      const redis = getRedisClient()
      if (redis) {
        const result = await redis.set(lockKey, token, 'PX', ttlMs, 'NX')
        return result === 'OK'
      }
    } catch {
      setRedisAvailable(false)
    }
  }

  const existing = memoryLocks.get(lockKey)
  if (existing && existing.expiresAt > now) {
    return false
  }

  memoryLocks.set(lockKey, {
    token,
    expiresAt: now + ttlMs,
  })
  return true
}

async function releaseExecutionLock(lockKey: string, token: string): Promise<void> {
  if (isRedisAvailable()) {
    try {
      const redis = getRedisClient()
      if (redis) {
        const current = await redis.get(lockKey)
        if (current === token) {
          await redis.del(lockKey)
        }
      }
    } catch {
      setRedisAvailable(false)
    }
  }

  const inMemory = memoryLocks.get(lockKey)
  if (inMemory?.token === token) {
    memoryLocks.delete(lockKey)
  }
}

async function markIdempotencyKey(idempotencyKey: string, ttlMs: number): Promise<boolean> {
  const now = Date.now()
  cleanupMemoryStores(now)

  if (isRedisAvailable()) {
    try {
      const redis = getRedisClient()
      if (redis) {
        const result = await redis.set(idempotencyKey, String(now), 'PX', ttlMs, 'NX')
        return result === 'OK'
      }
    } catch {
      setRedisAvailable(false)
    }
  }

  const existing = memoryIdempotency.get(idempotencyKey)
  if (existing && existing > now) {
    return false
  }
  memoryIdempotency.set(idempotencyKey, now + ttlMs)
  return true
}

function readIdempotencyKey(request: NextRequest): string | null {
  const headerValue = request.headers.get('x-idempotency-key')?.trim()
  if (headerValue) return headerValue

  const queryValue = request.nextUrl.searchParams.get('idempotencyKey')?.trim()
  if (queryValue) return queryValue

  return null
}

export async function withCronExecution(
  request: NextRequest,
  options: CronExecutionOptions,
  handler: () => Promise<NextResponse>,
): Promise<NextResponse> {
  const authError = requireCronAuth(request, options.endpoint)
  if (authError) {
    return authError
  }

  const idempotencyTtlMs = options.idempotencyTtlMs ?? getIdempotencyTtlMs()
  const idempotencyValue = readIdempotencyKey(request)
  if (idempotencyValue) {
    const idempotencyKey = `cron:idempotency:${options.endpoint}:${idempotencyValue}`
    const marked = await markIdempotencyKey(idempotencyKey, idempotencyTtlMs)
    if (!marked) {
      recordSecurityEvent('cron_duplicate_ignored', {
        endpoint: options.endpoint,
        idempotencyKey: idempotencyValue,
      }, 'info')
      return NextResponse.json(
        {
          success: true,
          duplicate: true,
          message: 'Duplicate cron request ignored',
        },
        { status: 202 },
      )
    }
  }

  const lockKey = `cron:lock:${options.endpoint}`
  const lockToken = randomUUID()
  const lockTtlMs = options.lockTtlMs ?? getLockTtlMs()
  const acquired = await acquireExecutionLock(lockKey, lockToken, lockTtlMs)
  if (!acquired) {
    recordSecurityEvent('cron_execution_locked', {
      endpoint: options.endpoint,
      reason: 'Another worker still holds the execution lock',
    })
    return NextResponse.json(
      { error: 'Cron job already running' },
      { status: 423 },
    )
  }

  try {
    return await handler()
  } finally {
    await releaseExecutionLock(lockKey, lockToken)
  }
}

export function resetCronCoordinationStateForTests(): void {
  memoryLocks.clear()
  memoryIdempotency.clear()
}

