/**
 * Stage 2FA TOTP: enroll → verify → login с 2FA → disable.
 * TOTP-коды генерируем на ходу из sharedKey (RFC 6238, Base32).
 */
import { createHmac } from 'node:crypto'
import { login, makeClient, ADMIN_BASE } from '../lib/api.js'
import type { CheckResult, Step, Report } from '../lib/report.js'

type Ctx = {
  apiOnly: boolean
  ts: number
  email?: string
  password?: string
  token?: string
  sharedKey?: string
}
interface StepCtx { ctx: Ctx; step: Step; report: Report }

const TS = Date.now()
function check(step: Step, c: CheckResult) { step.checks.push(c) }
function asString(x: unknown): string {
  if (x == null) return ''
  if (typeof x === 'string') return x
  return JSON.stringify(x)
}

/** Base32 (RFC 4648) decode → Buffer. ASP.NET выдаёт ключ без '=' padding и в upper-case. */
function base32Decode(raw: string): Buffer {
  const s = raw.replace(/=+$/, '').toUpperCase()
  const alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
  let bits = 0, value = 0
  const out: number[] = []
  for (const ch of s) {
    const idx = alpha.indexOf(ch)
    if (idx < 0) continue // пропускаем не-Base32 символы (пробелы)
    value = (value << 5) | idx
    bits += 5
    if (bits >= 8) {
      bits -= 8
      out.push((value >> bits) & 0xff)
    }
  }
  return Buffer.from(out)
}

/** TOTP code по RFC 6238 (period=30s, digits=6, SHA-1). */
function totp(secretBase32: string, atSeconds = Math.floor(Date.now() / 1000)): string {
  const key = base32Decode(secretBase32)
  const counter = Math.floor(atSeconds / 30)
  const cb = Buffer.alloc(8)
  cb.writeBigUInt64BE(BigInt(counter))
  const hmac = createHmac('sha1', key).update(cb).digest()
  const offset = hmac[hmac.length - 1] & 0x0f
  const bin =
    ((hmac[offset] & 0x7f) << 24) |
    ((hmac[offset + 1] & 0xff) << 16) |
    ((hmac[offset + 2] & 0xff) << 8) |
    (hmac[offset + 3] & 0xff)
  return String(bin % 1_000_000).padStart(6, '0')
}

// ---------------------------------------------------------------------------

async function bootstrap(ctx: Ctx): Promise<void> {
  if (ctx.token) return
  const api = makeClient()
  ctx.email = `stage-2fa-${TS}@food-market.local`
  ctx.password = 'Stage2fa12345!'
  let r = await api.post('/api/auth/signup', {
    email: ctx.email, password: ctx.password,
    organizationName: `TwoFA ${TS}`, phone: '+77011112000', plan: 'start',
  })
  for (let i = 0; i < 5 && r.status === 429; i++) {
    await new Promise(res => setTimeout(res, 15000))
    r = await api.post('/api/auth/signup', {
      email: ctx.email, password: ctx.password,
      organizationName: `TwoFA ${TS}`, phone: '+77011112000', plan: 'start',
    })
  }
  if (r.status !== 200) throw new Error(`signup: ${r.status} ${JSON.stringify(r.data)}`)
  const sess = await login(ctx.email, ctx.password)
  ctx.token = sess.accessToken
}

// ---------------------------------------------------------------------------

export async function tfa01_setup_and_status({ ctx, step, report }: StepCtx) {
  ctx.ts = TS
  await bootstrap(ctx)
  const api = makeClient(ctx.token)
  const status = await api.get('/api/me/2fa/status')
  check(step, { kind: 'api', description: 'GET /status → 200', ok: status.status === 200, detail: `${status.status}` })
  check(step, { kind: 'api', description: 'enabled = false для нового user', ok: status.data?.enabled === false, detail: `enabled=${status.data?.enabled}` })
}

// ---------------------------------------------------------------------------

export async function tfa02_enroll({ ctx, step, report }: StepCtx) {
  if (!ctx.token) { step.status = 'skip'; return }
  const api = makeClient(ctx.token)
  const r = await api.post('/api/me/2fa/enroll', {})
  check(step, { kind: 'api', description: 'POST /enroll → 200', ok: r.status === 200, detail: `${r.status}` })
  check(step, { kind: 'api', description: 'sharedKey не пустой', ok: !!r.data?.sharedKey && r.data.sharedKey.length >= 16, detail: `key.len=${r.data?.sharedKey?.length}` })
  check(step, { kind: 'api', description: 'authenticatorUri начинается с otpauth://totp/', ok: String(r.data?.authenticatorUri ?? '').startsWith('otpauth://totp/'), detail: `${String(r.data?.authenticatorUri ?? '').slice(0, 60)}…` })
  check(step, { kind: 'api', description: 'alreadyEnabled = false', ok: r.data?.alreadyEnabled === false, detail: `already=${r.data?.alreadyEnabled}` })
  ctx.sharedKey = r.data?.sharedKey
}

// ---------------------------------------------------------------------------

export async function tfa03_verify_invalid_code({ ctx, step, report }: StepCtx) {
  if (!ctx.token) { step.status = 'skip'; return }
  const api = makeClient(ctx.token)
  const r = await api.post('/api/me/2fa/verify', { code: '000000' })
  check(step, {
    kind: 'api', description: 'verify с 000000 → 400 "Неверный код"',
    ok: r.status === 400 && /неверн/i.test(asString(r.data)),
    detail: `${r.status} ${asString(r.data).slice(0, 200)}`,
  })
}

// ---------------------------------------------------------------------------

export async function tfa04_verify_valid_code({ ctx, step, report }: StepCtx) {
  if (!ctx.token || !ctx.sharedKey) { step.status = 'skip'; return }
  const api = makeClient(ctx.token)
  const code = totp(ctx.sharedKey)
  const r = await api.post('/api/me/2fa/verify', { code })
  check(step, {
    kind: 'api', description: `verify с реальным TOTP-кодом (${code}) → 200`,
    ok: r.status === 200 && r.data?.enabled === true,
    detail: `${r.status} ${asString(r.data).slice(0, 200)}`,
  })
  const status = await api.get('/api/me/2fa/status')
  check(step, { kind: 'api', description: 'status.enabled = true', ok: status.data?.enabled === true, detail: `enabled=${status.data?.enabled}` })
}

// ---------------------------------------------------------------------------

export async function tfa05_login_requires_2fa({ ctx, step, report }: StepCtx) {
  if (!ctx.email || !ctx.password || !ctx.sharedKey) { step.status = 'skip'; return }
  const api = makeClient()

  // Без otp_code → 400 invalid_grant 2fa_required
  const noOtp = await api.post('/connect/token', new URLSearchParams({
    grant_type: 'password',
    username: ctx.email, password: ctx.password,
    client_id: 'food-market-web',
    scope: 'openid profile email roles api offline_access',
  }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
  check(step, {
    kind: 'api', description: 'Логин без otp_code → 400 с 2fa_required',
    ok: noOtp.status === 400 && /2fa_required/.test(asString(noOtp.data)),
    detail: `${noOtp.status} ${asString(noOtp.data).slice(0, 250)}`,
  })

  // С правильным otp_code → 200
  const code = totp(ctx.sharedKey)
  const ok = await api.post('/connect/token', new URLSearchParams({
    grant_type: 'password',
    username: ctx.email, password: ctx.password,
    client_id: 'food-market-web',
    scope: 'openid profile email roles api offline_access',
    otp_code: code,
  }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
  check(step, {
    kind: 'api', description: `Логин с правильным otp_code (${code}) → 200`,
    ok: ok.status === 200 && !!ok.data?.access_token,
    detail: `${ok.status} access=${ok.data?.access_token ? 'yes' : 'no'}`,
  })

  // С неверным otp_code → 400 2fa_invalid
  const bad = await api.post('/connect/token', new URLSearchParams({
    grant_type: 'password',
    username: ctx.email, password: ctx.password,
    client_id: 'food-market-web',
    scope: 'openid profile email roles api offline_access',
    otp_code: '000000',
  }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
  check(step, {
    kind: 'api', description: 'Логин с неверным otp_code → 400 с 2fa_invalid',
    ok: bad.status === 400 && /2fa_invalid/.test(asString(bad.data)),
    detail: `${bad.status} ${asString(bad.data).slice(0, 250)}`,
  })

  // Обновим ctx.token валидным (для disable шага)
  if (ok.status === 200) ctx.token = ok.data.access_token
}

// ---------------------------------------------------------------------------

export async function tfa06_disable({ ctx, step, report }: StepCtx) {
  if (!ctx.token || !ctx.sharedKey) { step.status = 'skip'; return }
  const api = makeClient(ctx.token)

  // Без code → 400
  const noCode = await api.post('/api/me/2fa/disable', { code: '' })
  check(step, {
    kind: 'api', description: 'disable без кода → 400',
    ok: noCode.status === 400,
    detail: `${noCode.status} ${asString(noCode.data).slice(0, 200)}`,
  })

  // С кодом → 200
  const code = totp(ctx.sharedKey)
  const ok = await api.post('/api/me/2fa/disable', { code })
  check(step, {
    kind: 'api', description: `disable с code (${code}) → 200, enabled=false`,
    ok: ok.status === 200 && ok.data?.enabled === false,
    detail: `${ok.status} ${asString(ok.data).slice(0, 200)}`,
  })

  // status
  const status = await api.get('/api/me/2fa/status')
  check(step, { kind: 'api', description: 'status.enabled = false', ok: status.data?.enabled === false, detail: `enabled=${status.data?.enabled}` })

  // Re-enroll выдаёт НОВЫЙ key (т.к. был сброшен)
  const re = await api.post('/api/me/2fa/enroll', {})
  check(step, {
    kind: 'api', description: 'Повторный enroll → новый sharedKey (отличается от старого)',
    ok: !!re.data?.sharedKey && re.data.sharedKey !== ctx.sharedKey,
    detail: `same=${re.data?.sharedKey === ctx.sharedKey}`,
  })
}
