import { createHash, randomUUID, timingSafeEqual, createHmac, randomFillSync } from 'node:crypto'
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { parseCookies, cookieSecureFlag } from './helpers.js'
import { DATA_DIR } from './paths.js'

const TOTP_SECRET_FILE = path.join(DATA_DIR, 'totp-secret.json')

// Pending sessions: password verified, waiting for TOTP code
// Map<pendingToken, { expiresAt }>
const TOTP_PENDING_TTL_MS = 5 * 60 * 1000
const totpPendingSessions = new Map()

const ADMIN_SESSION_COOKIE = 'vbn_admin_session'
const ADMIN_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000
const ADMIN_SESSION_ABSOLUTE_TTL_MS = 30 * 24 * 60 * 60 * 1000
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD
const adminSessions = new Map()

function cookieFlags() {
  return cookieSecureFlag()
}

export function sha256(value) {
  return createHash('sha256').update(String(value)).digest('hex')
}

export function isAdminPasswordConfigured() {
  return Boolean(ADMIN_PASSWORD)
}

export function validateAdminPasswordSetup() {
  if (!isAdminPasswordConfigured() && process.env.NODE_ENV === 'production') {
    throw new Error('ADMIN_PASSWORD is required in production.')
  }
  if (!isAdminPasswordConfigured()) {
    console.warn('ADMIN_PASSWORD is not configured; admin routes will remain disabled until the environment is configured.')
  }
}

export function isAdminPasswordValid(password) {
  if (!isAdminPasswordConfigured()) return false
  const a = Buffer.from(sha256(password), 'utf8')
  const b = Buffer.from(sha256(ADMIN_PASSWORD), 'utf8')
  if (a.length !== b.length) return false
  return timingSafeEqual(a, b)
}

// ── TOTP (RFC 6238) — implemented with Node built-in crypto ─────────────────

const BASE32_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'

function base32Decode(str) {
  const s = str.toUpperCase().replace(/=+$/, '')
  let bits = 0
  let value = 0
  const output = []
  for (const char of s) {
    const idx = BASE32_CHARS.indexOf(char)
    if (idx === -1) continue
    value = (value << 5) | idx
    bits += 5
    if (bits >= 8) {
      output.push((value >>> (bits - 8)) & 0xff)
      bits -= 8
    }
  }
  return Buffer.from(output)
}

function base32Encode(buf) {
  let bits = 0
  let value = 0
  let output = ''
  for (const byte of buf) {
    value = (value << 8) | byte
    bits += 8
    while (bits >= 5) {
      output += BASE32_CHARS[(value >>> (bits - 5)) & 0x1f]
      bits -= 5
    }
  }
  if (bits > 0) output += BASE32_CHARS[(value << (5 - bits)) & 0x1f]
  return output
}

function totpToken(secret, counter) {
  const key = base32Decode(secret)
  const msg = Buffer.alloc(8)
  // Write 64-bit big-endian counter
  const hi = Math.floor(counter / 0x100000000)
  const lo = counter >>> 0
  msg.writeUInt32BE(hi, 0)
  msg.writeUInt32BE(lo, 4)
  const hmac = createHmac('sha1', key).update(msg).digest()
  const offset = hmac[hmac.length - 1] & 0x0f
  const code = ((hmac[offset] & 0x7f) << 24)
    | (hmac[offset + 1] << 16)
    | (hmac[offset + 2] << 8)
    | hmac[offset + 3]
  return String(code % 1000000).padStart(6, '0')
}

export function generateTotpSecret() {
  const buf = Buffer.allocUnsafe(20)
  randomFillSync(buf)
  return base32Encode(buf)
}

export async function loadTotpState() {
  try {
    const raw = await readFile(TOTP_SECRET_FILE, 'utf8')
    return JSON.parse(raw)
  } catch {
    return null
  }
}

export async function saveTotpState(state) {
  await writeFile(TOTP_SECRET_FILE, JSON.stringify(state, null, 2), 'utf8')
}

export async function isTotpEnabled() {
  const state = await loadTotpState()
  return Boolean(state?.secret && state?.verified)
}

function randomBytesForRecovery(n) {
  const buf = Buffer.allocUnsafe(n)
  randomFillSync(buf)
  return buf
}

export function getTotpUri(secret, label = 'Siteforge Admin') {
  const issuer = 'Siteforge'
  return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(label)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`
}

export function verifyTotpCode(secret, code) {
  try {
    const token = String(code).replace(/\s/g, '')
    const step = Math.floor(Date.now() / 1000 / 30)
    // Accept current step and one step either side (±30 seconds clock skew)
    for (const offset of [-1, 0, 1]) {
      if (totpToken(secret, step + offset) === token) return true
    }
    return false
  } catch {
    return false
  }
}

// ── Recovery Codes ──────────────────────────────────────────────────────────

const RECOVERY_CODE_COUNT = 8

function generateRecoveryCode() {
  // Format: XXXX-XXXX-XXXX (uppercase alphanumeric, no ambiguous chars)
  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
  const randBytes = randomBytesForRecovery(12)
  let code = ''
  for (let i = 0; i < 12; i++) {
    if (i > 0 && i % 4 === 0) code += '-'
    code += chars[randBytes[i] % chars.length]
  }
  return code
}

export function generateRecoveryCodes() {
  const codes = []
  for (let i = 0; i < RECOVERY_CODE_COUNT; i++) {
    codes.push(generateRecoveryCode())
  }
  return codes
}

export function hashRecoveryCode(code) {
  return sha256(code.replace(/-/g, '').toUpperCase())
}

// Returns the matched code if valid, null otherwise. Mutates state.hashedRecoveryCodes.
export function consumeRecoveryCode(state, inputCode) {
  if (!Array.isArray(state.hashedRecoveryCodes) || state.hashedRecoveryCodes.length === 0) return false
  const normalized = inputCode.replace(/[-\s]/g, '').toUpperCase()
  const inputHash = sha256(normalized)
  const idx = state.hashedRecoveryCodes.findIndex(h => {
    const a = Buffer.from(h, 'utf8')
    const b = Buffer.from(inputHash, 'utf8')
    return a.length === b.length && timingSafeEqual(a, b)
  })
  if (idx === -1) return false
  state.hashedRecoveryCodes.splice(idx, 1)
  return true
}

// ── Pending (password-ok, awaiting TOTP) sessions ───────────────────────────

export function createPendingSession() {
  const token = randomUUID()
  totpPendingSessions.set(token, { expiresAt: Date.now() + TOTP_PENDING_TTL_MS })
  return token
}

export function consumePendingSession(token) {
  if (!token) return false
  const entry = totpPendingSessions.get(token)
  if (!entry || entry.expiresAt <= Date.now()) {
    totpPendingSessions.delete(token)
    return false
  }
  totpPendingSessions.delete(token)
  return true
}

// ── Admin Sessions ───────────────────────────────────────────────────────────

export function createAdminSession() {
  const token = randomUUID()
  const now = Date.now()
  adminSessions.set(token, { expiresAt: now + ADMIN_SESSION_TTL_MS, absoluteExpiresAt: now + ADMIN_SESSION_ABSOLUTE_TTL_MS })
  return token
}

export function deleteAdminSession(token) {
  if (token) {
    adminSessions.delete(token)
  }
}

export function isValidAdminSession(req) {
  if (!isAdminPasswordConfigured()) return false

  const cookies = parseCookies(req.headers.cookie)
  const sessionToken = cookies[ADMIN_SESSION_COOKIE]
  if (!sessionToken) return false

  const now = Date.now()
  const session = adminSessions.get(sessionToken)
  if (!session) return false

  // Support legacy sessions stored as a plain number (expiresAt)
  const expiresAt = typeof session === 'object' ? session.expiresAt : session
  const absoluteExpiresAt = typeof session === 'object' ? session.absoluteExpiresAt : Infinity

  if (expiresAt <= now || absoluteExpiresAt <= now) {
    adminSessions.delete(sessionToken)
    return false
  }

  adminSessions.set(sessionToken, { expiresAt: now + ADMIN_SESSION_TTL_MS, absoluteExpiresAt })
  return true
}

export function setAdminSessionCookie(res, token) {
  res.append(
    'Set-Cookie',
    `${ADMIN_SESSION_COOKIE}=${encodeURIComponent(token)}; Max-Age=${Math.floor(ADMIN_SESSION_TTL_MS / 1000)}; Path=/; HttpOnly; SameSite=Lax${cookieFlags()}`,
  )
}

export function clearAdminSessionCookie(res) {
  res.append(
    'Set-Cookie',
    `${ADMIN_SESSION_COOKIE}=; Max-Age=0; Path=/; HttpOnly; SameSite=Lax${cookieFlags()}`,
  )
}

export function requireAdminAuth(req, res, next) {
  if (!isValidAdminSession(req)) {
    res.status(401).json({ message: 'Unauthorized' })
    return
  }
  next()
}
