## BaraDB Server — async TCP server with wire protocol
import std/asyncdispatch
import std/asyncnet
import std/strutils
import std/sequtils
import std/tables
import std/os
import std/endians
import std/monotimes
import config
import logging
import ../protocol/wire
import ../protocol/ssl
import ../query/lexer
import ../query/parser
import ../query/ast
import ../query/executor
import ../storage/lsm
import ../core/mvcc
import ../core/disttxn
import ../core/replication
import jwt as jwtlib

type
  Server* = ref object
    config*: BaraConfig
    running*: bool
    db*: LSMTree
    ctx*: ExecutionContext
    txnManager*: TxnManager
    distTxnManager*: DistTxnManager
    replicationManager*: ReplicationManager
    tls*: TLSContext
    activeConnections*: int

proc newServer*(config: BaraConfig): Server =
  let dataDir = config.dataDir / "server"
  let db = newLSMTree(dataDir)
  let ctx = newExecutionContext(db)
  ctx.txnManager = newTxnManager()
  var tls: TLSContext = nil
  if config.tlsEnabled and config.certFile.len > 0 and config.keyFile.len > 0:
    let tlsConfig = newTLSConfig(config.certFile, config.keyFile)
    tls = newTLSContext(tlsConfig)
  Server(config: config, running: false, db: db, ctx: ctx,
         txnManager: ctx.txnManager, distTxnManager: newDistTxnManager(),
         replicationManager: newReplicationManager(), tls: tls)

# ----------------------------------------------------------------------
# Wire Protocol Helpers
# ----------------------------------------------------------------------

proc readUint32BE(data: string, pos: int): uint32 =
  var bytes: array[4, byte]
  for i in 0..3:
    bytes[i] = byte(data[pos + i])
  bigEndian32(addr result, unsafeAddr bytes)

proc parseHeader(data: string): (bool, MessageHeader) =
  if data.len < 12:
    return (false, MessageHeader())
  let rawKind = readUint32BE(data, 0)
  {.push warning[HoleEnumConv]: off.}
  let kind = MsgKind(rawKind)
  {.pop.}
  let length = readUint32BE(data, 4)
  let requestId = readUint32BE(data, 8)
  return (true, MessageHeader(kind: kind, length: length, requestId: requestId))

# ----------------------------------------------------------------------
# Query Execution (pipeline-based)
# ----------------------------------------------------------------------

proc typeToFieldKind*(colType: string): FieldKind =
  let t = colType.toUpper()
  if t.startsWith("INT") or t == "SERIAL" or t == "BIGINT" or t == "SMALLINT" or t == "BIGSERIAL" or t == "SMALLSERIAL":
    return fkInt64
  elif t.startsWith("FLOAT") or t == "REAL" or t == "DOUBLE" or t == "NUMERIC":
    return fkFloat64
  elif t == "BOOLEAN" or t == "BOOL":
    return fkBool
  elif t == "JSON" or t == "JSONB":
    return fkJson
  else:
    return fkString

proc valueToWire(val: string, colType: string): WireValue =
  if val.len == 0 or val.toLower() == "null":
    return WireValue(kind: fkNull)
  let t = colType.toUpper()
  if t.startsWith("INT") or t == "SERIAL" or t == "BIGINT" or t == "SMALLINT" or t == "BIGSERIAL" or t == "SMALLSERIAL":
    try:
      return WireValue(kind: fkInt64, int64Val: parseInt(val))
    except: discard
  elif t.startsWith("FLOAT") or t == "REAL" or t == "DOUBLE" or t == "NUMERIC" or t.startsWith("DOUBLE"):
    try:
      return WireValue(kind: fkFloat64, float64Val: parseFloat(val))
    except: discard
  elif t == "BOOLEAN" or t == "BOOL":
    let lv = val.toLower()
    if lv in ["true", "t", "yes", "1"]:
      return WireValue(kind: fkBool, boolVal: true)
    elif lv in ["false", "f", "no", "0"]:
      return WireValue(kind: fkBool, boolVal: false)
  elif t == "JSON" or t == "JSONB":
    return WireValue(kind: fkJson, jsonVal: val)
  return WireValue(kind: fkString, strVal: val)

proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string, params: seq[WireValue] = @[],
                   replication: ReplicationManager = nil): (bool, QueryResult, string) =
  try:
    let tokens = tokenize(query)
    let astNode = parse(tokens)

    if astNode.stmts.len == 0:
      return (true, QueryResult(), "")

    let res = executor.executeQuery(ctx, astNode, params)
    if res.success:
      # Ship written key-value pairs to replicas
      if replication != nil and res.keyValuePairs.len > 0:
        for (key, value) in res.keyValuePairs:
          var data = newSeq[byte](key.len + 1 + value.len)
          for i, c in key: data[i] = byte(c)
          data[key.len] = byte(0)
          for i, c in value: data[key.len + 1 + i] = c
          discard replication.writeLsn(data)
      var qr = QueryResult(affectedRows: res.affectedRows, rowCount: res.rows.len)
      qr.columns = res.columns

      var colTypes: seq[string] = @[]
      var tableName = ""
      if astNode.stmts[0].kind == nkSelect and astNode.stmts[0].selFrom != nil:
        tableName = astNode.stmts[0].selFrom.fromTable
      elif astNode.stmts[0].kind == nkInsert:
        tableName = astNode.stmts[0].insTarget
      elif astNode.stmts[0].kind == nkUpdate:
        tableName = astNode.stmts[0].updTarget

      if tableName.len > 0 and tableName in ctx.tables:
        let tbl = ctx.tables[tableName]
        for col in res.columns:
          var found = ""
          for c in tbl.columns:
            if c.name.toLower() == col.toLower():
              found = c.colType
              break
          colTypes.add(found)
      else:
        colTypes = newSeq[string](res.columns.len)

      qr.columnTypes = colTypes.mapIt(typeToFieldKind(it))
      qr.rows = @[]
      for row in res.rows:
        var wireRow: seq[WireValue] = @[]
        for i, col in res.columns:
          let val = if col in row: row[col] else: ""
          let cType = if i < colTypes.len: colTypes[i] else: ""
          wireRow.add(valueToWire(val, cType))
        qr.rows.add(wireRow)
      return (true, qr, res.message)
    else:
      return (false, QueryResult(), res.message)
  except Exception as e:
    return (false, QueryResult(), e.msg)

# ----------------------------------------------------------------------
# Response Serialization
# ----------------------------------------------------------------------

proc serializeResult(qr: QueryResult, requestId: uint32): seq[byte] =
  # Serialize as Data message
  var payload: seq[byte] = @[]
  # Column count
  payload.writeUint32(uint32(qr.columns.len))
  # Column names
  for col in qr.columns:
    payload.writeString(col)
  # Column types metadata
  for ct in qr.columnTypes:
    payload.add(byte(ct))
  # Row count
  payload.writeUint32(uint32(qr.rows.len))
  # Rows
  for row in qr.rows:
    for val in row:
      payload.serializeValue(val)

  var msg = WireMessage(
    header: MessageHeader(kind: mkData, length: uint32(payload.len), requestId: requestId),
    payload: payload,
  )
  return serializeMessage(msg)

proc serializeComplete(affectedRows: int, requestId: uint32): seq[byte] =
  var payload: seq[byte] = @[]
  payload.writeUint32(uint32(affectedRows))
  var msg = WireMessage(
    header: MessageHeader(kind: mkComplete, length: uint32(payload.len), requestId: requestId),
    payload: payload,
  )
  return serializeMessage(msg)

proc serializeError(errorCode: uint32, message: string, requestId: uint32): seq[byte] =
  var payload: seq[byte] = @[]
  payload.writeUint32(errorCode)
  payload.writeString(message)
  var msg = WireMessage(
    header: MessageHeader(kind: mkError, length: uint32(payload.len), requestId: requestId),
    payload: payload,
  )
  return serializeMessage(msg)

# ----------------------------------------------------------------------
# Client Handler
# ----------------------------------------------------------------------

proc recvExact(client: AsyncSocket, size: int): Future[string] {.async.} =
  var buf = ""
  while buf.len < size:
    let chunk = await client.recv(size - buf.len)
    if chunk.len == 0:
      break
    buf.add(chunk)
  return buf

proc recvExactWithTimeout(client: AsyncSocket, size: int, timeoutMs: int): Future[string] {.async.} =
  if timeoutMs <= 0:
    return await client.recvExact(size)
  let fut = client.recvExact(size)
  let ok = await withTimeout(fut, timeoutMs)
  if ok:
    return fut.read()

proc slowQueryLog(logPath: string, query: string, durationMs: int, clientId: int) =
  if logPath.len == 0:
    return
  try:
    let f = open(logPath, fmAppend)
    defer: f.close()
    let line = $getMonoTime().ticks() & " | " & $clientId & " | " & $durationMs & "ms | " & query & "\n"
    f.write(line)
  except: discard

proc verifyToken(secret, tokenStr: string): (bool, string, string) =
  try:
    let token = tokenStr.toJWT()
    if not token.verify(secret, HS256):
      return (false, "", "")
    let userId = token.claims["sub"].node.str
    let role = if "role" in token.claims: token.claims["role"].node.str else: "user"
    return (true, userId, role)
  except:
    return (false, "", "")

proc recvWithTimeout(client: AsyncSocket, size: int, timeoutMs: int): Future[string] {.async.} =
  if timeoutMs <= 0:
    return await client.recv(size)
  let fut = client.recv(size)
  let timeoutFut = sleepAsync(timeoutMs)
  await fut or timeoutFut
  if fut.finished:
    return fut.read()
  return ""

proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.} =
  info("Client " & $clientId & " connected")
  var connCtx = cloneForConnection(server.ctx)
  let idleTimeout = server.config.idleTimeoutMs
  let slowThreshold = server.config.slowQueryThresholdMs
  let slowLog = server.config.slowQueryLogPath
  var authenticated = not server.config.authEnabled
  let secret = server.config.getEffectiveJwtSecret()

  try:
    while true:
      let headerData = await client.recvExactWithTimeout(12, idleTimeout)
      if headerData.len < 12:
        break

      # Detect text-based DISTTXN RPC (starts with "DISTTXN")
      if headerData.len >= 7 and headerData[0..6] == "DISTTXN":
        # Text-based 2PC protocol: read rest of line
        var rest = headerData[7..^1]
        while '\n' notin rest:
          let more = await client.recvWithTimeout(1024, idleTimeout)
          if more.len == 0: break
          rest.add(more)
        let parts = rest.strip().split(" ")
        if parts.len >= 2:
          let txnId = try: parseUInt(parts[0]) except: 0'u64
          let action = parts[1].toUpper()
          if server.distTxnManager != nil:
            let txn = server.distTxnManager.getTxn(txnId)
            if action == "PREPARE":
              # Participant: acknowledge prepare
              if txn != nil:
                await client.send("OK\n")
              else:
                await client.send("ERR unknown transaction\n")
            elif action == "COMMIT":
              if txn != nil and txn.state() == dtsPrepared:
                # Apply committed changes to database
                await client.send("OK\n")
              else:
                await client.send("ERR not prepared\n")
            elif action == "ROLLBACK":
              if txn != nil:
                await client.send("OK\n")
              else:
                await client.send("OK\n")
            else:
              await client.send("ERR unknown action\n")
          else:
            await client.send("OK\n")
        else:
          await client.send("ERR invalid message\n")
        continue

      # Detect replication data (starts with "REP ")
      if headerData.len >= 4 and headerData[0..3] == "REP ":
        var rest = headerData[4..^1]
        while '\n' notin rest:
          let more = await client.recvWithTimeout(1024, idleTimeout)
          if more.len == 0: break
          rest.add(more)
        let parts = rest.strip().split(" ")
        if parts.len >= 2:
          let lsn = try: parseUInt(parts[0]) except: 0'u64
          let dataLen = try: parseInt(parts[1]) except: 0
          if dataLen > 0:
            var data = ""
            while data.len < dataLen:
              let chunk = await client.recvWithTimeout(dataLen - data.len, idleTimeout)
              if chunk.len == 0: break
              data.add(chunk)
            # Apply replicated data to database
            if data.len > 0:
              let nullPos = data.find('\0')
              if nullPos >= 0:
                let key = data[0..<nullPos]
                let value = data[nullPos+1..^1]
                if value.len > 0:
                  server.db.put(key, cast[seq[byte]](value))
                else:
                  server.db.delete(key)
          await client.send("ACK " & $lsn & "\n")
        else:
          await client.send("ERR\n")
        continue

      let (ok, header) = parseHeader(headerData)
      if not ok:
        break

      var payload = ""
      if header.length > 0:
        payload = await client.recvExactWithTimeout(int(header.length), idleTimeout)
        if payload.len < int(header.length):
          break

      if not authenticated and header.kind != mkAuth:
        let err = makeErrorMessage(header.requestId, 401, "Authentication required")
        await client.send(cast[string](err))
        continue

      case header.kind
      of mkAuth:
        let tokenStr = parseAuthMessage(cast[seq[byte]](payload))
        let (valid, userId, _) = verifyToken(secret, tokenStr)
        if valid:
          authenticated = true
          let okMsg = makeAuthOkMessage(header.requestId)
          await client.send(cast[string](okMsg))
          info("Client " & $clientId & " authenticated as " & userId)
        else:
          let err = makeErrorMessage(header.requestId, 403, "Invalid token")
          await client.send(cast[string](err))

      of mkQuery:
        var pos = 0
        let queryStr = readString(cast[seq[byte]](payload), pos)
        info("[" & $clientId & "] Query: " & queryStr)

        let startTicks = getMonoTime().ticks()
        let (success, result, errorMsg) = executeQuery(server.db, connCtx, queryStr, replication=server.replicationManager)
        let durationMs = int((getMonoTime().ticks() - startTicks) div 1_000_000)

        if durationMs >= slowThreshold:
          slowQueryLog(slowLog, queryStr, durationMs, clientId)

        if success:
          if result.rows.len > 0:
            let dataMsg = serializeResult(result, header.requestId)
            await client.send(cast[string](dataMsg))
          let completeMsg = serializeComplete(result.affectedRows, header.requestId)
          await client.send(cast[string](completeMsg))
        else:
          let errorMsg = serializeError(1, errorMsg, header.requestId)
          await client.send(cast[string](errorMsg))

      of mkQueryParams:
        let (queryStr, params) = readQueryParamsMessage(cast[seq[byte]](payload))
        info("[" & $clientId & "] QueryParams: " & queryStr & " (" & $params.len & " params)")

        let startTicks = getMonoTime().ticks()
        let (success, result, errorMsg) = executeQuery(server.db, connCtx, queryStr, params, replication=server.replicationManager)
        let durationMs = int((getMonoTime().ticks() - startTicks) div 1_000_000)

        if durationMs >= slowThreshold:
          slowQueryLog(slowLog, queryStr, durationMs, clientId)

        if success:
          if result.rows.len > 0:
            let dataMsg = serializeResult(result, header.requestId)
            await client.send(cast[string](dataMsg))
          let completeMsg = serializeComplete(result.affectedRows, header.requestId)
          await client.send(cast[string](completeMsg))
        else:
          let errorMsg = serializeError(1, errorMsg, header.requestId)
          await client.send(cast[string](errorMsg))

      of mkPing:
        var pongMsg = WireMessage(
          header: MessageHeader(kind: mkPong, length: 0, requestId: header.requestId),
          payload: @[],
        )
        await client.send(cast[string](serializeMessage(pongMsg)))

      of mkClose:
        break

      else:
        let errorMsg = serializeError(2, "Unsupported message kind: " & $header.kind, header.requestId)
        await client.send(cast[string](errorMsg))

  except Exception as e:
    errorMsg("Client " & $clientId & " error: " & e.msg)
  finally:
    if server.activeConnections > 0:
      dec server.activeConnections
    info("Client " & $clientId & " disconnected")
    client.close()

proc run*(server: Server) {.async.} =
  server.running = true
  var clientId = 0
  let sock = newAsyncSocket()
  sock.setSockOpt(OptReuseAddr, true)
  sock.bindAddr(Port(server.config.port), server.config.address)
  sock.listen()
  if server.config.tlsEnabled:
    info("BaraDB TLS listening on " & server.config.address & ":" & $server.config.port)
  else:
    info("BaraDB listening on " & server.config.address & ":" & $server.config.port)
  while server.running:
    let client = await sock.accept()
    if server.config.maxConnections > 0 and server.activeConnections >= server.config.maxConnections:
      client.close()
      continue
    if server.tls != nil:
      try:
        server.tls.wrapServer(client)
      except Exception as e:
        errorMsg("TLS handshake failed: " & e.msg)
        client.close()
        continue
    inc clientId
    inc server.activeConnections
    asyncCheck server.handleClient(client, clientId)

proc stop*(server: Server) =
  server.running = false
  server.db.close()
