"""Python mirror of the GE-Enforce engine's targeting filters.

These re-implement, field for field, the four filter functions in
`Install-FromManifest.ps1` (Test-PCTypeMatches, Test-HostnameMatches,
Test-MachineNumberMatches, Test-CmmVersionMatches) plus the PCTypes alias graph
(lines 463-475). They operate on plain manifest-entry dicts (the exact shape of
an Applications[] entry) and a machine-profile dict, so the SAME code answers
two questions:

  1. Parity harness: do the on-share manifest and the shopdb-rebuilt manifest
     select the same entries in the same order for a set of machine profiles?
  2. Simulator: "what would PC Y get" - which entries apply and why.

The engine lib stays the single source of truth; this is a mirror. If the lib's
alias map changes, ALIAS_GROUPS here must change with it (the parity harness's
legacy-name profiles fail loudly on divergence).
"""

import fnmatch

# Mirror of $script:_pcTypeAliasGroups in Install-FromManifest.ps1:463-475.
# Rows are alias sets: names in the same set all match the same identity.
ALIAS_GROUPS = [
    ('Standard', 'gea-shopfloor-collections', 'gea-shopfloor-nocollections',
     'gea-shopfloor-common'),
    ('Standard-Machine', 'gea-shopfloor-collections', 'gea-shopfloor-nocollections'),
    ('Standard-Timeclock', 'gea-shopfloor-common'),
    ('CMM', 'gea-shopfloor-cmm'),
    ('Keyence', 'gea-shopfloor-keyence'),
    ('Lab', 'gea-shopfloor-common'),
    ('WaxAndTrace', 'gea-shopfloor-waxtrace'),
    ('Genspect', 'gea-shopfloor-genspect'),
    ('Display', 'gea-shopfloor-display'),
    ('Heattreat', 'gea-shopfloor-heattreat'),
    ('PartMarker', 'gea-shopfloor-partmarker'),
]


def _alias_sets(name):
    """Every alias group containing `name` (case-insensitive)."""
    lname = (name or '').lower()
    return [g for g in ALIAS_GROUPS if any(n.lower() == lname for n in g)]


def matches_pctype(entry, pctype, subtype=None, strict_allowed=False):
    """Test-PCTypeMatches: no PCTypes = all; '*' = all; alias-set intersection.

    PCTypesStrict=true disables alias expansion (both the PC's identity and the
    manifest values are compared literally), so a 'collections'-only strict entry
    does NOT match a 'nocollections' PC via their shared 'Standard' alias group.
    IMPORTANT: only the PREINSTALL runner honors this flag - the runtime engine
    (Install-FromManifest.ps1's Test-PCTypeMatches) has no strict handling. So
    strict is applied only when strict_allowed (i.e. phase == 'preinstall'),
    else a runtime entry with PCTypesStrict still alias-expands, matching the
    real engine.
    """
    values = entry.get('PCTypes') or []
    if not values:
        return True
    if not pctype:
        return True
    strict = strict_allowed and bool(entry.get('PCTypesStrict'))
    # Names the current PC matches: bare type, "type-subtype", and (unless strict)
    # all aliases of either.
    mynames = set()
    mynames.add(pctype.lower())
    seeds = [pctype]
    if subtype:
        combined = f'{pctype}-{subtype}'
        mynames.add(combined.lower())
        seeds.append(combined)
    if not strict:
        for seed in seeds:
            for group in _alias_sets(seed):
                for alias in group:
                    mynames.add(alias.lower())
    for value in values:
        if value == '*':
            return True
        if value.lower() in mynames:
            return True
        if strict:
            continue
        # The manifest value may itself be an alias - expand and check overlap.
        for group in _alias_sets(value):
            for alias in group:
                if alias.lower() in mynames:
                    return True
    return False


def matches_hostname(entry, hostname):
    """Test-HostnameMatches: exact or PowerShell -like glob (*, ?, [set])."""
    patterns = entry.get('TargetHostnames') or []
    if not patterns:
        return True
    myname = (hostname or '').lower()
    for pattern in patterns:
        lpattern = (pattern or '').lower()
        if lpattern == myname:
            return True
        if fnmatch.fnmatch(myname, lpattern):
            return True
    return False


def matches_machinenumber(entry, machinenumber):
    """Test-MachineNumberMatches: filter present but PC has no number = exclude."""
    numbers = entry.get('TargetMachineNumbers') or []
    if not numbers:
        return True
    if not machinenumber:
        return False
    mynumber = str(machinenumber).lower()
    return any(str(n).lower() == mynumber for n in numbers)


def matches_cmmversion(entry, cmmversion):
    """Test-CmmVersionMatches: untagged always applies; no PC version = install-all."""
    tag = entry.get('_CmmVersion')
    if not tag:
        return True
    if not cmmversion:
        return True
    return str(tag).lower() == str(cmmversion).lower()


def entry_applies(entry, profile):
    """All four filters ANDed, exactly as the engine's main loop applies them.

    `profile` keys: pctype, subtype, hostname, machinenumber, cmmversion, and
    optional phase ('preinstall' enables PCTypesStrict, matching the runners).
    """
    strict_allowed = profile.get('phase') == 'preinstall'
    return (matches_pctype(entry, profile.get('pctype'), profile.get('subtype'),
                           strict_allowed)
            and matches_hostname(entry, profile.get('hostname'))
            and matches_machinenumber(entry, profile.get('machinenumber'))
            and matches_cmmversion(entry, profile.get('cmmversion')))


def applicable_entry_names(applications, profile):
    """Ordered list of entry Names that pass all filters for a profile."""
    return [e.get('Name') for e in applications if entry_applies(e, profile)]
