#!/usr/bin/env python3

import sys
import json
import re

# Schema specifications
SCHEMA = {
    'label': {
        'type': str,
        'nullable': False,
        # Closed vocabulary (decided 2026-08-14). The bad/error split keeps
        # model performance separable from infrastructure failure.
        'enum': ['good', 'partial', 'bad', 'error']
    },
    'author_nick': {
        'type': str,
        'nullable': False,
        'regex': r'^[A-Za-z0-9_.-]{1,64}$'
    },
    'verified': {
        'type': bool,
        'nullable': False,
    },
    'model_id': {
        'type': str,
        'nullable': True,
        'regex': r'^[A-Za-z0-9][A-Za-z0-9/_.:-]{0,127}$'
    },
    'model_id_reason': {
        'type': str,
        'nullable': True,
        'enum': ['role_no_model', 'legacy_pre_schema', 'parse_failed']
    },
    'prompt_hash': {
        'type': str,
        'nullable': True,
        'regex': r'^[0-9a-f]{64}$'
    },
    'template_version': {
        'type': str,
        'nullable': True,
        'regex': r'^v[0-9]{1,4}$'
    },
    'ts': {
        'type': str,
        'nullable': False,
        'regex': r'^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$'
    },
    'ask_id': {
        'type': int,
        'nullable': False,
        'min_value': 0
    },
    'probe_score': {
        'type': type(None),
        'nullable': True,
    }
}

REQUIRED_KEYS = set(SCHEMA.keys())

def validate_file(filepath):
    errors = []
    checked = 0
    seen_pairs = set()
    prev_ts = None

    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            try:
                for lineno, line in enumerate(f, 1):
                    checked += 1

                    # Check for blank line (only whitespace)
                    if not line.strip():
                        errors.append(f"LINE {lineno}: E_JSON -: blank line")
                        continue

                    # Try to parse JSON
                    try:
                        obj = json.loads(line)
                    except (json.JSONDecodeError, ValueError) as e:
                        errors.append(f"LINE {lineno}: E_JSON -: invalid JSON")
                        continue

                    # Must be a dict/object
                    if not isinstance(obj, dict):
                        errors.append(f"LINE {lineno}: E_JSON -: not a JSON object")
                        continue

                    # Get object keys
                    obj_keys = set(obj.keys())

                    # Check for missing keys
                    missing = REQUIRED_KEYS - obj_keys
                    for key in sorted(missing):
                        errors.append(f"LINE {lineno}: E_KEY_MISSING {key}: key is missing")

                    # Check for unknown keys
                    unknown = obj_keys - REQUIRED_KEYS
                    for key in sorted(unknown):
                        errors.append(f"LINE {lineno}: E_KEY_UNKNOWN {key}: unknown key")

                    # If structural errors exist, skip field validation but continue checking other constraints
                    if missing or unknown:
                        # We can still check some things if we have the required fields
                        pass

                    # Only proceed with field validation if all required keys are present
                    if missing:
                        continue

                    # Validate each field
                    has_field_error = False
                    for key in REQUIRED_KEYS:
                        value = obj[key]
                        spec = SCHEMA[key]

                        # Special case for probe_score - must be null
                        if key == 'probe_score':
                            if value is not None:
                                errors.append(f"LINE {lineno}: E_PROBE probe_score: must be null")
                                has_field_error = True
                            continue

                        # Check if value is null
                        if value is None:
                            if not spec['nullable']:
                                errors.append(f"LINE {lineno}: E_TYPE {key}: null not allowed for this field")
                                has_field_error = True
                            continue

                        # Special check for ask_id: reject boolean (bool is subclass of int in Python)
                        if key == 'ask_id' and isinstance(value, bool):
                            errors.append(f"LINE {lineno}: E_TYPE ask_id: JSON boolean is not a JSON integer")
                            has_field_error = True
                            continue

                        # Type check
                        expected_type = spec['type']
                        if not isinstance(value, expected_type):
                            errors.append(f"LINE {lineno}: E_TYPE {key}: expected {expected_type.__name__}, got {type(value).__name__}")
                            has_field_error = True
                            continue

                        # Enum check
                        if 'enum' in spec:
                            if value not in spec['enum']:
                                errors.append(f"LINE {lineno}: E_VALUE {key}: '{value}' not in enum")
                                has_field_error = True
                            continue

                        # Min value check for integers
                        if 'min_value' in spec and isinstance(value, int):
                            if value < spec['min_value']:
                                errors.append(f"LINE {lineno}: E_VALUE {key}: value is negative")
                                has_field_error = True
                            continue

                        # Regex check for strings
                        if 'regex' in spec and isinstance(value, str):
                            if not re.match(spec['regex'], value):
                                errors.append(f"LINE {lineno}: E_VALUE {key}: does not match pattern")
                                has_field_error = True

                    # XOR check for model_id and model_id_reason
                    # One must be null, the other must be non-null
                    model_id = obj.get('model_id')
                    model_id_reason = obj.get('model_id_reason')
                    if (model_id is None) == (model_id_reason is None):
                        # Both null or both non-null is an error
                        errors.append(f"LINE {lineno}: E_REASON -: model_id and model_id_reason must have exactly one null")

                    # HASHPAIR check - both must be null or both must be non-null
                    prompt_hash = obj.get('prompt_hash')
                    template_version = obj.get('template_version')
                    if (prompt_hash is None) != (template_version is None):
                        errors.append(f"LINE {lineno}: E_HASHPAIR -: prompt_hash and template_version must be both null or both non-null")

                    # DUP check - (ask_id, author_nick) pair must be unique
                    ask_id = obj.get('ask_id')
                    author_nick = obj.get('author_nick')
                    if ask_id is not None and author_nick is not None:
                        pair = (ask_id, author_nick)
                        if pair in seen_pairs:
                            errors.append(f"LINE {lineno}: E_DUP -: duplicate (ask_id, author_nick) pair")
                        else:
                            seen_pairs.add(pair)

                    # ORDER check - ts must be non-decreasing
                    ts = obj.get('ts')
                    if ts is not None:
                        if prev_ts is not None and ts < prev_ts:
                            errors.append(f"LINE {lineno}: E_ORDER -: ts is less than previous line's ts")
                        prev_ts = ts

            except UnicodeDecodeError as e:
                print(f"label-validate: error: file is not valid UTF-8", file=sys.stderr)
                sys.exit(2)

    except (IOError, OSError) as e:
        print(f"label-validate: error: cannot read file: {str(e)}", file=sys.stderr)
        sys.exit(2)

    # Print all errors
    for error in errors:
        print(error)

    # Print summary line
    print(f"label-validate: checked={checked} errors={len(errors)}")

    # Return exit code
    return 1 if errors else 0

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("label-validate: error: exactly one FILE argument required", file=sys.stderr)
        sys.exit(2)

    sys.exit(validate_file(sys.argv[1]))
