## Authentication — JWT-based auth with real SCRAM-SHA-256
import std/strutils
import std/base64
import std/tables
import std/times
import checksums/sha2
import scram

type
  AuthMethod* = enum
    amNone
    amSCRAMSHA256
    amJWT
    amToken

  AuthCredentials* = object
    authMethod*: AuthMethod
    username*: string
    payload*: string

  JWTClaims* = object
    sub*: string
    iss*: string
    aud*: string
    exp*: int64
    iat*: int64
    nbf*: int64
    jti*: string
    role*: string
    database*: string

  AuthResult* = object
    authenticated*: bool
    username*: string
    role*: string
    database*: string
    error*: string

  AuthManager* = ref object
    secretKey*: string
    tokens*: seq[string]
    users*: Table[string, string]  # username -> password hash (legacy)
    scramUsers*: Table[string, ScramCredential]
    scramSessions*: Table[string, ScramServerState]

proc newAuthManager*(secretKey: string = ""): AuthManager =
  AuthManager(
    secretKey: secretKey,
    tokens: @[],
    users: initTable[string, string](),
    scramUsers: initTable[string, ScramCredential](),
    scramSessions: initTable[string, ScramServerState](),
  )

# ---------------------------------------------------------------------------
# Base64 URL-safe helpers
# ---------------------------------------------------------------------------

proc base64UrlEncode(data: string): string =
  result = encode(data)
  result = result.replace("+", "-").replace("/", "_").replace("=", "")

proc base64UrlDecode(data: string): string =
  var s = data.replace("-", "+").replace("_", "/")
  while s.len mod 4 != 0:
    s &= "="
  return decode(s)

# ---------------------------------------------------------------------------
# Legacy HMAC-SHA-256 (for JWT signing)
# ---------------------------------------------------------------------------

proc hmacSha256(key, message: string): string =
  var k = key
  if k.len > 64:
    var ctx = initSha_256()
    ctx.update(k.toOpenArray(0, k.len-1))
    let hash = ctx.digest()
    k = newString(32)
    for i in 0..<32: k[i] = char(hash[i])
  while k.len < 64:
    k &= "\x00"

  var ipad = newString(64)
  var opad = newString(64)
  for i in 0..<64:
    ipad[i] = chr(ord(k[i]) xor 0x36)
    opad[i] = chr(ord(k[i]) xor 0x5c)

  var innerCtx = initSha_256()
  innerCtx.update(ipad.toOpenArray(0, ipad.len-1))
  innerCtx.update(message.toOpenArray(0, message.len-1))
  let innerHash = innerCtx.digest()

  var outerCtx = initSha_256()
  outerCtx.update(opad.toOpenArray(0, opad.len-1))
  outerCtx.update(innerHash.toOpenArray(0, innerHash.len-1))
  let outerHash = outerCtx.digest()

  return $outerHash

proc constantTimeCompare(a, b: string): bool =
  let n = max(a.len, b.len)
  var diff = a.len xor b.len
  for i in 0..<n:
    let ca = if i < a.len: ord(a[i]) else: 0
    let cb = if i < b.len: ord(b[i]) else: 0
    diff = diff or (ca xor cb)
  return diff == 0

# ---------------------------------------------------------------------------
# JWT token helpers
# ---------------------------------------------------------------------------

proc jsonEscape(s: string): string =
  result = s.replace("\\", "\\\\").replace("\"", "\\\"")

proc createToken*(am: AuthManager, claims: JWTClaims): string =
  let header = base64UrlEncode("{\"alg\":\"HS256\",\"typ\":\"JWT\"}")
  var payloadJson = "{\"sub\":\"" & jsonEscape(claims.sub) & "\",\"role\":\"" & jsonEscape(claims.role) &
    "\",\"database\":\"" & jsonEscape(claims.database) & "\""
  if claims.exp > 0:
    payloadJson &= ",\"exp\":" & $claims.exp
  if claims.iat > 0:
    payloadJson &= ",\"iat\":" & $claims.iat
  if claims.nbf > 0:
    payloadJson &= ",\"nbf\":" & $claims.nbf
  payloadJson &= "}"
  let payload = base64UrlEncode(payloadJson)
  let data = header & "." & payload
  let signature = hmacSha256(am.secretKey, data)
  am.tokens.add(data & "." & base64UrlEncode(signature))
  return am.tokens[^1]

proc verifyToken*(am: AuthManager, token: string): (bool, JWTClaims) =
  let parts = token.split(".")
  if parts.len != 3:
    return (false, JWTClaims())
  let data = parts[0] & "." & parts[1]
  let sig = hmacSha256(am.secretKey, data)
  if not constantTimeCompare(base64UrlEncode(sig), parts[2]):
    return (false, JWTClaims())
  # Parse payload
  let payload = base64UrlDecode(parts[1])
  var claims = JWTClaims()
  # Simple JSON parse: {"key":"val","key2":"val2"}
  var i = 1  # skip {
  while i < payload.len:
    if payload[i] == '}':
      break
    if payload[i] == '"':
      var key = ""
      inc i
      while i < payload.len and payload[i] != '"':
        key &= payload[i]
        inc i
      inc i  # skip closing quote
      inc i  # skip :
      var val = ""
      if i < payload.len and payload[i] == '"':
        inc i
        while i < payload.len and payload[i] != '"':
          if payload[i] == '\\' and i + 1 < payload.len:
            case payload[i+1]
            of '"': val &= '"'
            of '\\': val &= '\\'
            of '/': val &= '/'
            of 'n': val &= '\n'
            of 't': val &= '\t'
            else: val &= payload[i+1]
            inc i; inc i
          else:
            val &= payload[i]
            inc i
        inc i
      elif i < payload.len and payload[i] in {'0'..'9', '-'}:
        while i < payload.len and payload[i] notin {',', '}'}:
          val &= payload[i]
          inc i
      # Assign to claims
      var parseOk = true
      case key
      of "sub": claims.sub = val
      of "role": claims.role = val
      of "database": claims.database = val
      of "iss": claims.iss = val
      of "aud": claims.aud = val
      of "exp":
        try: claims.exp = parseInt(val)
        except ValueError: parseOk = false
      of "iat":
        try: claims.iat = parseInt(val)
        except ValueError: parseOk = false
      of "nbf":
        try: claims.nbf = parseInt(val)
        except ValueError: parseOk = false
      of "jti": claims.jti = val
      else: discard
      if not parseOk: return (false, JWTClaims())
    if i < payload.len and payload[i] == ',':
      inc i
    inc i
  # Validate expiration
  let now = int64(getTime().toUnix())
  if claims.exp > 0 and now > claims.exp:
    return (false, JWTClaims())
  if claims.nbf > 0 and now < claims.nbf:
    return (false, JWTClaims())
  if claims.iat > 0 and now < claims.iat - 60:
    # Allow 60 seconds clock skew
    return (false, JWTClaims())
  return (true, claims)

# ---------------------------------------------------------------------------
# SCRAM-SHA-256 challenge-response
# ---------------------------------------------------------------------------

proc registerScramUser*(am: AuthManager, username, password: string,
                        iterationCount: int = DefaultIterationCount) =
  ## Register a user with real SCRAM-SHA-256 credentials.
  let cred = createScramCredential(password, iterationCount = iterationCount)
  am.scramUsers[username] = cred

proc startScram*(am: AuthManager, clientFirstMessage: string): string =
  ## Start SCRAM authentication. Returns server-first-message.
  let (_, username, clientNonce) = parseClientFirst(clientFirstMessage)
  if username notin am.scramUsers:
    raise newException(ValueError, "Authentication failed")

  let cred = am.scramUsers[username]
  let serverNonce = generateNonce()
  let combinedNonce = clientNonce & serverNonce

  let serverFirst = buildServerFirst(combinedNonce, cred.salt, cred.iterationCount)

  # Compute authMessage for later verification
  let clientFirstMessageBare = "n=" & username & ",r=" & clientNonce
  let authMessage = clientFirstMessageBare & "," & serverFirst

  var state = ScramServerState(
    username: username,
    clientFirstMessageBare: clientFirstMessageBare,
    serverFirstMessage: serverFirst,
    authMessage: authMessage,
    clientNonce: clientNonce,
    serverNonce: serverNonce,
    salt: cred.salt,
    iterationCount: cred.iterationCount,
    storedKey: cred.storedKey,
    serverKey: cred.serverKey,
  )

  # Store session keyed by combined nonce
  am.scramSessions[combinedNonce] = state
  return serverFirst

proc finishScram*(am: AuthManager, clientFinalMessage: string): (bool, string) =
  ## Finish SCRAM authentication. Returns (success, server-final-message).
  let (cbind, nonce, clientProof) = parseClientFinal(clientFinalMessage)

  if nonce notin am.scramSessions:
    return (false, "e=invalid-nonce")

  var state = am.scramSessions[nonce]
  am.scramSessions.del(nonce)

  # Update authMessage with client-final-message-without-proof
  let clientFinalWithoutProof = "c=" & cbind & ",r=" & nonce
  state.authMessage = state.authMessage & "," & clientFinalWithoutProof

  if not verifyClientProof(state, clientProof):
    return (false, "e=invalid-proof")

  let serverSignature = computeServerSignature(state)
  return (true, buildServerFinal(serverSignature))

# ---------------------------------------------------------------------------
# Credential validation dispatcher
# ---------------------------------------------------------------------------

proc validateCredentials*(am: AuthManager, creds: AuthCredentials): AuthResult =
  case creds.authMethod
  of amNone:
    return AuthResult(authenticated: true, username: "anonymous", role: "default",
                      database: "default")
  of amToken, amJWT:
    if creds.payload in am.tokens:
      let (valid, claims) = am.verifyToken(creds.payload)
      if valid:
        return AuthResult(authenticated: true, username: claims.sub,
                          role: claims.role, database: claims.database)
    return AuthResult(authenticated: false, error: "Invalid token")
  of amSCRAMSHA256:
    ## SCRAM-SHA-256 requires a full challenge-response handshake.
    ## Use startScram() / finishScram() instead of validateCredentials().
    return AuthResult(authenticated: false, error: "SCRAM requires challenge-response handshake. Use startScram/finishScram.")

# ---------------------------------------------------------------------------
# Token / user management
# ---------------------------------------------------------------------------

proc addToken*(am: var AuthManager, token: string) =
  am.tokens.add(token)

proc revokeToken*(am: var AuthManager, token: string) =
  var idx = am.tokens.find(token)
  if idx >= 0:
    am.tokens.del(idx)

proc isAuthenticated*(r: AuthResult): bool = r.authenticated

proc addScramUser*(am: var AuthManager, username, passwordHash: string) =
  ## Legacy helper: stores raw hash. Use registerScramUser() for real SCRAM.
  am.users[username] = passwordHash
