"""Canonical NEOedgeX -> GROSSO ingress document.

No component re-signs an edge vote.  This module only joins four already-existing public
facts: the local proposal, the register intent, the two carried votes, and the resulting
HANDSHAKE_MADE event.  H2 independently verifies the original signatures and every binding.
"""

from __future__ import annotations

import hashlib

from .canonical import canonical_bytes
from .market_worker import CONSENT_SCHEMA, Proposal

INGRESS_SCHEMA = "neoedgex-grosso-ingress.v1"
GROSSO_TERMS_SCHEMA = "neoedgex-grosso-terms.v1"

_INGRESS_FIELDS = {"schema_v", "proposal", "intent", "votes_carried", "handshake"}
_INTENT_FIELDS = {"schema", "intent_id", "offer_ref", "terms_hash", "connectors",
                  "process_version", "created_at"}
_VOTE_FIELDS = {"intent_id", "connector_id", "decision", "vote_digest", "at",
                "signature", "signature_suite", "txn_id"}
_HANDSHAKE_FIELDS = {"event_type", "ref", "occurred_at", "actor", "counterparty",
                     "parent", "payload"}
_HANDSHAKE_PAYLOAD_FIELDS = {"terms_hash", "vote_a_digest", "vote_b_digest",
                             "offer_version"}
_GROSSO_TERMS_FIELDS = {"schema_v", "buyer_id", "seller_id", "goods", "qty",
                        "unit_price_grosso", "amount_grosso", "synthetic", "currency"}


class GrossoIngressError(ValueError):
    """The public readbacks cannot describe one signed settlement."""


def build_grosso_ingress(handshake: object, intent_view: object,
                         proposal: Proposal) -> dict:
    """Join public readbacks without asserting that their signatures are valid.

    Signature verification belongs to H2, which owns the pinned cohort key registry.  The
    builder is still strict: a byte-different proposal, mismatched transaction identity,
    incomplete vote set, or non-GROSSO terms never leave the edge as a settlement request.
    """
    proposal_document = proposal.body()
    terms = proposal_document.get("terms")
    if proposal_document.get("schema") != CONSENT_SCHEMA \
            or not isinstance(terms, dict) or set(terms) != _GROSSO_TERMS_FIELDS \
            or terms.get("schema_v") != GROSSO_TERMS_SCHEMA \
            or terms.get("currency") != "GROSSO" or terms.get("synthetic") is not True:
        raise GrossoIngressError("GROSSO_TERMS_INVALID")
    buyer, seller = terms.get("buyer_id"), terms.get("seller_id")
    if not isinstance(buyer, str) or not isinstance(seller, str) or buyer == seller \
            or sorted((buyer, seller)) != proposal_document.get("parties"):
        raise GrossoIngressError("GROSSO_PARTIES_INVALID")
    qty, unit, amount = terms.get("qty"), terms.get("unit_price_grosso"), \
        terms.get("amount_grosso")
    if any(not isinstance(value, int) or isinstance(value, bool) or value < 1
           for value in (qty, unit, amount)) or qty * unit != amount:
        raise GrossoIngressError("GROSSO_AMOUNT_INVALID")

    if not isinstance(intent_view, dict) or set(intent_view) != {
            "intent", "state", "votes_carried"} or intent_view.get("state") != "SIGNED":
        raise GrossoIngressError("INTENT_VIEW_INVALID")
    intent = intent_view["intent"]
    votes = intent_view["votes_carried"]
    if not isinstance(intent, dict) or set(intent) != _INTENT_FIELDS \
            or intent.get("schema") != "NEO_REGISTER_SIGNING_INTENT_V1":
        raise GrossoIngressError("INTENT_INVALID")
    if intent.get("offer_ref") != proposal.offer_ref \
            or intent.get("process_version") != proposal.process_version \
            or intent.get("terms_hash") != proposal.digest():
        raise GrossoIngressError("INTENT_PROPOSAL_MISMATCH")
    connectors = intent.get("connectors")
    if not isinstance(connectors, list) or len(connectors) != 2 \
            or connectors != sorted(connectors) or len(set(connectors)) != 2:
        raise GrossoIngressError("INTENT_CONNECTORS_INVALID")

    if not isinstance(votes, list) or len(votes) != 2:
        raise GrossoIngressError("VOTE_COUNT_INVALID")
    ordered_votes = sorted(votes, key=lambda vote: vote.get("connector_id", "")
                           if isinstance(vote, dict) else "")
    if any(not isinstance(vote, dict) or set(vote) != _VOTE_FIELDS for vote in ordered_votes):
        raise GrossoIngressError("VOTE_FIELD_SET_INVALID")
    if [vote["connector_id"] for vote in ordered_votes] != connectors \
            or any(vote["intent_id"] != intent["intent_id"]
                   or vote["decision"] != "YES"
                   or vote["signature_suite"] != "ed25519-v1"
                   or vote["txn_id"] != proposal.txn_id()
                   for vote in ordered_votes):
        raise GrossoIngressError("VOTE_BINDING_INVALID")

    if not isinstance(handshake, dict) or set(handshake) != _HANDSHAKE_FIELDS \
            or handshake.get("event_type") != "HANDSHAKE_MADE":
        raise GrossoIngressError("HANDSHAKE_INVALID")
    payload = handshake.get("payload")
    if not isinstance(payload, dict) or set(payload) != _HANDSHAKE_PAYLOAD_FIELDS:
        raise GrossoIngressError("HANDSHAKE_PAYLOAD_INVALID")
    occurred_at = handshake.get("occurred_at")
    vote_times = [vote["at"] for vote in ordered_votes]
    if any(not isinstance(at, str) or not at for at in vote_times) \
            or not isinstance(occurred_at, str) or occurred_at != max(vote_times) \
            or not isinstance(payload.get("offer_version"), int) \
            or isinstance(payload.get("offer_version"), bool) \
            or payload["offer_version"] < 1:
        raise GrossoIngressError("HANDSHAKE_TIME_OR_VERSION_INVALID")
    if handshake.get("ref") != proposal.txn_id() or handshake.get("parent") != proposal.offer_ref \
            or handshake.get("actor") != seller or handshake.get("counterparty") != buyer \
            or payload.get("terms_hash") != proposal.digest() \
            or {payload.get("vote_a_digest"), payload.get("vote_b_digest")} != {
                vote["vote_digest"] for vote in ordered_votes}:
        raise GrossoIngressError("HANDSHAKE_BINDING_INVALID")

    document = {"schema_v": INGRESS_SCHEMA, "proposal": proposal_document,
                "intent": intent, "votes_carried": ordered_votes, "handshake": handshake}
    if set(document) != _INGRESS_FIELDS:  # pragma: no cover - construction invariant
        raise GrossoIngressError("INGRESS_FIELD_SET_INVALID")
    # Force canonicalizability before a caller crosses a process boundary.
    hashlib.sha256(canonical_bytes(document)).digest()
    return document
