# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Manage the operating system type search tree."""

import os
import platform

# The dependency table.  The first item of each tuple is an ostype,
# the second item is the fallback ostype if no match is found for the
# first item.  The table is probed for the fallback's fallback, etc.,
# until None is reached.  The order of the dependency table is
# important: this is the order in which the types are shown in error
# messages.  The order does not affect the probe sequence.

# XXX Beware!!!  An extended version of this table occurs in the
# server code as com/es/ecs/bo/os/OSDictionary.java.  When you add a
# type here, it must also be added there.  The reverse is not
# necessarily true; some os types can be observed on the network but
# we don't currently support agents running there.  Of course this may
# change over time.  In the Java code, the boolean 'hasFuelSupport' is
# true for types that are present in both places, and false for types
# that are only present there.

ostypedeps_raw = [

    ("ubuntu", "linux"),
    ("debian", "linux"),
    ("centos", "linux"),
    ("redhat", "linux"),

    ("linux", "unix"),

    ("sol10", "solaris"),
    ("sol9", "solaris"),
    ("sol8", "solaris"),
    ("solaris", "unix"),

    ("hpux11", "hpux"),
    ("hpux10", "hpux"),
    ("hpux", "unix"),

    ("aix3", "aix"),
    ("aix2", "aix"),
    ("aix", "unix"),

    ("osx4", "osx"),
    ("osx3", "osx"),
    ("osx2", "osx"),
    ("osx", "unix"),

    ("unix", "any"),

    ("winxp", "windows"),

    ("win2003s", "windows"),

    ("win2ks", "win2k"),

    ("win2kws", "win2k"),
    ("win2k", "windows"),

    ("winvista", "windows"),
    ("win2k8", "windows"),
    ("win2k8r2", "windows"),
    ("win7", "windows"),
    ("win8", "windows"),
    ("win8.1", "windows"),
    ("win2012", "windows"),
    ("win2012r2", "windows"),
    ("win10", "windows"),
    ("win2016", "windows"),
    ("win2019", "windows"),
    ("win11", "windows"),

    ("windows", "any"),

    # Cloud control-plane (CSPM) ostypes.  These are never returned by
    # guess_ostype() -- they are forced by the cloud agent (cloud_engine.py)
    # so that find_best_implementation() routes AWS fuel rules
    # (<implementation ostypes="aws">) via aws -> cloud -> any.  Adding these
    # is purely additive; on-host OS detection is unaffected.  NOTE: as with
    # every other type here, the server's OSDictionary.java must learn "aws"
    # and "cloud" (hasFuelSupport=true) before the server can assign cloud
    # directives by OS; running espaws as a local directive needs only this.
    ("aws", "cloud"),
    ("cloud", "any"),

    ("any", None),   # "any" is here so it gets shown in error messages
    ]

ostypedeps = dict(ostypedeps_raw)   # Table used by actual search algorithm

def validtypes():
    """Return a list of the valid ostype values, in a sensible order."""
    return [x for x, y in ostypedeps_raw]

def isvalid(ostype):
    """Return whether the argument is a valid ostype value."""
    return ostype in ostypedeps

def isancestor(a, d):
    """Returns whether a is an ancestor of d (possibly itself)."""
    assert isvalid(a), repr(a)
    assert isvalid(d), repr(d)
    while a != d:
        d = ostypedeps.get(d)
        if d is None:
            return False
    return True

def allancestors(ostype):
    """Return a list of all ancestors of ostype, including itself."""
    assert isvalid(ostype), repr(ostype)
    result = []
    while ostype is not None:
        result.append(ostype)
        ostype = ostypedeps.get(ostype)
    return result

def alldescendants(ostype):
    """Return a list of all descendants of osttype, including itself."""
    result = []
    for ost in validtypes():
        if isancestor(ostype, ost):
            result.append(ost)
    return result

def guess_ostype():
    ostype = platform.system().lower().replace("-", "") # HP-UX -> hpux
    if ostype == "linux":
        dist = ""
        try:
            import distro
            dist = distro.id()
        except ImportError:
            try:
                with open("/etc/os-release") as f:
                    for line in f:
                        if line.startswith("ID="):
                            dist = line.strip().split("=", 1)[1].strip('"')
                            break
            except (IOError, OSError):
                pass
        if dist in ("debian", "ubuntu", "redhat", "centos"):
            ostype = dist.lower()
        # XXX Other distros?
    elif ostype == "sunos":
        ostype = "solaris"
        rel = platform.release()
        if rel == "5.10":
            ostype = "sol10"
        elif rel == "5.9":
            ostype = "sol9"
        elif rel == "5.8":
            ostype = "sol8"
    elif ostype in ("windows", "microsoft", "microsoft windows"):
        ostype = "windows"
        rel = platform.release().lower()
        if rel == "xp":
            ostype = "winxp"
        elif rel == "2003server":
            ostype = "win2003s"
        elif rel == "2000":
            import winreg as _winreg
            # ostype = "win2k"
            try:
                rootkey = _winreg.OpenKey(getattr(_winreg, "HKEY_LOCAL_MACHINE"),
                                          r"System\CurrentControlSet\Control\ProductOptions")
                wintype = ""
                try:
                    nsubkeys, nvalues, mtime = _winreg.QueryInfoKey(rootkey)
                    for i in range(nvalues):
                        nm, val, typ = _winreg.EnumValue(rootkey, i)
                        if nm.lower() == "producttype":
                            wintype = val
                            break
                finally:
                    _winreg.CloseKey(rootkey)
                if wintype.lower() == "servernt" or wintype.lower() == "lanmannt":
                    ostype = "win2ks"
                elif wintype.lower() == "winnt":
                    ostype = "win2kws"
            except OSError as err:
                pass
        else:
            import sys
            major, minor, build, platForm, servicePack = sys.getwindowsversion()
            if major >= 6:
                import winreg as _winreg
                isServer = False
                currentMajorVersion = None
                fullversion = ""
                currentBuild = None
                try:
                    rootkey = _winreg.OpenKey(getattr(_winreg, "HKEY_LOCAL_MACHINE"),
                        r"Software\Microsoft\Windows NT\CurrentVersion")
                    try:
                        nsubkeys, nvalues, mtime = _winreg.QueryInfoKey(rootkey)
                        for i in range(nvalues):
                            nm, val, typ = _winreg.EnumValue(rootkey, i)
                            if nm.lower() == "productname":
                                fullversion = val
                                if val.lower().find("server") >= 0: 
                                    isServer = True
                            elif nm.lower() == "currentmajorversionnumber":
                                currentMajorVersion = val
                            elif nm.lower() == "currentbuildnumber":
                                currentBuild = int(val)
                            elif nm.lower() == "currentversion":
                                if val == "6.1":
                                    major = 6
                                    minor = 1
                                elif val == "6.2":
                                    major = 6
                                    minor = 2
                                elif val == "6.3":
                                    major = 6
                                    minor = 3
                    finally:
                        _winreg.CloseKey(rootkey)
                except OSError as err:
                    pass
                if currentMajorVersion == 10:
                   if isServer:
                       if "2019" in fullversion:
                           ostype = "win2019"
                       else:
                           ostype = 'win2016'
                   else:
                       if currentBuild >= 22000:
                           ostype = "win11"
                       else:
                           ostype = 'win10'

                elif major == 6:
                    if minor == 0:
                       if isServer:
                           ostype = 'win2k8'
                       else:
                           ostype = 'winvista'
                    elif minor == 1:
                       if isServer:
                           ostype = 'win2k8r2'
                       else:
                           ostype = 'win7'
                    elif minor == 2:
                       if isServer:
                           if fullversion.lower().find("r2") >= 0:
                               ostype = 'win2012r2'
                           else:
                               ostype = 'win2012'
                       else:
                           ostype = 'win8'
                    elif minor == 3:
                       if isServer:
                           ostype = 'win2012r2'
                       else:
                           ostype = 'win8.1'
    elif ostype == "darwin":
        ostype = "osx"
        rel = platform.release().split(".")[0]
        if rel == "6":
            ostype = "osx2"
        elif rel == "7":
            ostype = "osx3"
        elif rel == "8":
            ostype = "osx4"
    elif ostype == "aix":
        rel = platform.release()
        if rel == "2":
            ostype = "aix2"
        elif rel == "3":
            ostype = "aix3"
    elif ostype == "hpux":
        rel = platform.release().split(".")[1]
        if rel == "10":
            ostype = "hpux10"
        elif rel == "11":
            ostype = "hpux11"
    elif not isvalid(ostype):
        # Unknown type -- fall back to Unix until we know more
        if os.sep == "/":
            ostype = "unix"
    assert isvalid(ostype)
    return ostype
