## nimpak/remote/auth.nim
## Authentication and authorization for NimPak remote operations
##
## This module provides secure authentication for remote repositories including:
## - API key authentication
## - Token-based authentication with refresh
## - Certificate-based authentication
## - Integration with the trust and keyring systems

import std/[os, times, json, base64, strutils, strformat, httpclient, tables]
import ../security/[keyring_manager, signature_verifier_working, event_logger]

type
  AuthMethod* = enum
    AuthNone = "none"
    AuthApiKey = "api_key"
    AuthToken = "token"
    AuthCertificate = "certificate"
    AuthSignature = "signature"

  AuthCredentials* = object
    case method*: AuthMethod
    of AuthNone:
      discard
    of AuthApiKey:
      apiKey*: string
    of AuthToken:
      token*: string
      refreshToken*: Option[string]
      expiresAt*: times.DateTime
    of AuthCertificate:
      certificatePath*: string
      privateKeyPath*: string
    of AuthSignature:
      keyId*: string
      privateKey*: string

  AuthContext* = object
    repositoryId*: string
    credentials*: AuthCredentials
    lastAuth*: times.DateTime
    authValid*: bool
    errorCount*: int

  AuthManager* = object
    contexts*: Table[string, AuthContext]
    keyringManager*: KeyringManager
    config*: AuthConfig

  AuthConfig* = object
    tokenRefreshThreshold*: int  # Seconds before expiry to refresh
    maxAuthRetries*: int
    authTimeout*: int           # Authentication timeout in seconds
    enableSignatureAuth*: bool  # Enable signature-based authentication

  AuthResult*[T] = object
    case success*: bool
    of true:
      value*: T
    of false:
      error*: string
      errorCode*: int

# =============================================================================
# Authentication Manager
# =============================================================================

proc newAuthManager*(keyringManager: KeyringManager): AuthManager =
  ## Create a new authentication manager
  AuthManager(
    contexts: initTable[string, AuthContext](),
    keyringManager: keyringManager,
    config: AuthConfig(
      tokenRefreshThreshold: 300,  # 5 minutes
      maxAuthRetries: 3,
      authTimeout: 30000,         # 30 seconds
      enableSignatureAuth: true
    )
  )

proc addAuthContext*(manager: var AuthManager, repositoryId: string,
                    credentials: AuthCredentials): AuthResult[bool] =
  ## Add authentication context for a repository
  try:
    let context = AuthContext(
      repositoryId: repositoryId,
      credentials: credentials,
      lastAuth: default(times.DateTime),
      authValid: false,
      errorCount: 0
    )

    manager.contexts[repositoryId] = context

    # Log authentication setup
    logGlobalSecurityEvent(EventSystemStartup, SeverityInfo, "auth-manager",
      fmt"Authentication configured for repository: {repositoryId} (method: {credentials.method})")

    return AuthResult[bool](success: true, value: true)

  except Exception as e:
    return AuthResult[bool](success: false, error: fmt"Failed to add auth context: {e.msg}", errorCode: 500)

proc removeAuthContext*(manager: var AuthManager, repositoryId: string): AuthResult[bool] =
  ## Remove authentication context for a repository
  if repositoryId in manager.contexts:
    manager.contexts.del(repositoryId)

    logGlobalSecurityEvent(EventSystemStartup, SeverityInfo, "auth-manager",
      fmt"Authentication removed for repository: {repositoryId}")

    return AuthResult[bool](success: true, value: true)
  else:
    return AuthResult[bool](success: false, error: fmt"Auth context not found: {repositoryId}", errorCode: 404)

# =============================================================================
# API Key Authentication
# =============================================================================

proc authenticateWithApiKey*(manager: AuthManager, repositoryId: string,
                            client: var HttpClient): AuthResult[bool] =
  ## Authenticate using API key
  try:
    if repositoryId notin manager.contexts:
      return AuthResult[bool](success: false, error: "Auth context not found", errorCode: 404)

    let context = manager.contexts[repositoryId]
    if context.credentials.method != AuthApiKey:
      return AuthResult[bool](success: false, error: "Invalid auth method", errorCode: 400)

    # Add API key to headers
    client.headers["Authorization"] = fmt"Bearer {context.credentials.apiKey}"
    client.headers["X-API-Key"] = context.credentials.apiKey

    # Update context
    manager.contexts[repositoryId].lastAuth = now()
    manager.contexts[repositoryId].authValid = true

    return AuthResult[bool](success: true, value: true)

  except Exception as e:
    return AuthResult[bool](success: false, error: fmt"API key auth failed: {e.msg}", errorCode: 500)

# =============================================================================
# Token-Based Authentication
# =============================================================================

proc authenticateWithToken*(manager: var AuthManager, repositoryId: string,
                           client: var HttpClient): AuthResult[bool] =
  ## Authenticate using token with automatic refresh
  try:
    if repositoryId notin manager.contexts:
      return AuthResult[bool](success: false, error: "Auth context not found", errorCode: 404)

    var context = manager.contexts[repositoryId]
    if context.credentials.method != AuthToken:
      return AuthResult[bool](success: false, error: "Invalid auth method", errorCode: 400)

    # Check if token needs refresh
    let now = times.now()
    let timeToExpiry = (context.credentials.expiresAt - now).inSeconds

    if timeToExpiry <= manager.config.tokenRefreshThreshold:
      # Refresh token
      let refreshResult = refreshToken(manager, repositoryId, client)
      if not refreshResult.success:
        return AuthResult[bool](success: false, error: refreshResult.error, errorCode: refreshResult.errorCode)

      context = manager.contexts[repositoryId]  # Get updated context

    # Add token to headers
    client.headers["Authorization"] = fmt"Bearer {context.credentials.token}"

    # Update context
    manager.contexts[repositoryId].lastAuth = now
    manager.contexts[repositoryId].authValid = true

    return AuthResult[bool](success: true, value: true)

  except Exception as e:
    return AuthResult[bool](success: false, error: fmt"Token auth failed: {e.msg}", errorCode: 500)

proc refreshToken*(manager: var AuthManager, repositoryId: string,
                  client: HttpClient): AuthResult[bool] =
  ## Refresh authentication token
  try:
    if repositoryId notin manager.contexts:
      return AuthResult[bool](success: false, error: "Auth context not found", errorCode: 404)

    let context = manager.contexts[repositoryId]
    if context.credentials.refreshToken.isNone():
      return AuthResult[bool](success: false, error: "No refresh token available", errorCode: 400)

    # Prepare refresh request
    let refreshData = %*{
      "refresh_token": context.credentials.refreshToken.get(),
      "grant_type": "refresh_token"
    }

    # Make refresh request
    let response = client.postContent("/auth/refresh", $refreshData)
    let responseJson = parseJson(response)

    # Extract new tokens
    let newToken = responseJson["access_token"].getStr()
    let newRefreshToken = responseJson.getOrDefault("refresh_token").getStr(context.credentials.refreshToken.get())
    let expiresIn = responseJson["expires_in"].getInt()

    # Update credentials
    var newCredentials = context.credentials
    newCredentials.token = newToken
    newCredentials.refreshToken = some(newRefreshToken)
    newCredentials.expiresAt = now() + initDuration(seconds = expiresIn)

    # Update context
    manager.contexts[repositoryId].credentials = newCredentials
    manager.contexts[repositoryId].lastAuth = now()
    manager.contexts[repositoryId].authValid = true

    logGlobalSecurityEvent(EventSystemHealthCheck, SeverityInfo, "auth-manager",
      fmt"Token refreshed for repository: {repositoryId}")

    return AuthResult[bool](success: true, value: true)

  except Exception as e:
    return AuthResult[bool](success: false, error: fmt"Token refresh failed: {e.msg}", errorCode: 500)

# =============================================================================
# Certificate-Based Authentication
# =============================================================================

proc authenticateWithCertificate*(manager: AuthManager, repositoryId: string,
                                 client: var HttpClient): AuthResult[bool] =
  ## Authenticate using client certificate
  try:
    if repositoryId notin manager.contexts:
      return AuthResult[bool](success: false, error: "Auth context not found", errorCode: 404)

    let context = manager.contexts[repositoryId]
    if context.credentials.method != AuthCertificate:
      return AuthResult[bool](success: false, error: "Invalid auth method", errorCode: 400)

    # Check certificate files exist
    if not fileExists(context.credentials.certificatePath):
      return AuthResult[bool](success: false, error: "Certificate file not found", errorCode: 404)

    if not fileExists(context.credentials.privateKeyPath):
      return AuthResult[bool](success: false, error: "Private key file not found", errorCode: 404)

    # TODO: Configure client certificate when TLS library supports it
    # For now, we assume certificate authentication is handled at the TLS level

    # Update context
    manager.contexts[repositoryId].lastAuth = now()
    manager.contexts[repositoryId].authValid = true

    logGlobalSecurityEvent(EventSystemHealthCheck, SeverityInfo, "auth-manager",
      fmt"Certificate authentication configured for repository: {repositoryId}")

    return AuthResult[bool](success: true, value: true)

  except Exception as e:
    return AuthResult[bool](success: false, error: fmt"Certificate auth failed: {e.msg}", errorCode: 500)

# =============================================================================
# Signature-Based Authentication
# =============================================================================

proc createAuthSignature*(manager: AuthManager, repositoryId: string,
                         httpMethod: string, url: string, body: string = "",
                         timestamp: times.DateTime = now()): AuthResult[string] =
  ## Create authentication signature for request
  try:
    if repositoryId notin manager.contexts:
      return AuthResult[string](success: false, error: "Auth context not found", errorCode: 404)

    let context = manager.contexts[repositoryId]
    if context.credentials.method != AuthSignature:
      return AuthResult[string](success: false, error: "Invalid auth method", errorCode: 400)

    # Create signature payload
    let timestampStr = timestamp.format("yyyy-MM-dd'T'HH:mm:ss'Z'")
    let payload = fmt"{httpMethod}\n{url}\n{timestampStr}\n{body}"

    # TODO: Create actual signature when crypto is available
    # For now, create a placeholder signature
    let signature = base64.encode(fmt"signature-{context.credentials.keyId}-{payload.len}")

    return AuthResult[string](success: true, value: signature)

  except Exception as e:
    return AuthResult[string](success: false, error: fmt"Signature creation failed: {e.msg}", errorCode: 500)

proc authenticateWithSignature*(manager: AuthManager, repositoryId: string,
                               client: var HttpClient, httpMethod: string,
                               url: string, body: string = ""): AuthResult[bool] =
  ## Authenticate using request signature
  try:
    if repositoryId notin manager.contexts:
      return AuthResult[bool](success: false, error: "Auth context not found", errorCode: 404)

    let context = manager.contexts[repositoryId]
    if context.credentials.method != AuthSignature:
      return AuthResult[bool](success: false, error: "Invalid auth method", errorCode: 400)

    # Create signature
    let timestamp = now()
    let signatureResult = manager.createAuthSignature(repositoryId, httpMethod, url, body, timestamp)
    if not signatureResult.success:
      return AuthResult[bool](success: false, error: signatureResult.error, errorCode: signatureResult.errorCode)

    # Add signature headers
    client.headers["Authorization"] = fmt"Signature keyId=\"{context.credentials.keyId}\",signature=\"{signatureResult.value}\""
    client.headers["X-Timestamp"] = timestamp.format("yyyy-MM-dd'T'HH:mm:ss'Z'")
    client.headers["X-Key-Id"] = context.credentials.keyId

    # Update context
    manager.contexts[repositoryId].lastAuth = timestamp
    manager.contexts[repositoryId].authValid = true

    return AuthResult[bool](success: true, value: true)

  except Exception as e:
    return AuthResult[bool](success: false, error: fmt"Signature auth failed: {e.msg}", errorCode: 500)

# =============================================================================
# Generic Authentication
# =============================================================================

proc authenticate*(manager: var AuthManager, repositoryId: string,
                  client: var HttpClient, httpMethod: string = "GET",
                  url: string = "", body: string = ""): AuthResult[bool] =
  ## Authenticate using the configured method for the repository
  try:
    if repositoryId notin manager.contexts:
      return AuthResult[bool](success: false, error: "Auth context not found", errorCode: 404)

    let context = manager.contexts[repositoryId]

    case context.credentials.method:
    of AuthNone:
      return AuthResult[bool](success: true, value: true)

    of AuthApiKey:
      return manager.authenticateWithApiKey(repositoryId, client)

    of AuthToken:
      return manager.authenticateWithToken(repositoryId, client)

    of AuthCertificate:
      return manager.authenticateWithCertificate(repositoryId, client)

    of AuthSignature:
      return manager.authenticateWithSignature(repositoryId, client, httpMethod, url, body)

  except Exception as e:
    # Update error count
    if repositoryId in manager.contexts:
      manager.contexts[repositoryId].errorCount += 1
      manager.contexts[repositoryId].authValid = false

    return AuthResult[bool](success: false, error: fmt"Authentication failed: {e.msg}", errorCode: 500)

# =============================================================================
# Authentication Status and Management
# =============================================================================

proc isAuthValid*(manager: AuthManager, repositoryId: string): bool =
  ## Check if authentication is valid for a repository
  if repositoryId notin manager.contexts:
    return false

  let context = manager.contexts[repositoryId]

  # Check basic validity
  if not context.authValid:
    return false

  # Check token expiry for token-based auth
  if context.credentials.method == AuthToken:
    let timeToExpiry = (context.credentials.expiresAt - now()).inSeconds
    if timeToExpiry <= 0:
      return false

  return true

proc getAuthStatus*(manager: AuthManager, repositoryId: string): JsonNode =
  ## Get authentication status for a repository
  if repositoryId notin manager.contexts:
    return %*{
      "repository_id": repositoryId,
      "authenticated": false,
      "error": "Auth context not found"
    }

  let context = manager.contexts[repositoryId]
  var status = %*{
    "repository_id": repositoryId,
    "method": $context.credentials.method,
    "authenticated": context.authValid,
    "last_auth": $context.lastAuth,
    "error_count": context.errorCount
  }

  # Add method-specific information
  case context.credentials.method:
  of AuthToken:
    status["expires_at"] = %($context.credentials.expiresAt)
    status["time_to_expiry"] = %(context.credentials.expiresAt - now()).inSeconds
  of AuthSignature:
    status["key_id"] = %context.credentials.keyId
  else:
    discard

  return status

proc clearAuthCache*(manager: var AuthManager, repositoryId: string = ""): AuthResult[bool] =
  ## Clear authentication cache
  try:
    if repositoryId == "":
      # Clear all contexts
      for repoId in manager.contexts.keys:
        manager.contexts[repoId].authValid = false
        manager.contexts[repoId].lastAuth = default(times.DateTime)

      logGlobalSecurityEvent(EventSystemHealthCheck, SeverityInfo, "auth-manager",
        "Authentication cache cleared for all repositories")
    else:
      # Clear specific context
      if repositoryId in manager.contexts:
        manager.contexts[repositoryId].authValid = false
        manager.contexts[repositoryId].lastAuth = default(times.DateTime)

        logGlobalSecurityEvent(EventSystemHealthCheck, SeverityInfo, "auth-manager",
          fmt"Authentication cache cleared for repository: {repositoryId}")

    return AuthResult[bool](success: true, value: true)

  except Exception as e:
    return AuthResult[bool](success: false, error: fmt"Failed to clear auth cache: {e.msg}", errorCode: 500)

# =============================================================================
# Credential Management
# =============================================================================

proc createApiKeyCredentials*(apiKey: string): AuthCredentials =
  ## Create API key credentials
  AuthCredentials(method: AuthApiKey, apiKey: apiKey)

proc createTokenCredentials*(token: string, refreshToken: string = "",
                           expiresIn: int = 3600): AuthCredentials =
  ## Create token credentials
  AuthCredentials(
    method: AuthToken,
    token: token,
    refreshToken: if refreshToken != "": some(refreshToken) else: none(string),
    expiresAt: now() + initDuration(seconds = expiresIn)
  )

proc createCertificateCredentials*(certificatePath: string, privateKeyPath: string): AuthCredentials =
  ## Create certificate credentials
  AuthCredentials(
    method: AuthCertificate,
    certificatePath: certificatePath,
    privateKeyPath: privateKeyPath
  )

proc createSignatureCredentials*(keyId: string, privateKey: string): AuthCredentials =
  ## Create signature credentials
  AuthCredentials(
    method: AuthSignature,
    keyId: keyId,
    privateKey: privateKey
  )

# =============================================================================
# Export main functions
# =============================================================================

export AuthMethod, AuthCredentials, AuthContext, AuthManager, AuthConfig, AuthResult
export newAuthManager, addAuthContext, removeAuthContext
export authenticateWithApiKey, authenticateWithToken, refreshToken
export authenticateWithCertificate, authenticateWithSignature
export createAuthSignature, authenticate
export isAuthValid, getAuthStatus, clearAuthCache
export createApiKeyCredentials, createTokenCredentials
export createCertificateCredentials, createSignatureCredentials