#!/usr/bin/env python3
"""Compare HTTP response evidence without JSON type coercion or array reordering.

Inputs should include HTTP status and selected response headers alongside the parsed
body when those are part of the interface contract. Dynamic fields must be mapped
explicitly in the fixture before comparison. This version only normalizes a
validated top-level trace_id; generated business IDs require separate mapping.
"""

import argparse
from decimal import Decimal
import json
from pathlib import Path
import re


def load_json(path):
    with Path(path).open(encoding="utf-8") as source:
        return json.load(source, parse_float=Decimal, parse_int=int)


def kind(value):
    if value is None:
        return "null"
    if isinstance(value, bool):
        return "boolean"
    if isinstance(value, (int, Decimal)):
        return "number"
    if isinstance(value, str):
        return "string"
    if isinstance(value, list):
        return "array"
    if isinstance(value, dict):
        return "object"
    raise TypeError("Unsupported response value: " + type(value).__name__)


def compare(expected, actual, path="$", rules=None):
    """Return field-level differences; object key order is irrelevant."""
    rules = rules or {}
    expected_kind, actual_kind = kind(expected), kind(actual)
    if expected_kind != actual_kind:
        return [(path, expected_kind, actual_kind, expected, actual)]
    if path in rules:
        rule = rules[path]
        if path != '$["trace_id"]' or expected_kind != "string" \
                or not rule.get("pattern") or not rule.get("reason") \
                or rule["pattern"] in (".*", "^.*$"):
            raise ValueError("Only a validated top-level trace_id rule is supported: " + path)
        if expected_kind != rule["type"]:
            return [(path, rule["type"], actual_kind, expected, actual)]
        pattern = rule["pattern"]
        if pattern and (not re.fullmatch(pattern, str(expected))
                        or not re.fullmatch(pattern, str(actual))):
            return [(path, "matching " + pattern, actual_kind, expected, actual)]
        if rule.get("require_distinct") and expected == actual:
            return [(path, "distinct dynamic value", actual_kind, expected, actual)]
        return []
    if expected_kind == "object":
        differences = []
        for key in sorted(expected.keys() | actual.keys()):
            child = path + "[" + json.dumps(key, ensure_ascii=False) + "]"
            if key not in expected:
                differences.append((child, "missing", kind(actual[key]), None, actual[key]))
            elif key not in actual:
                differences.append((child, kind(expected[key]), "missing", expected[key], None))
            else:
                differences.extend(compare(expected[key], actual[key], child, rules))
        return differences
    if expected_kind == "array":
        differences = []
        if len(expected) != len(actual):
            differences.append((path + ".length", "number", "number", len(expected), len(actual)))
        for index in range(min(len(expected), len(actual))):
            differences.extend(compare(expected[index], actual[index], path + f"[{index}]", rules))
        return differences
    if expected_kind == "number":
        equal = Decimal(expected) == Decimal(actual)
    else:
        equal = expected == actual
    return [] if equal else [(path, expected_kind, actual_kind, expected, actual)]


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("expected", help="frozen A response or independent contract fixture")
    parser.add_argument("actual", help="B or C response")
    parser.add_argument("--rules", help="exact-path dynamic value rules (no global ignores)")
    args = parser.parse_args()
    rules = load_json(args.rules) if args.rules else {}
    differences = compare(load_json(args.expected), load_json(args.actual), rules=rules)
    for path, expected_kind, actual_kind, expected, actual in differences:
        print(json.dumps({"path": path, "expected_type": expected_kind,
                          "actual_type": actual_kind, "expected": expected,
                          "actual": actual}, ensure_ascii=False, default=str))
    print(json.dumps({"status": "PASS" if not differences else "FAIL",
                      "differences": len(differences)}))
    return 0 if not differences else 1


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