# SPDX-License-Identifier: LSL-1.0
# Copyright (c) 2026 Markus Maiwald
# Stewardship: Self Sovereign Society Foundation
#
# This file is part of the Nexus Sovereign Core.
# See legal/LICENSE_SOVEREIGN.md for license terms.

## variant_profiles.nim
## Profile system for NIP variant management
## Loads and merges variant profiles from KDL files

import std/[tables, options, strformat, strutils]
import kdl_parser
import variant_types
import config

type
  ProfileError* = object of CatchableError
  ProfileParseError* = object of ProfileError
  ProfileMergeError* = object of ProfileError

# #############################################################################
# Profile Loading
# #############################################################################

proc loadProfile*(path: string): VariantProfile =
  ## Load a variant profile from a KDL file
  ##
  ## Expected KDL format:
  ## ```kdl
  ## profile "fleet-node" {
  ##     description "Optimized for fleet deployment"
  ##
  ##     domains {
  ##         init "dinit"
  ##         runtime "musl" "systemd-shim"
  ##         graphics "headless"
  ##         optimization "lto" "strip"
  ##         security "pie" "relro"
  ##     }
  ##
  ##     compiler {
  ##         CFLAGS "-O3 -flto"
  ##         LDFLAGS "-Wl,-O1"
  ##         MAKEFLAGS "-j8"
  ##     }
  ## }
  ## ```

  try:
    let doc = parseKdlFile(path)

    # Find the profile node
    let profileNodeOpt = doc.findNode("profile")
    if profileNodeOpt.isNone:
      raise newException(ProfileParseError, "No 'profile' node found in " & path)

    let profileNode = profileNodeOpt.get

    # Get profile name from argument
    var profileName = ""
    if profileNode.args.len > 0:
      # Verify the first argument is a string
      if profileNode.args[0].isString:
        profileName = profileNode.getArgString(0)
      else:
        raise newException(ProfileParseError, "Profile name must be a string literal")
    else:
      raise newException(ProfileParseError, "Profile node must have a name argument")

    result = newVariantProfile(profileName, "")

    # Parse description
    let descNode = profileNode.findChild("description")
    if descNode.isSome and descNode.get.args.len > 0:
      # Verify the description argument is a string
      if descNode.get.args[0].isString:
        result.description = descNode.get.getArgString(0)
      else:
        # Skip invalid description rather than failing
        discard

    # Parse domains
    let domainsNode = profileNode.findChild("domains")
    if domainsNode.isSome:
      for domainNode in domainsNode.get.children:
        let domainName = domainNode.name
        var values: seq[string] = @[]

        # Collect all arguments as domain values
        for arg in domainNode.args:
          if arg.isString:
            values.add(arg.getString())

        if values.len > 0:
          result.domains[domainName] = values

    # Parse compiler flags
    let compilerNode = profileNode.findChild("compiler")
    if compilerNode.isSome:
      for flagNode in compilerNode.get.children:
        let flagName = flagNode.name
        if flagNode.args.len > 0:
          # Verify the flag value argument is a string
          if not flagNode.args[0].isString:
            raise newException(ProfileParseError,
              fmt"Compiler flag '{flagName}' value must be a string literal")

          let flagValue = flagNode.getArgString(0)

          case flagName
          of "CFLAGS", "cflags":
            result.compilerFlags.cflags = flagValue
          of "CXXFLAGS", "cxxflags":
            result.compilerFlags.cxxflags = flagValue
          of "LDFLAGS", "ldflags":
            result.compilerFlags.ldflags = flagValue
          of "MAKEFLAGS", "makeflags":
            result.compilerFlags.makeflags = flagValue
          else:
            # Ignore RUSTFLAGS, GOFLAGS and other unsupported flags for now
            discard

    return result

  except KdlError as e:
    raise newException(ProfileParseError, fmt"KDL parse error in {path}: {e.msg}")
  except IOError as e:
    raise newException(ProfileParseError, fmt"Failed to read profile {path}: {e.msg}")
  except KeyError as e:
    raise newException(ProfileParseError, fmt"Missing required field in {path}: {e.msg}")

# #############################################################################
# Profile Merging
# #############################################################################

proc mergeProfileWithOverrides*(
  profile: VariantProfile,
  overrides: Table[string, seq[string]],
  compilerOverrides: Option[CompilerFlags] = none(CompilerFlags)
): VariantProfile =
  ## Merge a profile with user-specified overrides
  ## User overrides take precedence over profile defaults

  result = profile

  # Merge domain overrides
  for domain, values in overrides:
    result.domains[domain] = values

  # Merge compiler flag overrides
  if compilerOverrides.isSome:
    let overrideFlags = compilerOverrides.get

    if overrideFlags.cflags.len > 0:
      result.compilerFlags.cflags = overrideFlags.cflags
    if overrideFlags.cxxflags.len > 0:
      result.compilerFlags.cxxflags = overrideFlags.cxxflags
    if overrideFlags.ldflags.len > 0:
      result.compilerFlags.ldflags = overrideFlags.ldflags
    if overrideFlags.makeflags.len > 0:
      result.compilerFlags.makeflags = overrideFlags.makeflags

proc mergeProfiles*(base: VariantProfile, overlay: VariantProfile): VariantProfile =
  ## Merge two profiles, with overlay taking precedence
  result = base
  result.name = overlay.name
  result.description = overlay.description

  # Merge domains (overlay wins)
  for domain, values in overlay.domains:
    result.domains[domain] = values

  # Merge compiler flags (overlay wins for non-empty values)
  if overlay.compilerFlags.cflags.len > 0:
    result.compilerFlags.cflags = overlay.compilerFlags.cflags
  if overlay.compilerFlags.cxxflags.len > 0:
    result.compilerFlags.cxxflags = overlay.compilerFlags.cxxflags
  if overlay.compilerFlags.ldflags.len > 0:
    result.compilerFlags.ldflags = overlay.compilerFlags.ldflags
  if overlay.compilerFlags.makeflags.len > 0:
    result.compilerFlags.makeflags = overlay.compilerFlags.makeflags

# #############################################################################
# Profile Utilities
# #############################################################################

proc `$`*(profile: VariantProfile): string =
  ## String representation of a profile
  result = fmt"Profile: {profile.name}"
  if profile.description.len > 0:
    result.add(fmt" - {profile.description}")
  result.add("\n")

  if profile.domains.len > 0:
    result.add("  Domains:\n")
    for domain, values in profile.domains:
      result.add("    " & domain & ": " & values.join(", ") & "\n")

  result.add("  Compiler Flags:\n")
  if profile.compilerFlags.cflags.len > 0:
    result.add("    CFLAGS: " & profile.compilerFlags.cflags & "\n")
  if profile.compilerFlags.ldflags.len > 0:
    result.add("    LDFLAGS: " & profile.compilerFlags.ldflags & "\n")
  if profile.compilerFlags.makeflags.len > 0:
    result.add("    MAKEFLAGS: " & profile.compilerFlags.makeflags & "\n")

proc getDomainValues*(profile: VariantProfile, domain: string): seq[string] =
  ## Get values for a specific domain, returns empty seq if not found
  if profile.domains.hasKey(domain):
    return profile.domains[domain]
  return @[]

proc hasDomain*(profile: VariantProfile, domain: string): bool =
  ## Check if profile has configuration for a domain
  profile.domains.hasKey(domain)

proc listDomains*(profile: VariantProfile): seq[string] =
  ## List all configured domains in the profile
  result = @[]
  for domain in profile.domains.keys:
    result.add(domain)
