"""MEOK bounded rational arithmetic; no Python eval/exec, I/O or model calls.

Accepts arithmetic assignments ending in ``answer = ...``, or one expression.
This checks calculation, not whether a model translated a word problem correctly.
MIT licensed. Copyright 2026 Council of AI contributors.
"""
from __future__ import annotations
import ast
import hashlib
import json
import math
import re
from fractions import Fraction
from typing import Any

VERSION = '1.1.0'
MAX_BYTES = 4096
MAX_NODES = 256
MAX_DEPTH = 24
MAX_STEPS = 24
MAX_INTEGER_BITS = 160
MAX_LITERAL_CHARS = 60
MAX_EXPONENT = 6
_NAME = re.compile(r'[A-Za-z][A-Za-z0-9_]{0,31}\Z')
FUNCTIONS = {'abs', 'min', 'max', 'round', 'floor', 'ceil'}


class ArithmeticRefusal(ValueError):
    """Out-of-contract expression or resource limit, not a computed result."""


def _bounded(value: Fraction) -> Fraction:
    if value.numerator.bit_length() > MAX_INTEGER_BITS or value.denominator.bit_length() > MAX_INTEGER_BITS:
        raise ArithmeticRefusal('ARITHMETIC_SIZE_LIMIT')
    return value


def _valid_name(name: str) -> bool:
    return bool(_NAME.fullmatch(name)) and name not in FUNCTIONS


def calculate(source: str) -> dict[str, Any]:
    if not isinstance(source, str) or not source.strip():
        raise ArithmeticRefusal('EMPTY_INPUT')
    if len(source.encode('utf-8')) > MAX_BYTES:
        raise ArithmeticRefusal('INPUT_SIZE_LIMIT')
    original = source
    # Permit one explicit surrounding code fence, never extract a fragment from prose.
    stripped = source.strip()
    if stripped.startswith('```'):
        match = re.fullmatch(r'```(?:python)?[ \t]*\n([\s\S]*?)\n```', stripped)
        if not match:
            raise ArithmeticRefusal('INVALID_CODE_FENCE')
        source = match.group(1)
    else:
        source = stripped
    try:
        tree = ast.parse(source, mode='exec')
    except (SyntaxError, RecursionError, ValueError) as exc:
        raise ArithmeticRefusal('INVALID_SYNTAX') from exc
    nodes = list(ast.walk(tree))
    if len(nodes) > MAX_NODES or not 1 <= len(tree.body) <= MAX_STEPS:
        raise ArithmeticRefusal('STRUCTURE_SIZE_LIMIT')
    values: dict[str, Fraction] = {}
    trace = []

    def evaluate(node: ast.AST, depth: int = 0) -> Fraction:
        if depth > MAX_DEPTH:
            raise ArithmeticRefusal('DEPTH_LIMIT')
        if isinstance(node, ast.Constant):
            if type(node.value) not in (int, float):
                raise ArithmeticRefusal('NUMERIC_LITERALS_ONLY')
            text = ast.get_source_segment(source, node)
            if not text or len(text) > MAX_LITERAL_CHARS or '_' in text or any(x in text.lower() for x in ['0x','0o','0b']):
                raise ArithmeticRefusal('DECIMAL_LITERAL_REQUIRED')
            if isinstance(node.value, float) and not math.isfinite(node.value):
                raise ArithmeticRefusal('NONFINITE_LITERAL')
            # Guard before constructing any very large denominator.
            exponent = re.search(r'[eE]([+-]?\d+)\Z', text)
            if exponent and (len(exponent.group(1).lstrip('+-')) > 3 or abs(int(exponent.group(1))) > 48):
                raise ArithmeticRefusal('DECIMAL_EXPONENT_LIMIT')
            try:
                return _bounded(Fraction(text))
            except (ValueError, ZeroDivisionError, OverflowError) as exc:
                raise ArithmeticRefusal('INVALID_NUMBER') from exc
        if isinstance(node, ast.Name):
            if node.id not in values:
                raise ArithmeticRefusal('UNDEFINED_NAME')
            return values[node.id]
        if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd,ast.USub)):
            v = evaluate(node.operand, depth + 1)
            return v if isinstance(node.op, ast.UAdd) else -v
        if isinstance(node, ast.BinOp):
            a, b = evaluate(node.left,depth+1), evaluate(node.right,depth+1)
            try:
                if isinstance(node.op, ast.Add): v = a + b
                elif isinstance(node.op, ast.Sub): v = a - b
                elif isinstance(node.op, ast.Mult): v = a * b
                elif isinstance(node.op, ast.Div): v = a / b
                elif isinstance(node.op, ast.FloorDiv): v = Fraction(a // b)
                elif isinstance(node.op, ast.Mod): v = a % b
                elif isinstance(node.op, ast.Pow):
                    if b.denominator != 1 or abs(b.numerator) > MAX_EXPONENT:
                        raise ArithmeticRefusal('EXPONENT_LIMIT')
                    v = a ** int(b)
                else: raise ArithmeticRefusal('OPERATOR_NOT_ALLOWED')
            except (ZeroDivisionError,OverflowError) as exc:
                raise ArithmeticRefusal('INVALID_ARITHMETIC') from exc
            return _bounded(v)
        if isinstance(node, ast.Call):
            if not isinstance(node.func, ast.Name) or node.func.id not in FUNCTIONS or node.keywords or not 1 <= len(node.args) <= 8:
                raise ArithmeticRefusal('CALL_NOT_ALLOWED')
            args = [evaluate(arg,depth+1) for arg in node.args]
            name = node.func.id
            if name in {'abs','floor','ceil'} and len(args) != 1:
                raise ArithmeticRefusal('ARGUMENT_COUNT')
            if name == 'abs': value = abs(args[0])
            elif name == 'floor': value = Fraction(math.floor(args[0]))
            elif name == 'ceil': value = Fraction(math.ceil(args[0]))
            elif name == 'min': value = min(args)
            elif name == 'max': value = max(args)
            else:
                if len(args) not in (1,2): raise ArithmeticRefusal('ARGUMENT_COUNT')
                places = args[1] if len(args) == 2 else Fraction(0)
                if places.denominator != 1 or abs(places.numerator)>6:
                    raise ArithmeticRefusal('ROUNDING_LIMIT')
                value = Fraction(round(args[0],int(places)))
            return _bounded(value)
        raise ArithmeticRefusal('NODE_NOT_ALLOWED:' + type(node).__name__)

    for index, statement in enumerate(tree.body):
        if isinstance(statement, ast.Expr) and len(tree.body) == 1:
            name, value = 'answer', evaluate(statement.value)
        elif isinstance(statement, ast.Assign) and len(statement.targets) == 1 and isinstance(statement.targets[0],ast.Name):
            name = statement.targets[0].id
            if not _valid_name(name) or name in values:
                raise ArithmeticRefusal('INVALID_OR_REUSED_NAME')
            value = evaluate(statement.value)
        else:
            raise ArithmeticRefusal('ONLY_SINGLE_ASSIGNMENTS_OR_EXPRESSION')
        if 'answer' in values:
            raise ArithmeticRefusal('ANSWER_MUST_BE_LAST')
        values[name] = value
        trace.append({'variable':name,'numerator':str(value.numerator),'denominator':str(value.denominator)})
    if 'answer' not in values:
        raise ArithmeticRefusal('MISSING_ANSWER')
    answer = values['answer']
    return {
        'schema':'meok.bounded-arithmetic/1','version':VERSION,
        'value':str(answer.numerator) if answer.denominator == 1 else str(answer),
        'numerator':str(answer.numerator),'denominator':str(answer.denominator),
        'trace':trace,'input_sha256':hashlib.sha256(original.encode()).hexdigest(),
        'calculation_valid':True,'word_problem_translation_verified':False,
        'external_action_authority':False,'model_weights_changed':False,
        'scope':'Exact rational evaluation of the accepted expression, not independent validation of its assumptions.'}


def result(source: str) -> dict[str, Any]:
    """A total wrapper suitable for a UI: refusals contain no previous result."""
    try:
        return {'status':'COMPUTED','result':calculate(source),'error':None}
    except ArithmeticRefusal as exc:
        return {'status':'REFUSED','result':None,'error':str(exc)}


if __name__ == '__main__':
    import sys
    print(json.dumps(result(sys.stdin.read()),indent=2,allow_nan=False))
