## variant_validator.nim
## Domain validation system for NIP variant management
## Validates domain configurations and enforces type constraints

import std/[tables, strutils]
import variant_types
import variant_domains

# #############################################################################
# Error Types
# #############################################################################

type
  VariantError* = object of CatchableError
    ## Base error type for variant system errors

  DomainValidationError* = object of VariantError
    ## Error when domain or value is invalid
    domain*: string
    invalidValue*: string
    validOptions*: seq[string]

  ConflictError* = object of VariantError
    ## Error when conflicting flags are specified
    domain*: string
    conflictingFlags*: seq[string]
    reason*: string

  FingerprintCollisionError* = object of VariantError
    ## Error when different configs produce same fingerprint (extremely rare)
    existingPath*: string
    newConfig*: string

# #############################################################################
# Validation Result Type
# #############################################################################

type
  ValidationResult*[T] = object
    case isOk*: bool
    of true:
      value*: T
    of false:
      error*: ref VariantError

proc ok*[T](val: T): ValidationResult[T] =
  ## Create a successful validation result
  ValidationResult[T](isOk: true, value: val)

proc err*[T](error: ref VariantError): ValidationResult[T] =
  ## Create a failed validation result
  ValidationResult[T](isOk: false, error: error)

# #############################################################################
# Domain Validation
# #############################################################################

proc validateDomainExists*(domain: string): ValidationResult[bool] =
  ## Validate that a domain exists
  if not hasDomain(domain):
    let validDomains = getAllDomainNames()
    var error = new(DomainValidationError)
    error.msg = "Unknown domain: '" & domain & "'. Valid domains: " & validDomains.join(", ")
    error.domain = domain
    error.invalidValue = ""
    error.validOptions = validDomains
    return err[bool](error)

  return ok(true)

proc validateDomainValue*(domain: string, value: string): ValidationResult[bool] =
  ## Validate that a value is valid for a domain
  if not hasDomain(domain):
    var error = new(DomainValidationError)
    error.msg = "Unknown domain: '" & domain & "'"
    error.domain = domain
    error.invalidValue = value
    return err[bool](error)

  if not isValidDomainValue(domain, value):
    let validOptions = getDomainOptions(domain)
    var error = new(DomainValidationError)
    error.msg = "Invalid value '" & value & "' for domain '" & domain & "'. Valid options: " & validOptions.join(", ")
    error.domain = domain
    error.invalidValue = value
    error.validOptions = validOptions
    return err[bool](error)

  return ok(true)


proc validateExclusiveConstraint*(domain: string, values: seq[string]): ValidationResult[bool] =
  ## Validate that exclusive domains only have one value
  if not hasDomain(domain):
    var error = new(DomainValidationError)
    error.msg = "Unknown domain: '" & domain & "'"
    error.domain = domain
    return err[bool](error)

  if isExclusiveDomain(domain) and values.len > 1:
    var error = new(ConflictError)
    error.msg = "Domain '" & domain & "' is exclusive but multiple values were specified: " & values.join(", ")
    error.domain = domain
    error.conflictingFlags = values
    error.reason = "exclusive domain allows only one value"
    return err[bool](error)

  return ok(true)

proc validateDomainConfig*(domains: Table[string, seq[string]]): ValidationResult[bool] =
  ## Validate complete domain configuration
  ## Checks:
  ## - All domains exist
  ## - All values are valid for their domains
  ## - Exclusive constraints are respected

  for domain, values in domains:
    # Check domain exists
    let domainCheck = validateDomainExists(domain)
    if not domainCheck.isOk:
      return err[bool](domainCheck.error)

    # Check exclusive constraint
    let exclusiveCheck = validateExclusiveConstraint(domain, values)
    if not exclusiveCheck.isOk:
      return err[bool](exclusiveCheck.error)

    # Check all values are valid
    for value in values:
      let valueCheck = validateDomainValue(domain, value)
      if not valueCheck.isOk:
        return err[bool](valueCheck.error)

  return ok(true)

# #############################################################################
# Variant Flag Validation
# #############################################################################

proc validateVariantFlag*(flag: VariantFlag): ValidationResult[bool] =
  ## Validate a single variant flag

  # Check domain exists
  let domainCheck = validateDomainExists(flag.domain)
  if not domainCheck.isOk:
    return err[bool](domainCheck.error)

  # Check value is valid
  if flag.value.len > 0:
    # For choice/enum types with explicit values
    let values = flag.value.split(',')
    for val in values:
      let valueCheck = validateDomainValue(flag.domain, val.strip())
      if not valueCheck.isOk:
        return err[bool](valueCheck.error)
  else:
    # For bool types, validate the flag name
    let valueCheck = validateDomainValue(flag.domain, flag.name)
    if not valueCheck.isOk:
      return err[bool](valueCheck.error)

  return ok(true)

proc validateVariantFlags*(flags: seq[VariantFlag]): ValidationResult[bool] =
  ## Validate a sequence of variant flags
  ## Also checks for conflicts within the same domain

  # Group flags by domain
  var domainFlags = initTable[string, seq[string]]()

  for flag in flags:
    if not flag.enabled:
      continue  # Skip disabled flags

    if not domainFlags.hasKey(flag.domain):
      domainFlags[flag.domain] = @[]

    if flag.value.len > 0:
      let values = flag.value.split(',')
      for val in values:
        domainFlags[flag.domain].add(val.strip())
    else:
      domainFlags[flag.domain].add(flag.name)

  # Validate the grouped configuration
  return validateDomainConfig(domainFlags)

# #############################################################################
# Conflict Detection
# #############################################################################

proc detectConflicts*(domains: Table[string, seq[string]]): seq[string] =
  ## Detect potential conflicts in domain configuration
  ## Returns list of conflict descriptions
  result = @[]

  for domain, values in domains:
    if not hasDomain(domain):
      result.add("Unknown domain: " & domain)
      continue

    # Check exclusive constraint
    if isExclusiveDomain(domain) and values.len > 1:
      result.add("Domain '" & domain & "' is exclusive but has multiple values: " & values.join(", "))

    # Check for invalid values
    for value in values:
      if not isValidDomainValue(domain, value):
        result.add("Invalid value '" & value & "' for domain '" & domain & "'")

proc hasConflicts*(domains: Table[string, seq[string]]): bool =
  ## Check if domain configuration has any conflicts
  let conflicts = detectConflicts(domains)
  result = conflicts.len > 0

# #############################################################################
# Validation Helpers
# #############################################################################

proc getValidationErrors*(domains: Table[string, seq[string]]): seq[string] =
  ## Get all validation errors as human-readable strings
  result = detectConflicts(domains)

proc isValidConfiguration*(domains: Table[string, seq[string]]): bool =
  ## Check if a domain configuration is valid
  let validation = validateDomainConfig(domains)
  result = validation.isOk

proc validateOrThrow*(domains: Table[string, seq[string]]) =
  ## Validate domain configuration and raise exception if invalid
  let validation = validateDomainConfig(domains)
  if not validation.isOk:
    raise validation.error

# #############################################################################
# Error Message Formatting
# #############################################################################

proc formatValidationError*(error: ref VariantError): string =
  ## Format a validation error as a human-readable message
  result = error.msg

  if error of DomainValidationError:
    let err = cast[ref DomainValidationError](error)
    if err.validOptions.len > 0:
      result.add("\nValid options: " & err.validOptions.join(", "))
  elif error of ConflictError:
    let err = cast[ref ConflictError](error)
    if err.reason.len > 0:
      result.add("\nReason: " & err.reason)

proc formatValidationErrors*(domains: Table[string, seq[string]]): string =
  ## Format all validation errors for a domain configuration
  let errors = getValidationErrors(domains)
  if errors.len == 0:
    return "No validation errors"

  result = "Validation errors:\n"
  for i, error in errors:
    result.add("  " & $(i+1) & ". " & error & "\n")
