#!/usr/bin/env python3
"""Build and verify MazeX commit-reveal files.

Commands:
- build-layer: split an allocation JSON into public commitments and private reveals
- verify-layer: verify public Merkle proofs
- verify-reveals: verify private reveals against public commitments

Local hashing uses SHA-256 over canonical JSON. EVM contracts should use a
separate keccak256/ABI-compatible implementation with the same field model.
"""

from __future__ import annotations

import argparse
import hashlib
import json
from pathlib import Path
from typing import Any


def canonical_json(value: Any) -> str:
    return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))


def hash_obj(value: Any) -> str:
    return "0x" + hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()


def hash_text(value: str) -> str:
    return "0x" + hashlib.sha256(value.encode("utf-8")).hexdigest()


def read_json(path: str | Path) -> Any:
    return json.loads(Path(path).read_text(encoding="utf-8"))


def write_json(path: str | Path, value: Any) -> None:
    Path(path).write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8")


def hash_file(path: str | Path) -> str:
    data = Path(path).read_bytes()
    return "0x" + hashlib.sha256(data).hexdigest()


def pair_hash(left: str, right: str) -> str:
    return hash_obj({"left": left, "right": right})


def merkle_root(leaves: list[str]) -> str:
    if not leaves:
        return hash_obj({"empty": True})
    level = leaves[:]
    while len(level) > 1:
        if len(level) % 2 == 1:
            level.append(level[-1])
        level = [pair_hash(level[i], level[i + 1]) for i in range(0, len(level), 2)]
    return level[0]


def merkle_proof(leaves: list[str], index: int) -> list[dict[str, str]]:
    proof: list[dict[str, str]] = []
    idx = index
    level = leaves[:]
    while len(level) > 1:
        if len(level) % 2 == 1:
            level.append(level[-1])
        sibling_idx = idx - 1 if idx % 2 else idx + 1
        position = "left" if idx % 2 else "right"
        proof.append({"position": position, "hash": level[sibling_idx]})
        idx //= 2
        level = [pair_hash(level[i], level[i + 1]) for i in range(0, len(level), 2)]
    return proof


def verify_merkle_proof(leaf: str, proof: list[dict[str, str]], root: str) -> bool:
    current = leaf
    for item in proof:
        if item["position"] == "left":
            current = pair_hash(item["hash"], current)
        elif item["position"] == "right":
            current = pair_hash(current, item["hash"])
        else:
            raise ValueError(f"Invalid proof position: {item['position']}")
    return current == root


def public_step_payload(step: dict[str, Any], reveal: dict[str, Any]) -> dict[str, Any]:
    verification_hash = hash_obj(
        {
            "verification_seed": reveal["verification_seed"],
            "verification_policy_version": "verification-policy-layered",
        }
    )
    return {
        "layer_no": step["layer_no"],
        "step_no": step["step_no"],
        "step_count": step["step_count"],
        "progress_band": step["progress_band"],
        "target_difficulty": step["target_difficulty"],
        "theme_hint": step["theme_hint"],
        "sample_cost_gas": step["sample_cost_gas"],
        "submit_cost_gas": step["submit_cost_gas"],
        "reward_weight": step["reward_weight"],
        "boss_zone": step["boss_zone"],
        "allocator_hash": hash_obj({"allocator_seed": reveal["allocator_seed"]}),
        "formula_hash": step["formula_hash_commit"],
        "verification_hash": verification_hash,
    }


def build_layer(args: argparse.Namespace) -> None:
    allocation = read_json(args.allocation)
    rules = read_json(args.rules)
    allocator_rules_hash = hash_file(args.allocator_rules)
    asset_index_hash = hash_file(args.asset_index)
    commit_reveal_rules_hash = hash_file(args.rules)

    public_steps: list[dict[str, Any]] = []
    private_reveals: list[dict[str, Any]] = []
    leaves: list[str] = []

    for step in allocation["steps"]:
        verification_seed = hash_text(f"{step['formula_seed']}:verification")
        reveal = {
            "layer_no": step["layer_no"],
            "step_no": step["step_no"],
            "asset_id": step["asset_id"],
            "pool_id": step["pool_id"],
            "family_id": step["family_id"],
            "primary_structure": step["primary_structure"],
            "allocator_seed": step["allocator_seed"],
            "formula_seed": step["formula_seed"],
            "parameter_seed": step["parameter_seed"],
            "formula_hash_commit": step["formula_hash_commit"],
            "verification_seed": verification_seed,
        }
        payload = public_step_payload(step, reveal)
        step_commit = hash_obj(payload)
        public_step = {**payload, "step_commit": step_commit}

        public_steps.append(public_step)
        private_reveals.append({**reveal, "step_commit": step_commit})
        leaves.append(step_commit)

    root = merkle_root(leaves)
    for index, step in enumerate(public_steps):
        step["merkle_proof"] = merkle_proof(leaves, index)

    public_layer = {
        "version": rules["version"],
        "hash_mode": rules["hash_mode_local"],
        "layer_no": allocation["layer_no"],
        "step_count": allocation["step_count"],
        "allocator_rules_hash": allocator_rules_hash,
        "asset_index_hash": asset_index_hash,
        "commit_reveal_rules_hash": commit_reveal_rules_hash,
        "layer_root": root,
        "public_steps": public_steps,
    }

    private_layer = {
        "version": rules["version"],
        "warning": "Do not publish this file before the corresponding steps are solved.",
        "layer_no": allocation["layer_no"],
        "step_count": allocation["step_count"],
        "private_reveals": private_reveals,
    }

    public_path = f"{args.out_prefix}_public.json"
    private_path = f"{args.out_prefix}_private_reveals.json"
    write_json(public_path, public_layer)
    write_json(private_path, private_layer)
    print(f"Wrote {public_path}")
    print(f"Wrote {private_path}")
    print(f"layer_root={root}")


def verify_layer(args: argparse.Namespace) -> None:
    public_layer = read_json(args.public)
    root = public_layer["layer_root"]
    failures: list[str] = []
    leaves: list[str] = []

    for step in public_layer["public_steps"]:
        payload = {key: value for key, value in step.items() if key not in {"step_commit", "merkle_proof"}}
        recomputed = hash_obj(payload)
        leaves.append(step["step_commit"])
        if recomputed != step["step_commit"]:
            failures.append(f"step {step['step_no']}: step_commit mismatch")
        if not verify_merkle_proof(step["step_commit"], step["merkle_proof"], root):
            failures.append(f"step {step['step_no']}: merkle proof mismatch")

    recomputed_root = merkle_root(leaves)
    if recomputed_root != root:
        failures.append("layer_root mismatch")

    if failures:
        print("Verification failed:")
        for failure in failures:
            print(f"- {failure}")
        raise SystemExit(1)

    print(f"Layer verified: layer={public_layer['layer_no']} steps={public_layer['step_count']} root={root}")


def verify_reveals(args: argparse.Namespace) -> None:
    public_layer = read_json(args.public)
    private_layer = read_json(args.reveals)
    public_by_step = {step["step_no"]: step for step in public_layer["public_steps"]}
    failures: list[str] = []

    for reveal in private_layer["private_reveals"]:
        step_no = reveal["step_no"]
        public = public_by_step.get(step_no)
        if public is None:
            failures.append(f"step {step_no}: missing public commitment")
            continue

        if public["allocator_hash"] != hash_obj({"allocator_seed": reveal["allocator_seed"]}):
            failures.append(f"step {step_no}: allocator_hash mismatch")

        expected_verification_hash = hash_obj(
            {
                "verification_seed": reveal["verification_seed"],
                "verification_policy_version": "verification-policy-layered",
            }
        )
        if public["verification_hash"] != expected_verification_hash:
            failures.append(f"step {step_no}: verification_hash mismatch")

        if public["formula_hash"] != reveal["formula_hash_commit"]:
            failures.append(f"step {step_no}: formula_hash mismatch")

        payload = {key: value for key, value in public.items() if key not in {"step_commit", "merkle_proof"}}
        if public["step_commit"] != hash_obj(payload):
            failures.append(f"step {step_no}: public step_commit mismatch")

        if reveal["step_commit"] != public["step_commit"]:
            failures.append(f"step {step_no}: reveal step_commit mismatch")

        if not verify_merkle_proof(public["step_commit"], public["merkle_proof"], public_layer["layer_root"]):
            failures.append(f"step {step_no}: merkle proof mismatch")

    if failures:
        print("Reveal verification failed:")
        for failure in failures:
            print(f"- {failure}")
        raise SystemExit(1)

    print(
        f"Reveals verified: layer={private_layer['layer_no']} reveals={len(private_layer['private_reveals'])}"
    )


def main() -> None:
    parser = argparse.ArgumentParser(description="Build and verify MazeX commit-reveal artifacts.")
    sub = parser.add_subparsers(dest="command", required=True)

    build = sub.add_parser("build-layer", help="Build public commitments and private reveals from allocation JSON.")
    build.add_argument("--allocation", required=True)
    build.add_argument("--rules", default="MazeX_commit_reveal_rules.json")
    build.add_argument("--allocator-rules", default="MazeX_formula_allocator_rules.json")
    build.add_argument("--asset-index", default="MazeX_formula_assets.json")
    build.add_argument("--out-prefix", required=True)
    build.set_defaults(func=build_layer)

    verify_public = sub.add_parser("verify-layer", help="Verify public Merkle commitments.")
    verify_public.add_argument("--public", required=True)
    verify_public.set_defaults(func=verify_layer)

    verify_reveal = sub.add_parser("verify-reveals", help="Verify private reveals against public commitments.")
    verify_reveal.add_argument("--public", required=True)
    verify_reveal.add_argument("--reveals", required=True)
    verify_reveal.set_defaults(func=verify_reveals)

    args = parser.parse_args()
    args.func(args)


if __name__ == "__main__":
    main()
