## Read-Only Protection Manager
##
## This module implements the read-only protection system for CAS storage,
## ensuring immutability by default with controlled write access elevation.
##
## SECURITY NOTE: chmod-based protection is a UX feature, NOT a security feature!
## In user-mode (~/.local/share/nexus/cas/), chmod 555 only prevents ACCIDENTAL
## deletion/modification. A user who owns the files can bypass this trivially.
##
## Real security comes from:
## 1. Merkle tree verification (cryptographic integrity)
## 2. User namespaces (kernel-enforced read-only mounts during execution)
## 3. Root ownership (system-mode only: /var/lib/nexus/cas/)
##
## See docs/cas-security-architecture.md for full security model.

import std/[os, times, sequtils, strutils]
import xxhash

type
  # Result types for error handling
  VoidResult*[E] = object
    case isOk*: bool
    of true:
      discard
    of false:
      errValue*: E

  # Error types
  ErrorCode* = enum
    FileWriteError, FileReadError, UnknownError

  CasError* = object of CatchableError
    code*: ErrorCode
    objectHash*: string

  ProtectionManager* = object
    casPath*: string      ## Path to CAS root directory
    auditLog*: string     ## Path to audit log file

  SecurityError* = object of CatchableError
    code*: string
    context*: string

proc ok*[E](dummy: typedesc[E]): VoidResult[E] =
  VoidResult[E](isOk: true)

proc newProtectionManager*(casPath: string): ProtectionManager =
  ## Create a new protection manager for the given CAS path
  result = ProtectionManager(
    casPath: casPath,
    auditLog: casPath / "audit.log"
  )

proc logOperation*(pm: ProtectionManager, op: string, path: string, hash: string = "") =
  ## Log a write operation to the audit log
  try:
    let timestamp = now().format("yyyy-MM-dd'T'HH:mm:ss'Z'")
    var logEntry = "[" & timestamp & "] " & op & " path=" & path
    if hash.len > 0:
      logEntry.add(" hash=" & hash)
    logEntry.add("\n")

    let logFile = open(pm.auditLog, fmAppend)
    logFile.write(logEntry)
    logFile.close()
  except IOError:
    # If we can't write to audit log, continue anyway
    # (better to allow operation than to fail)
    discard

proc setReadOnly*(pm: ProtectionManager): VoidResult[CasError] =
  ## Set CAS directory to read-only (chmod 555)
  try:
    setFilePermissions(pm.casPath, {fpUserRead, fpUserExec,
                                     fpGroupRead, fpGroupExec,
                                     fpOthersRead, fpOthersExec})
    pm.logOperation("SET_READONLY", pm.casPath)
    return ok(CasError)
  except OSError as e:
    return VoidResult[CasError](isOk: false, errValue: CasError(
      code: FileWriteError,
      msg: "Failed to set read-only permissions: " & e.msg
    ))

proc setWritable*(pm: ProtectionManager): VoidResult[CasError] =
  ## Set CAS directory to writable (chmod 755)
  try:
    setFilePermissions(pm.casPath, {fpUserRead, fpUserWrite, fpUserExec,
                                     fpGroupRead, fpGroupExec,
                                     fpOthersRead, fpOthersExec})
    pm.logOperation("SET_WRITABLE", pm.casPath)
    return ok(CasError)
  except OSError as e:
    return VoidResult[CasError](isOk: false, errValue: CasError(
      code: FileWriteError,
      msg: "Failed to set writable permissions: " & e.msg
    ))

proc withWriteAccess*(pm: ProtectionManager, operation: proc()): VoidResult[CasError] =
  ## Execute operation with temporary write access, then restore read-only
  ## This ensures atomic permission elevation and restoration
  var oldPerms: set[FilePermission]

  try:
    # Save current permissions
    oldPerms = getFilePermissions(pm.casPath)

    # Enable write (755)
    let setWritableResult = pm.setWritable()
    if not setWritableResult.isOk:
      return setWritableResult

    # Perform operation
    operation()

    # Restore read-only (555)
    let setReadOnlyResult = pm.setReadOnly()
    if not setReadOnlyResult.isOk:
      return setReadOnlyResult

    return ok(CasError)

  except Exception as e:
    # Ensure permissions restored even on error
    try:
      setFilePermissions(pm.casPath, oldPerms)
      pm.logOperation("RESTORE_PERMS_AFTER_ERROR", pm.casPath)
    except:
      discard  # Best effort to restore

    return VoidResult[CasError](isOk: false, errValue: CasError(
      code: UnknownError,
      msg: "Write operation failed: " & e.msg
    ))

proc ensureReadOnly*(pm: ProtectionManager): VoidResult[CasError] =
  ## Ensure CAS directory is in read-only state
  ## This should be called during initialization
  return pm.setReadOnly()

proc verifyReadOnly*(pm: ProtectionManager): bool =
  ## Verify that CAS directory is in read-only state
  try:
    let perms = getFilePermissions(pm.casPath)
    # Check that write permission is not set for user
    return fpUserWrite notin perms
  except:
    return false


# Merkle Integrity Verification
# This is the PRIMARY security mechanism (not chmod)

type
  IntegrityViolation* = object of CatchableError
    hash*: string
    expectedHash*: string
    chunkPath*: string

  SecurityEvent* = object
    timestamp*: DateTime
    eventType*: string
    hash*: string
    details*: string
    severity*: string  # "info", "warning", "critical"

proc logSecurityEvent*(pm: ProtectionManager, event: SecurityEvent) =
  ## Log security events (integrity violations, tampering attempts, etc.)
  try:
    let timestamp = event.timestamp.format("yyyy-MM-dd'T'HH:mm:ss'Z'")
    let logEntry = "[" & timestamp & "] SECURITY_EVENT type=" & event.eventType &
                   " severity=" & event.severity & " hash=" & event.hash &
                   " details=" & event.details & "\n"

    let logFile = open(pm.auditLog, fmAppend)
    logFile.write(logEntry)
    logFile.close()
  except IOError:
    # If we can't write to audit log, at least try stderr
    stderr.writeLine("SECURITY EVENT: " & event.eventType & " - " & event.details)

proc verifyChunkIntegrity*(pm: ProtectionManager, data: seq[byte], expectedHash: string): VoidResult[CasError] =
  ## Verify chunk integrity by recalculating hash
  ## This is the PRIMARY security mechanism - always verify before use
  try:
    let calculatedHash = "xxh3-" & $XXH3_128bits(cast[string](data))

    if calculatedHash != expectedHash:
      # CRITICAL: Hash mismatch detected!
      let event = SecurityEvent(
        timestamp: now(),
        eventType: "INTEGRITY_VIOLATION",
        hash: expectedHash,
        details: "Hash mismatch: expected=" & expectedHash & " calculated=" & calculatedHash,
        severity: "critical"
      )
      pm.logSecurityEvent(event)

      return VoidResult[CasError](isOk: false, errValue: CasError(
        code: UnknownError,
        objectHash: expectedHash,
        msg: "Chunk integrity violation detected! Expected: " & expectedHash &
             ", Got: " & calculatedHash & ". This chunk may be corrupted or tampered with."
      ))

    # Hash matches - integrity verified
    let event = SecurityEvent(
      timestamp: now(),
      eventType: "INTEGRITY_VERIFIED",
      hash: expectedHash,
      details: "Chunk integrity verified successfully",
      severity: "info"
    )
    pm.logSecurityEvent(event)

    return ok(CasError)

  except Exception as e:
    return VoidResult[CasError](isOk: false, errValue: CasError(
      code: UnknownError,
      msg: "Failed to verify chunk integrity: " & e.msg,
      objectHash: expectedHash
    ))

proc verifyChunkIntegrityFromFile*(pm: ProtectionManager, filePath: string, expectedHash: string): VoidResult[CasError] =
  ## Verify chunk integrity by reading file and checking hash
  try:
    let data = readFile(filePath)
    let byteData = data.toOpenArrayByte(0, data.len - 1).toSeq()
    return pm.verifyChunkIntegrity(byteData, expectedHash)
  except IOError as e:
    return VoidResult[CasError](isOk: false, errValue: CasError(
      code: FileReadError,
      msg: "Failed to read chunk file for verification: " & e.msg,
      objectHash: expectedHash
    ))

proc scanCASIntegrity*(pm: ProtectionManager, casPath: string): tuple[verified: int, corrupted: seq[string]] =
  ## Scan entire CAS directory and verify integrity of all chunks
  ## Returns count of verified chunks and list of corrupted chunk hashes
  result.verified = 0
  result.corrupted = @[]

  try:
    let chunksDir = casPath / "chunks"
    if not dirExists(chunksDir):
      return

    for entry in walkDirRec(chunksDir):
      if fileExists(entry):
        # Extract hash from filename
        let filename = extractFilename(entry)
        # Assume format: xxh3-<hash>.zst or just <hash>
        var hash = filename
        if not hash.startsWith("xxh3-"):
          hash = "xxh3-" & hash.replace(".zst", "")

        # Verify integrity
        let verifyResult = pm.verifyChunkIntegrityFromFile(entry, hash)
        if verifyResult.isOk:
          result.verified.inc
        else:
          result.corrupted.add(hash)

          # Log corruption
          let event = SecurityEvent(
            timestamp: now(),
            eventType: "CORRUPTION_DETECTED",
            hash: hash,
            details: "Chunk failed integrity check during scan",
            severity: "critical"
          )
          pm.logSecurityEvent(event)
  except Exception as e:
    stderr.writeLine("Error during CAS integrity scan: " & e.msg)
