#!/usr/bin/env python3
"""Check a memory store against the Vercy Governance Overlay.

  python check.py store.jsonl
  python check.py store.json --profile profile.yaml

The input is a JSON array, or one JSON object per line, where each object is one record.
Field names may be your own: pass --map to say which of your keys plays each overlay role.

  python check.py store.jsonl --map valid_from=valid_at,valid_to=invalid_at,record_id=uuid

The checker reports the highest level the store satisfies and, for every requirement it
fails, the records that caused the failure. It refuses to guess: a level is satisfied only
when every record that the requirement applies to carries the field.

Exit status is 0 when the store reaches at least level 1, 1 otherwise, so it can be used
in continuous integration.
"""
import argparse
import json
import sys

LEVELS = {
    1: ["record_id", "valid_from", "valid_to"],
    2: ["source", "concept_owner", "conflict_policy"],
    3: ["release_to"],
}
SHOULD = ["owner_role", "supersedes"]
# required only on records that state a rule, and only where the store has such records
RULE_FIELDS = ["applies_to", "does_not_apply_to"]
# a record is taken to state a rule when it carries any of these
RULE_MARKERS = ["conflict_policy", "policy", "rule", "applies_to", "does_not_apply_to"]
# a record is taken to be restricted when it names an audience
RESTRICTED_MARKERS = ["release_to", "classification", "confidential", "restricted"]


def load(path):
    text = open(path, encoding="utf-8").read().strip()
    if not text:
        return []
    if text[0] == "[":
        return json.loads(text)
    return [json.loads(line) for line in text.splitlines() if line.strip()]


def parse_map(spec):
    out = {}
    for pair in (spec or "").split(","):
        if not pair.strip():
            continue
        role, _, key = pair.partition("=")
        out[role.strip()] = key.strip()
    return out


# For these, an explicit null is a statement rather than a gap: a null valid_to means the
# record is still open, and a store that omits the key cannot say whether it knows.
NULLABLE = {"valid_to"}


def present(record, field, mapping):
    return mapping.get(field, field) in record


def get(record, field, mapping):
    key = mapping.get(field, field)
    value = record.get(key)
    if value is None:
        return None
    if isinstance(value, str) and not value.strip():
        return None
    if isinstance(value, (list, dict)) and not value:
        return None
    return value


def rid(record, mapping, index):
    return get(record, "record_id", mapping) or f"#{index}"


def states_a_rule(record, mapping):
    return any(get(record, m, mapping) is not None for m in RULE_MARKERS)


def is_restricted(record, mapping):
    return any(get(record, m, mapping) is not None for m in RESTRICTED_MARKERS)


def check(records, mapping, policy_present):
    """Returns (level reached, findings). A finding is (field, requirement, offenders)."""
    findings = []
    reached = 0

    for level in (1, 2, 3):
        ok = True
        for field in LEVELS[level]:
            if field == "conflict_policy":
                # one policy per store, not one per record
                if not policy_present and not any(
                        get(r, "conflict_policy", mapping) for r in records):
                    findings.append((field, f"level {level} MUST",
                                     ["no conflict policy anywhere in the store"]))
                    ok = False
                continue
            if field == "release_to":
                restricted = [(i, r) for i, r in enumerate(records)
                              if is_restricted(r, mapping)]
                if not restricted:
                    continue          # nothing restricted, the requirement is vacuous
                missing = [rid(r, mapping, i) for i, r in restricted
                           if get(r, "release_to", mapping) is None]
                if missing:
                    findings.append((field, f"level {level} MUST where restricted",
                                     missing))
                    ok = False
                continue
            if field in NULLABLE:
                missing = [rid(r, mapping, i) for i, r in enumerate(records)
                           if not present(r, field, mapping)]
                note = f"level {level} MUST be present, null means open"
            else:
                missing = [rid(r, mapping, i) for i, r in enumerate(records)
                           if get(r, field, mapping) is None]
                note = f"level {level} MUST"
            if missing:
                findings.append((field, note, missing))
                ok = False
        if not ok:
            break
        reached = level

    rules = [(i, r) for i, r in enumerate(records) if states_a_rule(r, mapping)]
    for field in RULE_FIELDS:
        missing = [rid(r, mapping, i) for i, r in rules
                   if get(r, field, mapping) is None]
        if missing:
            findings.append((field, "MUST on any record that states a rule", missing))
            if reached >= 2:
                reached = 1

    for field in SHOULD:
        missing = [rid(r, mapping, i) for i, r in enumerate(records)
                   if get(r, field, mapping) is None]
        if missing:
            findings.append((field, "SHOULD", missing))
    return reached, findings


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("store")
    ap.add_argument("--map", default="", help="role=yourkey,role=yourkey")
    ap.add_argument("--policy", action="store_true",
                    help="the conflict policy is configured outside the records")
    ap.add_argument("--quiet", action="store_true")
    a = ap.parse_args()

    records = load(a.store)
    if not records:
        print("no records found")
        return 1
    mapping = parse_map(a.map)
    level, findings = check(records, mapping, a.policy)

    print(f"records: {len(records)}")
    print(f"level reached: {level} of 3")
    if not a.quiet:
        for field, requirement, offenders in findings:
            shown = ", ".join(str(x) for x in offenders[:6])
            more = f" and {len(offenders) - 6} more" if len(offenders) > 6 else ""
            print(f"  {field:20} {requirement:40} {shown}{more}")
    if level == 0:
        print("\nLevel 1 needs record_id, valid_from and valid_to on every record.")
    return 0 if level >= 1 else 1


if __name__ == "__main__":
    sys.exit(main())
