#!/usr/bin/env python3
"""Standalone ocDRILL format validator. Requires only Python's standard library.

Usage: python3 validate_question_file.py <set.json>
Does not validate chemistry, SMILES, or name/structure agreement.
"""

import json
import sys
from pathlib import Path


class ValidationError(ValueError):
    pass


def require(condition, path, message):
    if not condition:
        raise ValidationError(f"{path}: {message}")


def obj(value, required, optional, path):
    require(isinstance(value, dict), path, "expected an object")
    missing = set(required) - value.keys()
    extra = value.keys() - set(required) - set(optional)
    require(not missing, path, f"missing fields: {', '.join(sorted(missing))}")
    require(not extra, path, f"unknown fields: {', '.join(sorted(extra))}")


def string(value, path, nonempty=False):
    require(isinstance(value, str), path, "expected a string")
    require(not nonempty or bool(value.strip()), path, "must not be empty")


def array(value, path, nonempty=False):
    require(isinstance(value, list), path, "expected an array")
    require(not nonempty or bool(value), path, "must not be empty")


def enum(value, allowed, path):
    require(isinstance(value, str) and value in allowed, path,
            f"expected one of: {', '.join(allowed)}")


def item(value, path, annotation=False):
    require(isinstance(value, dict), path, "expected an object")
    kind = value.get("type")
    enum(kind, ("text", "blank") if annotation else
         ("text", "molecule", "blank", "arrow"), path + ".type")
    if kind == "text":
        obj(value, ("type", "text"), (), path)
        string(value["text"], path + ".text")
    elif kind == "molecule":
        obj(value, ("type", "smiles", "name"), ("size",), path)
        string(value["smiles"], path + ".smiles", True)
        string(value["name"], path + ".name", True)
        if "size" in value:
            enum(value["size"], ("small", "medium", "large"), path + ".size")
    elif kind == "blank":
        obj(value, ("type",), ("size", "label"), path)
        if "size" in value:
            enum(value["size"], ("short", "medium", "large"), path + ".size")
        if "label" in value:
            string(value["label"], path + ".label")
    else:
        obj(value, ("type",), ("direction", "above", "below"), path)
        if "direction" in value:
            enum(value["direction"], ("right", "equilibrium"), path + ".direction")
        for field in ("above", "below"):
            if field in value:
                array(value[field], path + "." + field)
                for index, entry in enumerate(value[field], 1):
                    item(entry, f"{path}.{field}[{index}]", annotation=True)


def validate_question_file(data):
    obj(data, ("version", "title", "questions"), (), "File")
    require(type(data["version"]) in (int, float) and data["version"] == 1,
            "File.version", "must be the number 1")
    string(data["title"], "File.title", True)
    array(data["questions"], "File.questions", True)
    seen = set()
    for index, question in enumerate(data["questions"], 1):
        path = f"Question {index}"
        obj(question, ("id", "label", "question", "answer"), (), path)
        string(question["id"], path + ".id", True)
        require(question["id"] not in seen, path + ".id", "duplicate ID")
        seen.add(question["id"])
        require(question["id"] == f"q-{index:03d}", path + ".id",
                f"use q-{index:03d} to match question order")
        string(question["label"], path + ".label", True)
        for section in ("question", "answer"):
            array(question[section], path + "." + section, True)
            for row_index, row in enumerate(question[section], 1):
                row_path = f"{path}, {section} row {row_index}"
                obj(row, ("items",), (), row_path)
                array(row["items"], row_path + ".items", True)
                for item_index, entry in enumerate(row["items"], 1):
                    item(entry, f"{row_path}, item {item_index}")
    return len(data["questions"])


def unique_keys(pairs):
    result = {}
    for key, value in pairs:
        require(key not in result, "JSON", f"duplicate object key: {key}")
        result[key] = value
    return result


def reject_constant(value):
    raise ValidationError(f"JSON: invalid constant {value}")


def main():
    if len(sys.argv) != 2:
        print("Usage: python3 validate_question_file.py <set.json>", file=sys.stderr)
        return 2
    try:
        data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"),
                          object_pairs_hook=unique_keys, parse_constant=reject_constant)
        count = validate_question_file(data)
    except (OSError, UnicodeError, ValueError) as error:
        print(f"ERROR: {error}", file=sys.stderr)
        return 1
    print(f"Valid ocDRILL authoring format: {count} question(s).")
    print("Chemistry, SMILES validity, and name/structure agreement were NOT validated.")
    return 0


if __name__ == "__main__":
    sys.exit(main())
