import math

POSITIVE_STATES = {
    "INTEROPERABLE",
    "INTEROPERABLE_WITH_KNOWN_DIFFERENCES",
    "TARGET_SPECIFIC",
}
ALLOWED_STATES = POSITIVE_STATES | {
    "REWORK_REQUIRED", "HOLD", "EVIDENCE_INCOMPLETE", "PROVENANCE_UNKNOWN", "METHOD_UNSUITABLE",
}

NUMERIC_BUDGETS = [
    ("triangle_observed", "triangle_budget", "triangles"),
    ("vertex_observed", "vertex_budget", "vertices"),
    ("texture_memory_observed_mib", "texture_memory_budget_mib", "texture_memory_mib"),
    ("material_count_observed", "material_count_budget", "materials"),
    ("draw_primitive_observed", "draw_primitive_budget", "draw_primitives"),
    ("quality_delta_pct", "quality_delta_budget_pct", "quality_delta_pct"),
]
INTEGER_COUNT_FIELDS = {
    "triangle_budget", "triangle_observed", "vertex_budget", "vertex_observed",
    "material_count_budget", "material_count_observed", "draw_primitive_budget", "draw_primitive_observed",
}
POSITIVE_DECISIVE_STRINGS = [
    "delivery_target", "format", "format_version", "coordinate_convention", "lod_policy", "compression_extension",
    "animation_requirement", "skin_requirement", "morph_target_requirement", "transform_observation_ref",
    "transform_validation", "material_validation", "round_trip_check", "downstream_viewer_check", "provenance_ref", "decision_rationale",
]
LIST_FIELDS = [
    "used_extensions", "required_extensions", "target_supported_extensions", "unsupported_feature",
    "missing_decisive_evidence", "known_differences",
]
REQUIRED_FIELDS = {
    "delivery_target", "format", "format_version", "unit_scale", "coordinate_convention",
    "triangle_budget", "triangle_observed", "vertex_budget", "vertex_observed",
    "texture_memory_budget_mib", "texture_memory_observed_mib", "material_count_budget", "material_count_observed",
    "draw_primitive_budget", "draw_primitive_observed", "lod_policy", "compression_extension", "used_extensions",
    "animation_requirement", "skin_requirement", "morph_target_requirement", "transform_tolerance",
    "transform_observation_ref", "transform_delta_observed", "transform_validation", "material_validation",
    "required_extensions", "target_supported_extensions", "unsupported_feature", "round_trip_check", "downstream_viewer_check",
    "quality_delta_pct", "quality_delta_budget_pct", "provenance_ref", "missing_decisive_evidence", "known_differences",
    "target_specific_constraint", "hold_reason", "method_unsuitable_reason", "final_state", "decision_rationale",
}
TRANSFORM_AXES = ("translation_m", "rotation_deg", "scale_abs")


def _issue(code, message, severity="BLOCKER"):
    return {"code": code, "severity": severity, "message": message}


def _is_number(value):
    return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)


def _is_integer_count(value):
    return isinstance(value, int) and not isinstance(value, bool)


def _nonempty_string(value):
    return isinstance(value, str) and bool(value.strip())


def _valid_string_list(value):
    return isinstance(value, list) and all(_nonempty_string(x) for x in value)


def _known_difference_status(known_differences):
    if not isinstance(known_differences, list): return False, False
    if not known_differences: return True, True
    well_formed = True; all_accepted = True
    for item in known_differences:
        if not isinstance(item, dict): well_formed = False; all_accepted = False; continue
        required = ("description", "impact", "acceptance_status", "acceptance_ref")
        if not all(_nonempty_string(item.get(k)) for k in required): well_formed = False; all_accepted = False; continue
        if item.get("acceptance_status") != "ACCEPTED": all_accepted = False
    return well_formed, all_accepted


def _validate_transform_evidence(pack):
    tol = pack.get("transform_tolerance")
    obs = pack.get("transform_delta_observed")
    ref = pack.get("transform_observation_ref")
    if not isinstance(tol, dict) or not isinstance(obs, dict) or not _nonempty_string(ref):
        return False, None
    for axis in TRANSFORM_AXES:
        t, o = tol.get(axis), obs.get(axis)
        if not _is_number(t) or t <= 0 or not _is_number(o) or o < 0:
            return False, None
    measured_pass = all(obs[a] <= tol[a] for a in TRANSFORM_AXES)
    return True, measured_pass


def validate_pack(pack):
    issues = []
    if not isinstance(pack, dict): return [_issue("PACK_TYPE_INVALID", "Evidence pack must be a JSON object.")]
    missing = sorted(k for k in REQUIRED_FIELDS if k not in pack)
    if missing:
        issues.append(_issue("REQUIRED_FIELDS_MISSING", f"Missing required fields: {', '.join(missing)}")); return issues

    state = pack.get("final_state")
    if state not in ALLOWED_STATES: issues.append(_issue("INVALID_FINAL_STATE", f"Unknown final_state: {state}"))

    for key in LIST_FIELDS[:-1]:
        if not _valid_string_list(pack.get(key)):
            issues.append(_issue("LIST_FIELD_INVALID", f"{key} must be a JSON list of non-empty strings."))
    known_differences = pack.get("known_differences")
    known_well_formed, known_all_accepted = _known_difference_status(known_differences)
    if not known_well_formed:
        issues.append(_issue("KNOWN_DIFFERENCE_RECORD_INVALID", "known_differences must contain description, impact, acceptance_status and acceptance_ref."))

    numeric_rules = {
        "unit_scale": (False, 0), "triangle_budget": (False, 0), "triangle_observed": (True, 0),
        "vertex_budget": (False, 0), "vertex_observed": (True, 0), "texture_memory_budget_mib": (False, 0),
        "texture_memory_observed_mib": (True, 0), "material_count_budget": (False, 0), "material_count_observed": (True, 0),
        "draw_primitive_budget": (False, 0), "draw_primitive_observed": (True, 0), "quality_delta_pct": (True, 0),
        "quality_delta_budget_pct": (True, 0),
    }
    invalid_numeric=set()
    for key,(zero_allowed,minimum) in numeric_rules.items():
        value=pack.get(key)
        if value is None: continue
        valid = _is_integer_count(value) if key in INTEGER_COUNT_FIELDS else _is_number(value)
        if not valid or value < minimum or (not zero_allowed and value == 0):
            invalid_numeric.add(key); issues.append(_issue("NUMERIC_FIELD_INVALID", f"{key} must be finite and in its declared integer/numeric domain."))

    transform_valid, measured_transform_pass = _validate_transform_evidence(pack)
    if not transform_valid:
        issues.append(_issue("TRANSFORM_EVIDENCE_INVALID", "Transform evidence requires positive finite tolerances, a stable observation ref, and finite non-negative measured deltas."))
    elif pack.get("transform_validation") not in {"PASS","FAIL"}:
        issues.append(_issue("TRANSFORM_VALIDATION_DOMAIN_INVALID", "transform_validation must be PASS or FAIL."))
    elif (pack.get("transform_validation") == "PASS") != measured_transform_pass:
        issues.append(_issue("TRANSFORM_EVIDENCE_CONTRADICTION", "transform_validation conflicts with measured transform deltas versus declared tolerance."))

    invalid_decisive=[]
    for key in POSITIVE_DECISIVE_STRINGS:
        if not _nonempty_string(pack.get(key)): invalid_decisive.append(key)
    if pack.get("unit_scale") is None or "unit_scale" in invalid_numeric: invalid_decisive.append("unit_scale")
    if state in POSITIVE_STATES and invalid_decisive:
        issues.append(_issue("DECISIVE_FIELD_INVALID", "Positive state requires non-empty, typed decisive identity/context fields: " + ", ".join(sorted(set(invalid_decisive)))))

    budget_failures=[]; missing_numeric=[]
    for observed_key,budget_key,label in NUMERIC_BUDGETS:
        observed,budget=pack.get(observed_key),pack.get(budget_key)
        if observed is None or budget is None: missing_numeric.append(label); continue
        if observed_key in invalid_numeric or budget_key in invalid_numeric: continue
        if observed > budget: budget_failures.append(f"{label}:{observed}>{budget}")
    if state in POSITIVE_STATES and missing_numeric: issues.append(_issue("DECISIVE_EVIDENCE_MISSING_SUCCESS", "Positive state conflicts with missing numeric evidence: " + ", ".join(missing_numeric)))
    if state in POSITIVE_STATES and budget_failures: issues.append(_issue("BUDGET_EXCEEDED_SUCCESS", "Positive state conflicts with exceeded budgets: " + "; ".join(budget_failures)))

    used = set(pack.get("used_extensions") or []) if _valid_string_list(pack.get("used_extensions")) else set()
    required = set(pack.get("required_extensions") or []) if _valid_string_list(pack.get("required_extensions")) else set()
    supported = set(pack.get("target_supported_extensions") or []) if _valid_string_list(pack.get("target_supported_extensions")) else set()
    required_not_used=sorted(required-used)
    if required_not_used: issues.append(_issue("REQUIRED_EXTENSION_NOT_DECLARED_USED", "Every required extension must also be declared in used_extensions: " + ", ".join(required_not_used)))
    compression=str(pack.get("compression_extension") or "").strip()
    if compression and compression.lower() != "none" and compression not in used:
        issues.append(_issue("COMPRESSION_EXTENSION_NOT_DECLARED_USED", "compression_extension must appear in used_extensions."))
    unsupported_required=sorted(required-supported)
    unsupported_features=pack.get("unsupported_feature") if _valid_string_list(pack.get("unsupported_feature")) else []
    if state in POSITIVE_STATES and unsupported_required: issues.append(_issue("UNSUPPORTED_REQUIRED_EXTENSION_SUCCESS", "Positive state conflicts with unsupported required extensions: " + ", ".join(unsupported_required)))
    if state in POSITIVE_STATES and unsupported_features: issues.append(_issue("UNSUPPORTED_FEATURE_SUCCESS", "Positive state cannot retain an unresolved unsupported target feature."))

    failed_checks=[k for k in ("transform_validation","material_validation","round_trip_check","downstream_viewer_check") if pack.get(k) != "PASS"]
    if state in POSITIVE_STATES and failed_checks: issues.append(_issue("VALIDATION_FAILURE_SUCCESS", "Positive state conflicts with failed/non-PASS checks: " + ", ".join(failed_checks)))

    provenance=str(pack.get("provenance_ref") or "").strip().upper(); provenance_unknown=not provenance or provenance in {"UNKNOWN","NONE","N/A","NULL"}
    if state in POSITIVE_STATES and provenance_unknown: issues.append(_issue("PROVENANCE_MISSING_SUCCESS", "Positive state requires explicit provenance evidence."))
    missing_decisive=pack.get("missing_decisive_evidence") if _valid_string_list(pack.get("missing_decisive_evidence")) else []
    if state in POSITIVE_STATES and missing_decisive: issues.append(_issue("MISSING_DECISIVE_EVIDENCE_CONFLICT", "Positive state conflicts with non-empty missing_decisive_evidence."))
    if state == "EVIDENCE_INCOMPLETE" and not (missing_decisive or missing_numeric or invalid_decisive or invalid_numeric or not transform_valid): issues.append(_issue("INCOMPLETE_WITHOUT_MISSING_EVIDENCE", "EVIDENCE_INCOMPLETE requires visible missing or invalid decisive evidence."))
    if state == "PROVENANCE_UNKNOWN" and not provenance_unknown: issues.append(_issue("PROVENANCE_UNKNOWN_WITH_KNOWN_PROVENANCE", "PROVENANCE_UNKNOWN requires unresolved provenance."))

    pending_known_difference=bool(known_differences) and (not known_well_formed or not known_all_accepted)
    if state == "INTEROPERABLE_WITH_KNOWN_DIFFERENCES" and (not known_differences or not known_well_formed or not known_all_accepted): issues.append(_issue("KNOWN_DIFFERENCE_ACCEPTANCE_MISSING", "INTEROPERABLE_WITH_KNOWN_DIFFERENCES requires at least one visible bounded difference with explicit ACCEPTED status and acceptance_ref."))

    target_specific_constraint=str(pack.get("target_specific_constraint") or "").strip(); hold_reason=str(pack.get("hold_reason") or "").strip(); method_unsuitable_reason=str(pack.get("method_unsuitable_reason") or "").strip()
    if state == "TARGET_SPECIFIC" and not target_specific_constraint: issues.append(_issue("TARGET_SPECIFIC_WITHOUT_CONSTRAINT", "TARGET_SPECIFIC requires an explicit target_specific_constraint."))
    if state == "HOLD" and not (hold_reason or pending_known_difference): issues.append(_issue("HOLD_WITHOUT_VISIBLE_REASON", "HOLD requires a visible external hold reason or pending known-difference acceptance."))
    if state == "METHOD_UNSUITABLE" and not method_unsuitable_reason: issues.append(_issue("METHOD_UNSUITABLE_WITHOUT_REASON", "METHOD_UNSUITABLE requires method_unsuitable_reason."))

    hard_failures=bool(budget_failures or unsupported_required or unsupported_features or failed_checks or required_not_used)
    evidence_gaps=bool(missing_decisive or missing_numeric or invalid_decisive or invalid_numeric or not transform_valid)
    if method_unsuitable_reason: expected_state="METHOD_UNSUITABLE"
    elif provenance_unknown: expected_state="PROVENANCE_UNKNOWN"
    elif hold_reason or pending_known_difference: expected_state="HOLD"
    elif evidence_gaps: expected_state="EVIDENCE_INCOMPLETE"
    elif hard_failures: expected_state="REWORK_REQUIRED"
    elif target_specific_constraint: expected_state="TARGET_SPECIFIC"
    elif known_differences: expected_state="INTEROPERABLE_WITH_KNOWN_DIFFERENCES"
    else: expected_state="INTEROPERABLE"
    if state in ALLOWED_STATES and state != expected_state: issues.append(_issue("PRIMARY_STATE_PRECEDENCE_MISMATCH", "Declared final_state conflicts with the published primary-state precedence for the visible evidence."))
    if state == "REWORK_REQUIRED" and not hard_failures: issues.append(_issue("REWORK_WITHOUT_VISIBLE_BLOCKER", "REWORK_REQUIRED should name at least one visible failing gate.", severity="WARNING"))
    if state == "INTEROPERABLE" and known_differences: issues.append(_issue("KNOWN_DIFFERENCE_HIDDEN", "INTEROPERABLE cannot hide declared known differences."))
    if not _nonempty_string(pack.get("decision_rationale")): issues.append(_issue("RATIONALE_MISSING", "decision_rationale must be explicit."))
    return issues
