#!/usr/bin/env python3
"""
keystone_policy_compare.py -- offline policy-artifact helper for
scripts/keystone-policy-drift.sh (hardening register H4; the D-051/D-064/
D-065/D-073 keystone policy family). Read-only; no network, no juju.

Modes:
  parse <file.yaml>
      Validate the overlay parses as a FLAT string->string rule mapping.
      Prints "rules=<N>". Exit 0 ok | 2 unreadable/bad shape.
  zipcheck <file.zip> <file.yaml>
      Byte-compare the zip member domain-manager-policy.yaml against the
      on-disk yaml (same check as provider-bundle-check item 7 -- mtimes
      inside the zip cannot false-fail a content compare).
      Exit 0 match | 1 differs / member missing | 2 unreadable.
  compare <repo.yaml> <live.yaml>
      Compare PARSED rule sets. The keystone charm strips comments in the
      policyd round-trip, so bytes/sizes NEVER match between the repo file
      and /etc/keystone/policy.d/ -- only parsed rules may be compared.
      Prints per-rule DIFF / REPO-ONLY / LIVE-ONLY lines when not identical.
      Exit 0 identical
           3 staged-D-073: live == repo minus EXACTLY identity:list_trusts
             held at the adopted D-073 value (repo ahead of live by the
             staged delta; live apply pending in the gated attach window)
           1 any other difference
           2 unreadable / bad shape.

ASCII + LF.
"""
import sys
import zipfile

import yaml

MEMBER = "domain-manager-policy.yaml"
D073_RULE = "identity:list_trusts"
D073_VALUE = "rule:admin_required or (role:reader and system_scope:all)"


def load_rules(path):
    try:
        with open(path, "r") as f:
            d = yaml.safe_load(f)
    except Exception as e:  # noqa: BLE001 -- report, do not crash
        print("UNREADABLE %s: %s" % (path, e))
        return None
    if not isinstance(d, dict) or not all(
        isinstance(k, str) and isinstance(v, str) for k, v in d.items()
    ):
        print("BAD-SHAPE %s: not a flat string->string rule mapping" % path)
        return None
    return d


def cmd_parse(path):
    d = load_rules(path)
    if d is None:
        return 2
    print("rules=%d" % len(d))
    return 0


def cmd_zipcheck(zpath, ypath):
    try:
        with zipfile.ZipFile(zpath) as z:
            names = z.namelist()
            if MEMBER not in names:
                print("zip lacks top-level %s (members: %s)" % (MEMBER, names))
                return 1
            inzip = z.read(MEMBER)
        with open(ypath, "rb") as f:
            ondisk = f.read()
    except Exception as e:  # noqa: BLE001
        print("UNREADABLE: %s" % e)
        return 2
    if inzip == ondisk:
        print("zip member %s matches %s byte-for-byte" % (MEMBER, ypath))
        return 0
    print("zip content DIFFERS from %s (rebuild + recommit the zip)" % ypath)
    return 1


def cmd_compare(rpath, lpath):
    repo = load_rules(rpath)
    live = load_rules(lpath)
    if repo is None or live is None:
        return 2
    if repo == live:
        print("parsed rule sets IDENTICAL (%d rules)" % len(repo))
        return 0
    repo_only = sorted(set(repo) - set(live))
    live_only = sorted(set(live) - set(repo))
    changed = sorted(k for k in set(repo) & set(live) if repo[k] != live[k])
    if (
        repo_only == [D073_RULE]
        and not live_only
        and not changed
        and repo[D073_RULE] == D073_VALUE
    ):
        print(
            "STAGED-D-073: live lacks exactly %s at the adopted value "
            "(%d live vs %d repo rules); live apply pending" % (D073_RULE, len(live), len(repo))
        )
        return 3
    for k in changed:
        print("DIFF rule %s:" % k)
        print("  repo: %s" % repo[k])
        print("  live: %s" % live[k])
    for k in repo_only:
        print("REPO-ONLY rule %s = %s" % (k, repo[k]))
    for k in live_only:
        print("LIVE-ONLY rule %s = %s" % (k, live[k]))
    return 1


def main(argv):
    if len(argv) == 2 and argv[0] == "parse":
        return cmd_parse(argv[1])
    if len(argv) == 3 and argv[0] == "zipcheck":
        return cmd_zipcheck(argv[1], argv[2])
    if len(argv) == 3 and argv[0] == "compare":
        return cmd_compare(argv[1], argv[2])
    sys.stderr.write(__doc__)
    return 2


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
