#!/usr/bin/env python3
"""
otp_nostr_30078.py — example: encrypt data with OTP, wrap in a Nostr kind 30078
event, sign it with n_signer, and print the signed event for publishing.

Workflow:
  1. Call n_signer's `otp_encrypt` verb to encrypt plaintext with the bound OTP pad.
  2. Build a Nostr kind 30078 (replaceable parameterized) event with the ASCII-armored
     ciphertext as the `content` field.
  3. Call n_signer's `sign_event` verb to sign the event with the secp256k1 key.
  4. Print the signed event JSON, ready to publish to Nostr relays.

This is a demo — it does not actually publish to a relay. To publish, send the
signed event to your preferred Nostr relay using a library like nostr-tools,
nostril, or nak.

Usage:
  python3 examples/otp_nostr_30078.py "Your secret message here"

Requirements:
  - n_signer running with --otp-pad-dir / --otp-pad bound, and a secp256k1
    role (e.g. "main") available for sign_event.
  - This script connects to n_signer via stdio (one process per request).

See plans/otp_nostr_integration.md for the full design.
"""
import base64
import hashlib
import json
import os
import struct
import subprocess
import sys
import time

NSIGNER = "./build/nsigner"
PAD_DIR = "/media/user/Music/pads"
PAD_SPEC = "333e9902db839d9d"
MNEMONIC_FILE = ".test_mnemonic"
MNEMONIC_TMP = ".test_mnemonic_otp_30078.tmp"


def send_framed(proc, obj):
    payload = json.dumps(obj).encode()
    proc.stdin.write(struct.pack(">I", len(payload)))
    proc.stdin.write(payload)
    proc.stdin.flush()


def recv_framed(proc):
    """Read a framed response, skipping any banner text on stdout."""
    buf = b""
    while True:
        b = proc.stdout.read(1)
        if not b:
            return None
        buf = (buf + b)[-4:]
        if len(buf) < 4:
            continue
        (length,) = struct.unpack(">I", buf)
        if 1 <= length <= 1024 * 1024:
            peek = proc.stdout.read(1)
            if peek == b"{":
                body = peek + proc.stdout.read(length - 1)
                return json.loads(body.decode())
            else:
                buf = (buf + peek)[-4:]


def run_one_request(req_obj):
    """Run nsigner in stdio mode for a single framed request/response."""
    shell_cmd = (
        f"exec 3<{MNEMONIC_TMP}; "
        f"exec {NSIGNER} --listen stdio --mnemonic-fd 3 "
        f"--otp-pad-dir {PAD_DIR} --otp-pad {PAD_SPEC} "
        f"--otp-allow-blkback --allow-all"
    )
    proc = subprocess.Popen(
        ["bash", "-c", shell_cmd],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
    )
    import time as _time
    _time.sleep(1.0)
    if proc.poll() is not None:
        err = proc.stderr.read().decode()
        print(f"ERROR: nsigner exited early (code {proc.returncode})")
        print(f"stderr: {err}")
        return None
    send_framed(proc, req_obj)
    resp = recv_framed(proc)
    proc.stdin.close()
    try:
        proc.wait(timeout=5)
    except subprocess.TimeoutExpired:
        proc.kill()
    return resp


def compute_event_id(event):
    """Compute the Nostr event ID (SHA-256 of the canonical serialized event)."""
    # Nostr event serialization: [0, pubkey, created_at, kind, tags, content]
    serialized = json.dumps([
        0,
        event["pubkey"],
        event["created_at"],
        event["kind"],
        event["tags"],
        event["content"],
    ], separators=(",", ":"), ensure_ascii=False)
    return hashlib.sha256(serialized.encode()).hexdigest()


def main():
    plaintext = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Secret OTP message"
    print(f"Plaintext: {plaintext}")

    # Prepare the mnemonic temp file.
    with open(MNEMONIC_FILE) as f:
        mnemonic = f.read().strip()
    with open(MNEMONIC_TMP, "w") as f:
        f.write(mnemonic + "\n")

    try:
        # Step 1: Get the public key for the "main" role
        print("\n=== Step 1: get_public_key ===")
        resp = run_one_request({
            "id": "1",
            "method": "get_public_key",
            "params": [{"role": "main"}],
        })
        if resp is None or "result" not in resp:
            print("ERROR: get_public_key failed")
            print(f"Response: {resp}")
            return 1
        # The result is a plain hex string for secp256k1 backward compat.
        pubkey_hex = resp["result"].strip('"')
        print(f"Public key: {pubkey_hex}")

        # Step 2: Encrypt the plaintext with OTP
        print("\n=== Step 2: otp_encrypt ===")
        pt_b64 = base64.b64encode(plaintext.encode()).decode()
        resp = run_one_request({
            "id": "2",
            "method": "otp_encrypt",
            "params": [pt_b64, {"encoding": "ascii"}],
        })
        if resp is None or "result" not in resp:
            print("ERROR: otp_encrypt failed")
            print(f"Response: {resp}")
            return 1
        enc_result = json.loads(resp["result"])
        ciphertext = enc_result["ciphertext"]
        pad_chksum = enc_result["pad_chksum"]
        pad_offset = enc_result["pad_offset_after"]
        print(f"Pad checksum: {pad_chksum}")
        print(f"Pad offset after encrypt: {pad_offset}")
        print(f"Ciphertext (first 60 chars): {ciphertext[:60]}...")

        # Step 3: Build the Nostr kind 30078 event
        print("\n=== Step 3: Build kind 30078 event ===")
        # Use a unique d-tag based on the pad checksum and offset.
        d_tag = f"otp-{pad_chksum[:16]}-{pad_offset}"
        event = {
            "pubkey": pubkey_hex,
            "created_at": int(time.time()),
            "kind": 30078,
            "tags": [
                ["d", d_tag],
                ["otp-pad", pad_chksum[:16]],
                ["otp-version", "v0.0.2-otp"],
                ["otp-encoding", "ascii"],
            ],
            "content": ciphertext,
        }
        # Compute the event ID.
        event_id = compute_event_id(event)
        event["id"] = event_id
        print(f"Event ID: {event_id}")
        print(f"d-tag: {d_tag}")

        # Step 4: Sign the event with n_signer
        print("\n=== Step 4: sign_event ===")
        # sign_event expects the event JSON as the first param (without id/sig).
        # The signer computes the id and signature internally.
        event_for_signing = {
            "pubkey": event["pubkey"],
            "created_at": event["created_at"],
            "kind": event["kind"],
            "tags": event["tags"],
            "content": event["content"],
        }
        resp = run_one_request({
            "id": "3",
            "method": "sign_event",
            "params": [json.dumps(event_for_signing), {"role": "main"}],
        })
        if resp is None or "result" not in resp:
            print("ERROR: sign_event failed")
            print(f"Response: {resp}")
            return 1
        sig = resp["result"].strip('"')
        event["sig"] = sig
        print(f"Signature: {sig[:60]}...")

        # Step 5: Print the signed event
        print("\n=== Signed Nostr event (ready to publish) ===")
        print(json.dumps(event, indent=2))
        print(f"\nTo publish: send this event to a Nostr relay.")
        print(f"To decrypt: call otp_decrypt with the content field.")
        return 0
    finally:
        try:
            os.unlink(MNEMONIC_TMP)
        except OSError:
            pass


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