#!/usr/bin/env python3
"""Synthetic supplier-price mapping validator, not an Odoo import or AI test.

Run: python3 mapping-validator.py
The candidate rows were constructed manually. The separately supplied source
policy stands in for decisions approved by a data owner. No network or database
is used. These normalized fields are not a native Odoo import template.
"""
import argparse
import json
import re
from collections import Counter
from datetime import datetime
from decimal import Decimal
from pathlib import Path


class NeedsReview(ValueError):
    pass


def parse_price(value):
    # The fixture contract specifies decimal comma, optional grouped thousands,
    # exactly two fractional digits and a non-negative net purchase price.
    if not re.fullmatch(r"(?:0|[1-9]\d*|[1-9]\d{0,2}(?:\.\d{3})+),\d{2}", value):
        raise NeedsReview("source price does not match the approved numeric format")
    return Decimal(value.replace(".", "").replace(",", "."))


def expected_rows(source, policy):
    if policy.get("price_basis") != "net price per purchase box, retained without normalization" or policy.get("minimum_basis") != "number of purchase boxes":
        raise NeedsReview("this checker supports only the approved retained-box contract")
    if not policy.get("date_format"):
        raise NeedsReview("source date convention has not been approved")
    rows = []
    for raw in source:
        sku = raw["sku"]
        vendor_key = raw["company"] + ":" + raw["vendor_code"]
        if sku not in policy["products"]:
            raise NeedsReview("product reference has no approved lookup: " + sku)
        if vendor_key not in policy["vendors"]:
            raise NeedsReview("vendor reference has no approved company-scoped lookup")
        if raw["source_line"] not in policy["offer_ids"]:
            raise NeedsReview("existing offer identity has not been resolved")
        pack = policy["packs"].get(sku)
        if not pack or raw["units_per_box"] != str(pack["factor"]):
            raise NeedsReview("source pack factor is absent or disagrees with the approved unit")
        try:
            minimum = int(raw["minimum_boxes"])
            date = datetime.strptime(raw["valid_from"], policy["date_format"]).date().isoformat()
        except (ValueError, TypeError) as exc:
            raise NeedsReview("source minimum or date needs review") from exc
        if minimum <= 0:
            raise NeedsReview("source minimum must be positive for this fixture contract")
        price = parse_price(raw["box_price"])
        rows.append({"source_line": raw["source_line"], "external_id": policy["offer_ids"][raw["source_line"]],
                     "sku": sku, "vendor_external_id": policy["vendors"][vendor_key],
                     "product_external_id": policy["products"][sku], "company": raw["company"],
                     "currency": raw["currency"], "purchase_unit": pack["external_id"],
                     "unit_price": format(price, ".2f"), "minimum_quantity": str(minimum), "valid_from": date})
    return rows


def validate(source, policy, candidate):
    try:
        expected = expected_rows(source, policy)
    except NeedsReview as exc:
        return {"status": "HOLD", "reasons": [str(exc)]}
    reasons = []
    source_keys = [row["source_line"] for row in expected]
    candidate_keys = [row.get("source_line") for row in candidate]
    candidate_ids = [row.get("external_id") for row in candidate]
    if len(set(candidate_ids)) != len(candidate_ids):
        reasons.append("candidate external IDs are not unique")
    if len(set(candidate_keys)) != len(candidate_keys):
        reasons.append("candidate source-line coverage contains duplicates")
    if set(candidate_keys) != set(source_keys):
        reasons.append("candidate source-line coverage differs")
    by_line = {row.get("source_line"): row for row in candidate}
    for row in expected:
        actual = by_line.get(row["source_line"], {})
        for field, value in row.items():
            if actual.get(field) != value:
                reasons.append(row["source_line"] + ": " + field + " differs")
        if set(actual) - set(row):
            reasons.append(row["source_line"] + ": unapproved target field")
    return {"status": "REJECT" if reasons else "READY_FOR_REHEARSAL", "reasons": reasons}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("fixtures", nargs="?", type=Path, default=Path(__file__).with_name("mapping-fixtures.json"))
    args = parser.parse_args()
    data = json.loads(args.fixtures.read_text())
    results = []
    for case in data["cases"]:
        actual = validate(case["source"], case["approved_policy"], case["candidate"])
        results.append({"id": case["id"], "label": case["label"], "expected": case["expected"],
                        "observed": actual["status"], "matches_expected": actual["status"] == case["expected"],
                        "reasons": actual["reasons"]})
    output = {"evidence_type": "executed synthetic mapping-validator cases",
              "not_measured": ["AI mapping quality", "native Odoo import", "native unit conversion", "RFQ selection"],
              "case_count": len(results), "classification_counts": dict(Counter(r["observed"] for r in results)),
              "all_expected_classifications_match": all(r["matches_expected"] for r in results), "results": results}
    print(json.dumps(output, indent=2))
    raise SystemExit(0 if output["all_expected_classifications_match"] else 1)


if __name__ == "__main__":
    main()
