"""Offline reference verification for iam-core 1.1 (wire v=2).

Standard-library-only, intentionally small and auditable. This module does not
derive private keys, sign messages, provide networking, or implement production
resource controls. Its Edwards arithmetic handles public inputs and is not a
constant-time implementation. canonical_bytes supports the restricted IAM data
domain, not arbitrary RFC 8785 JSON numbers or Unicode strings.
"""

from __future__ import annotations

import base64
import binascii
import hashlib
import json
import re
from functools import lru_cache

WIRE_VERSION = 2
MAX_M = (1 << 53) - 1
PROFILE = "iam-core-1.1-wire-2"
ID_PREFIX = b"IAM2:id\x00"
RECORD_PREFIX = b"IAM2:record\x00"
TREE_PREFIX = b"IAM2:tree\x00"
RECORD_FIELDS = frozenset(("v", "kind", "tree", "context", "m", "actor", "target", "prev", "body"))
DESCRIPTOR_FIELDS = frozenset(("v", "kind", "context", "m", "actor", "target"))
ENVELOPE_FIELDS = frozenset(("record", "id", "sig"))
_HEX64 = re.compile(r"[0-9a-f]{64}\Z", re.ASCII)
_INTEGER_TOKEN = re.compile(r"(?:0|[1-9][0-9]*)\Z", re.ASCII)
_B64 = re.compile(r"[A-Za-z0-9+/]{86}==\Z", re.ASCII)


class ValidationError(ValueError):
    """An input does not conform to the iam-core 1.1 wire profile."""


def _fail(message):
    raise ValidationError(message)


def _json_integer(token):
    if not _INTEGER_TOKEN.fullmatch(token):
        _fail("number token must be a non-negative decimal integer")
    # Lexical bound avoids a large integer conversion and rounding entirely.
    maximum = str(MAX_M)
    if len(token) > len(maximum) or (len(token) == len(maximum) and token > maximum):
        _fail("number exceeds the safe integer bound")
    return int(token)


def _forbidden_number(token):
    _fail("non-integer number token: " + token[:32])


def _object_pairs(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            _fail("duplicate JSON member name")
        result[key] = value
    return result


def _check_ijson_strings(value):
    if isinstance(value, str):
        for char in value:
            point = ord(char)
            if 0xD800 <= point <= 0xDFFF or 0xFDD0 <= point <= 0xFDEF or (point & 0xFFFF) >= 0xFFFE:
                _fail("I-JSON forbids surrogate and noncharacter code points")
    elif isinstance(value, dict):
        for key, item in value.items():
            _check_ijson_strings(key)
            _check_ijson_strings(item)
    elif isinstance(value, list):
        for item in value:
            _check_ijson_strings(item)


def parse_json(text):
    """Parse JSON, rejecting duplicates, non-I-JSON strings and number aliases.

    Accepts UTF-8 bytes or a decoded string. Applies the IAM integer-token rule
    to every number; this is a profile parser, not a generic runtime-wrapper
    parser. Call the shape validators separately after parsing.
    """
    if isinstance(text, bytes):
        try:
            text = text.decode("utf-8", errors="strict")
        except UnicodeDecodeError as exc:
            raise ValidationError("JSON is not valid UTF-8") from exc
    if not isinstance(text, str):
        _fail("JSON input must be str or bytes")
    try:
        value = json.loads(text, object_pairs_hook=_object_pairs, parse_int=_json_integer,
                           parse_float=_forbidden_number, parse_constant=_forbidden_number)
        _check_ijson_strings(value)
    except (json.JSONDecodeError, RecursionError) as exc:
        raise ValidationError("invalid JSON input") from exc
    return value


def canonical_bytes(value):
    """JCS bytes for IAM's printable-ASCII / non-negative safe-integer subset."""
    def check(item):
        if type(item) is int:
            if not 0 <= item <= MAX_M:
                _fail("canonical integer outside the profile domain")
        elif type(item) is str:
            if any(not 0x20 <= ord(char) <= 0x7E for char in item):
                _fail("canonical strings must be printable ASCII in this profile")
        elif type(item) is list:
            for child in item:
                check(child)
        elif type(item) is dict:
            for key, child in item.items():
                if type(key) is not str:
                    _fail("JSON object member name must be a string")
                check(key)
                check(child)
        else:
            _fail("value outside the restricted IAM canonicalization domain")
    check(value)
    return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")


# Extended Edwards coordinates (X:Y:Z:T), x=X/Z, y=Y/Z, T=XY/Z.
# Formulas and canonical point decoding: RFC 8032 sections 5.1.3 and 5.1.4.
P = (1 << 255) - 19
L = (1 << 252) + 27742317777372353535851937790883648493
D = (-121665 * pow(121666, P - 2, P)) % P
SQRT_MINUS_ONE = pow(2, (P - 1) // 4, P)
IDENTITY = (0, 1, 1, 0)


def _decode_point(encoded):
    if type(encoded) is not bytes or len(encoded) != 32:
        _fail("point encoding must contain exactly 32 bytes")
    integer = int.from_bytes(encoded, "little")
    sign = integer >> 255
    y = integer & ((1 << 255) - 1)
    if y >= P:
        _fail("noncanonical point y coordinate")
    yy = y * y % P
    xx = (yy - 1) * pow((D * yy + 1) % P, P - 2, P) % P
    x = pow(xx, (P + 3) // 8, P)
    if (x * x - xx) % P:
        x = x * SQRT_MINUS_ONE % P
    if (x * x - xx) % P:
        _fail("encoding is not a point on Ed25519")
    if x == 0 and sign:
        _fail("noncanonical sign bit for x=0")
    if (x & 1) != sign:
        x = P - x
    return x, y, 1, x * y % P


def _add(first, second):
    x1, y1, z1, t1 = first
    x2, y2, z2, t2 = second
    a = (y1 - x1) * (y2 - x2) % P
    b = (y1 + x1) * (y2 + x2) % P
    c = 2 * D * t1 * t2 % P
    d = 2 * z1 * z2 % P
    e, f, g, h = (b - a) % P, (d - c) % P, (d + c) % P, (b + a) % P
    return e * f % P, g * h % P, f * g % P, e * h % P


def _multiply(point, scalar):
    result = IDENTITY
    while scalar:
        if scalar & 1:
            result = _add(result, point)
        point = _add(point, point)
        scalar >>= 1
    return result


def _equal(first, second):
    return ((first[0] * second[2] - second[0] * first[2]) % P == 0
            and (first[1] * second[2] - second[1] * first[2]) % P == 0)


BASE_POINT = _decode_point(bytes.fromhex("58" + "66" * 31))


def _require_hex64(value, label):
    if type(value) is not str or not _HEX64.fullmatch(value):
        _fail(label + " must be 64 lowercase hexadecimal characters")


@lru_cache(maxsize=4096)
def _validated_public_point(public_hex):
    # This bounded cache is keyed by canonical bytes in this one fixed profile.
    _require_hex64(public_hex, "public key")
    point = _decode_point(bytes.fromhex(public_hex))
    if _equal(point, IDENTITY) or not _equal(_multiply(point, L), IDENTITY):
        _fail("public key must be a nonidentity point in the prime-order subgroup")
    return point


def public_key_valid(public_hex):
    if type(public_hex) is not str:
        return False
    try:
        _validated_public_point(public_hex)
        return True
    except ValidationError:
        return False


def decode_signature(signature_base64):
    if type(signature_base64) is not str or not _B64.fullmatch(signature_base64):
        _fail("signature must be 88-character standard padded base64")
    try:
        signature = base64.b64decode(signature_base64, validate=True)
    except (ValueError, binascii.Error) as exc:
        raise ValidationError("invalid signature base64") from exc
    if len(signature) != 64 or base64.b64encode(signature).decode("ascii") != signature_base64:
        _fail("signature has incorrect length or noncanonical base64 pad bits")
    return signature


def verify_signature(public_hex, message_bytes, signature_base64):
    """Verify pure Ed25519 with canonical points and the exact IAM subgroup rules."""
    if type(public_hex) is not str or type(message_bytes) is not bytes:
        return False
    try:
        public_point = _validated_public_point(public_hex)
        signature = decode_signature(signature_base64)
        r_encoded = signature[:32]
        r_point = _decode_point(r_encoded)
        if not _equal(_multiply(r_point, L), IDENTITY):
            return False
        # R=O is intentionally allowed; A=O is prohibited above.
        s = int.from_bytes(signature[32:], "little")
        if s >= L:
            return False
        k = int.from_bytes(hashlib.sha512(r_encoded + bytes.fromhex(public_hex) + message_bytes).digest(), "little") % L
        return _equal(_multiply(BASE_POINT, s), _add(r_point, _multiply(public_point, k)))
    except ValidationError:
        return False


def _require_fields(obj, expected, label):
    if type(obj) is not dict or obj.keys() != expected:
        _fail(label + " must have exactly its declared fields")


def _validate_common(record):
    if type(record["v"]) is not int or record["v"] != WIRE_VERSION:
        _fail("v must be the integer 2")
    if type(record["m"]) is not int or not 0 <= record["m"] <= MAX_M:
        _fail("m must be a non-negative safe integer")
    for name in ("actor", "target"):
        if not public_key_valid(record[name]):
            _fail(name + " is not a valid public key under this profile")


def validate_record_shape(record):
    """Check intrinsic record rules; no known_trees, prev lookup or graph state."""
    _require_fields(record, RECORD_FIELDS, "record")
    _validate_common(record)
    if type(record["kind"]) is not str or record["kind"] not in ("ACCEPT", "REVOKE", "LEAVE"):
        _fail("unknown record kind")
    if type(record["context"]) is not str or record["context"] not in ("personal", "community"):
        _fail("unknown record context")
    _require_hex64(record["tree"], "tree")
    if record["context"] == "personal" and record["tree"] != record["actor"]:
        _fail("personal tree must equal actor")
    if type(record["prev"]) is not str or (record["prev"] != "" and not _HEX64.fullmatch(record["prev"])):
        _fail("prev must be empty or a 64-character lowercase hex id")
    if type(record["body"]) is not dict or record["body"]:
        _fail("body must be an empty object")
    if record["kind"] == "ACCEPT" and record["actor"] == record["target"]:
        _fail("ACCEPT actor must differ from target")
    if record["kind"] == "LEAVE":
        if record["context"] != "community" or record["actor"] != record["target"]:
            _fail("LEAVE requires community context and target equal to actor")


def record_id(record):
    return hashlib.sha256(ID_PREFIX + canonical_bytes(record)).hexdigest()


def record_message(record):
    return RECORD_PREFIX + canonical_bytes(record)


def validate_envelope_shape(envelope, check_id=True):
    """Return the inner record after shape/encoding/id validation, not signature math."""
    _require_fields(envelope, ENVELOPE_FIELDS, "envelope")
    record = envelope["record"]
    validate_record_shape(record)
    _require_hex64(envelope["id"], "id")
    decode_signature(envelope["sig"])
    if check_id and envelope["id"] != record_id(record):
        _fail("record id does not match IAM2 domain-separated canonical bytes")
    return record


def validate_envelope(envelope):
    """Validate a parsed envelope; use parse_json on wire input to retain token rules."""
    record = validate_envelope_shape(envelope)
    if not verify_signature(record["actor"], record_message(record), envelope["sig"]):
        _fail("invalid Ed25519 signature")
    return record


def validate_bootstrap_descriptor(descriptor):
    """The six-field tree-hash input is a separate closed schema, not a full record."""
    _require_fields(descriptor, DESCRIPTOR_FIELDS, "bootstrap descriptor")
    _validate_common(descriptor)
    if descriptor["kind"] != "ACCEPT" or descriptor["context"] != "community":
        _fail("bootstrap descriptor requires community ACCEPT")
    if descriptor["actor"] == descriptor["target"]:
        _fail("genesis cannot be a bootstrap target")


def bootstrap_descriptor(record):
    validate_record_shape(record)
    descriptor = {key: record[key] for key in DESCRIPTOR_FIELDS}
    validate_bootstrap_descriptor(descriptor)
    return descriptor


def tree_id_from_descriptors(descriptors):
    if type(descriptors) is not list or not descriptors:
        _fail("bootstrap descriptor list must be nonempty")
    for descriptor in descriptors:
        validate_bootstrap_descriptor(descriptor)
    return hashlib.sha256(TREE_PREFIX + canonical_bytes(descriptors)).hexdigest()


def tree_id(bootstrap_records):
    """Compute a tree id from ordered inner records, not stored envelopes.

    Full bootstrap constraints (shared actor/m, target uniqueness, prev, and
    signature validation) belong to iam_graph; this helper computes the hash.
    """
    if type(bootstrap_records) is not list or not bootstrap_records:
        _fail("bootstrap record list must be nonempty")
    return tree_id_from_descriptors([bootstrap_descriptor(record) for record in bootstrap_records])
