#!/usr/bin/env python3
"""Read-only Docker State classifier. No Docker calls, network or file writes.

Input: one Docker .State JSON object (or a one-element docker inspect array) on stdin.
Output: small structured verdict that never echoes secrets or arbitrary log content.
This is a decision aid, not a guarantee about the source of SIGKILL.
"""
from __future__ import annotations

import argparse
import json
import sys
from typing import Any


def state_from_payload(value: Any) -> dict[str, Any]:
    if isinstance(value, list):
        if len(value) != 1:
            raise ValueError("Expected exactly one container object")
        value = value[0]
    if not isinstance(value, dict):
        raise ValueError("Expected a Docker State JSON object")
    if isinstance(value.get("State"), dict):
        value = value["State"]
    if "ExitCode" not in value:
        raise ValueError("Missing State.ExitCode")
    code = value["ExitCode"]
    if isinstance(code, bool) or not isinstance(code, int):
        raise ValueError("State.ExitCode must be an integer")
    if "OOMKilled" in value and not isinstance(value["OOMKilled"], bool):
        raise ValueError("State.OOMKilled must be boolean when present")
    return value


def classify(state: dict[str, Any]) -> dict[str, Any]:
    code = state["ExitCode"]
    oom = state.get("OOMKilled")
    running = state.get("Running")
    if running is True:
        verdict = "CURRENTLY_RUNNING"
        explanation = "The container is running. ExitCode describes its stored state, not necessarily a new incident."
    elif code == 137 and oom is True:
        verdict = "OOM_KILL_REPORTED_BY_DOCKER"
        explanation = "Exit 137 and Docker OOMKilled=true support an OOM kill. Correlate with kernel/cgroup events."
    elif code == 137:
        verdict = "SIGKILL_OOM_NOT_ESTABLISHED"
        explanation = "Exit 137 means SIGKILL. OOMKilled is false or missing, so an OOM cause is not established."
    elif oom is True:
        verdict = "MISMATCHED_EXIT_AND_OOM_STATUS"
        explanation = "Docker reports OOMKilled but the stored ExitCode is not 137; inspect the original events."
    elif code == 0:
        verdict = "NORMAL_EXIT_REPORTED"
        explanation = "ExitCode is 0 and no OOM kill was reported."
    else:
        verdict = "OTHER_EXIT_STATUS"
        explanation = "This is not Exit 137. Analyze the exit code and application logs separately."
    return {
        "verdict": verdict,
        "exit_code": code,
        "oom_killed": oom,
        "running": running if isinstance(running, bool) else None,
        "explanation": explanation,
        "next_steps": [
            "Inspect Docker and kernel event timestamps without restarting other workloads.",
            "For a running cgroup-v2 container, compare memory.events(.local) with memory.max and memory.current.",
            "Record whether a human, orchestrator or resource limit sent SIGKILL before attributing the incident.",
        ],
    }


def synthetic_tests() -> None:
    cases = [
        ({"ExitCode": 137, "OOMKilled": True, "Running": False}, "OOM_KILL_REPORTED_BY_DOCKER"),
        ({"ExitCode": 137, "OOMKilled": False}, "SIGKILL_OOM_NOT_ESTABLISHED"),
        ({"ExitCode": 137}, "SIGKILL_OOM_NOT_ESTABLISHED"),
        ({"ExitCode": 0, "OOMKilled": False}, "NORMAL_EXIT_REPORTED"),
        ({"ExitCode": 1, "OOMKilled": True}, "MISMATCHED_EXIT_AND_OOM_STATUS"),
        ({"ExitCode": 137, "OOMKilled": True, "Running": True}, "CURRENTLY_RUNNING"),
        ({"State": {"ExitCode": 137, "OOMKilled": True}}, "OOM_KILL_REPORTED_BY_DOCKER"),
        ([{"State": {"ExitCode": 0, "OOMKilled": False}}], "NORMAL_EXIT_REPORTED"),
    ]
    for inp, expected in cases:
        assert classify(state_from_payload(inp))["verdict"] == expected, (inp, expected)
    for invalid in [{}, {"ExitCode": "137"}, {"ExitCode": 137, "OOMKilled": "true"}, [], [1, 2]]:
        try:
            state_from_payload(invalid)
        except ValueError:
            continue
        raise AssertionError("Invalid input accepted")
    print("PASS: 8 synthetic classifications + 5 invalid-input checks", file=sys.stdout)


def main() -> int:
    parser = argparse.ArgumentParser(description="Read-only Docker Exit 137 classifier")
    parser.add_argument("--json", action="store_true", help="Print machine-readable JSON")
    parser.add_argument("--self-test", action="store_true", help="Run offline synthetic tests without Docker")
    args = parser.parse_args()
    if args.self_test:
        synthetic_tests()
        return 0
    try:
        payload = json.load(sys.stdin)
        result = classify(state_from_payload(payload))
    except (ValueError, json.JSONDecodeError) as exc:
        print(f"Invalid input: {type(exc).__name__}", file=sys.stderr)
        return 2
    if args.json:
        print(json.dumps(result, indent=2, ensure_ascii=False))
    else:
        print(f"Verdict: {result['verdict']}")
        print(f"Exit code: {result['exit_code']}, Docker OOMKilled: {result['oom_killed']}")
        print(result["explanation"])
        for index, next_step in enumerate(result["next_steps"], 1):
            print(f"{index}. {next_step}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
