import argparse
import csv
import json
from datetime import date
from decimal import Decimal, InvalidOperation
from pathlib import Path


VALIDATOR_ENGINE_VERSION = "2.0.0"


def load_json_object(path, label):
    try:
        value = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, UnicodeError, json.JSONDecodeError) as error:
        raise ValueError(f"{label} is not readable JSON: {error}") from error
    if not isinstance(value, dict):
        raise ValueError(f"{label} root must be an object")
    return value


def is_iso_date(value):
    try:
        date.fromisoformat(value)
    except (TypeError, ValueError):
        return False
    return True


def is_decimal_greater_than(value, threshold):
    try:
        number = Decimal(value)
        minimum = Decimal(threshold)
    except (InvalidOperation, TypeError, ValueError):
        return False
    return number.is_finite() and number > minimum


def add_check(checks, name, passed, expected, actual):
    checks.append(
        {
            "name": name,
            "passed": bool(passed),
            "expected": expected,
            "actual": actual,
        }
    )


def validate(args):
    manifest_path = Path(args.manifest)
    data_path = Path(args.data_file)
    policy_path = Path(args.policy)

    manifest = load_json_object(manifest_path, "manifest")
    policy = load_json_object(policy_path, "policy")
    if policy.get("validator_id") != args.validator_id:
        raise ValueError("policy validator_id does not match --validator-id")
    if policy.get("delivery_type") != manifest.get("delivery_type"):
        raise ValueError("policy delivery_type does not match the manifest")
    if policy.get("schema_version") != manifest.get("schema_version"):
        raise ValueError("policy schema_version does not match the manifest")
    if policy.get("required_file") != data_path.name:
        raise ValueError("policy required_file does not match the selected data file")

    expected_columns = policy.get("columns")
    required_columns = policy.get("required_columns")
    column_rules = policy.get("column_rules")
    if not isinstance(expected_columns, list) or not all(isinstance(item, str) for item in expected_columns):
        raise ValueError("policy columns must be an array of strings")
    if not isinstance(required_columns, list) or not all(isinstance(item, str) for item in required_columns):
        raise ValueError("policy required_columns must be an array of strings")
    if not isinstance(column_rules, dict):
        raise ValueError("policy column_rules must be an object")

    try:
        with data_path.open("r", encoding="utf-8", newline="") as source:
            reader = csv.DictReader(source)
            actual_columns = reader.fieldnames or []
            rows = list(reader)
    except (OSError, UnicodeError, csv.Error) as error:
        raise ValueError(f"data file is not readable CSV: {error}") from error

    checks = []
    add_check(checks, "columns", actual_columns == expected_columns, expected_columns, actual_columns)
    add_check(checks, "rows", len(rows) == manifest.get("rows"), manifest.get("rows"), len(rows))

    missing_values = []
    for row_number, row in enumerate(rows, start=2):
        missing = [column for column in required_columns if not (row.get(column) or "").strip()]
        if missing:
            missing_values.append({"row": row_number, "columns": missing})
    add_check(checks, "required_values", not missing_values, "all required values are present", missing_values)

    for column, rule in column_rules.items():
        if not isinstance(rule, dict) or not isinstance(rule.get("check_name"), str):
            raise ValueError(f"policy rule for {column} must contain check_name")
        violations = []
        expected = {}
        if "type" in rule:
            expected["type"] = rule["type"]
        if "greater_than" in rule:
            expected["greater_than"] = rule["greater_than"]
        if "allowed_values" in rule:
            expected["allowed_values"] = rule["allowed_values"]

        for row_number, row in enumerate(rows, start=2):
            value = row.get(column)
            passed = True
            if rule.get("type") == "iso_date":
                passed = is_iso_date(value)
            elif rule.get("type") == "decimal":
                if "greater_than" not in rule:
                    raise ValueError(f"decimal rule for {column} must contain greater_than")
                passed = is_decimal_greater_than(value, rule["greater_than"])
            elif "type" in rule:
                raise ValueError(f"unsupported policy type for {column}: {rule['type']}")

            allowed_values = rule.get("allowed_values")
            if allowed_values is not None:
                if not isinstance(allowed_values, list) or not all(isinstance(item, str) for item in allowed_values):
                    raise ValueError(f"allowed_values for {column} must be an array of strings")
                passed = passed and value in allowed_values

            if not passed:
                violations.append({"row": row_number, "value": value})

        add_check(checks, rule["check_name"], not violations, expected, violations)

    failed_checks = [item["name"] for item in checks if not item["passed"]]
    return {
        "validator_engine_version": VALIDATOR_ENGINE_VERSION,
        "validator_id": args.validator_id,
        "status": "PASS" if not failed_checks else "FAIL",
        "failed_checks": failed_checks,
        "checks": checks,
    }


def parse_args(argv=None):
    parser = argparse.ArgumentParser()
    parser.add_argument("--manifest", required=True)
    parser.add_argument("--data-file", required=True)
    parser.add_argument("--policy", required=True)
    parser.add_argument("--validator-id", required=True)
    return parser.parse_args(argv)


def main(argv=None):
    args = parse_args(argv)
    try:
        result = validate(args)
    except ValueError as error:
        print(json.dumps({"status": "ERROR", "error": str(error)}, ensure_ascii=False))
        return 2
    print(json.dumps(result, ensure_ascii=False))
    return 0


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