#!/usr/bin/env python3
"""Judgement Spine - Evidence Pack Integrity Verifier (Public)

- Verifies every file hash listed in manifest.json (SHA-256)
- Verifies manifest.json hash matches manifest.sha256
- Verifies signature.sig over manifest.sha256 (RSA + SHA-256)
  - Uses 'cryptography' if available
  - Falls back to 'openssl' if available
"""

from __future__ import annotations

import base64
import hashlib
import json
import os
import subprocess
import sys
import tempfile
from typing import Dict, List, Tuple


ROOT = os.path.dirname(os.path.abspath(__file__))


def sha256_file(path: str) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()


def load_text(path: str) -> str:
    with open(path, "r", encoding="utf-8") as f:
        return f.read()


def load_bytes(path: str) -> bytes:
    with open(path, "rb") as f:
        return f.read()


def verify_signature_with_cryptography(pub_pem: bytes, sig: bytes, msg: bytes) -> Tuple[bool, str]:
    try:
        from cryptography.hazmat.primitives import hashes, serialization
        from cryptography.hazmat.primitives.asymmetric import padding
    except Exception as e:
        return (False, f"cryptography import failed: {e}")

    try:
        public_key = serialization.load_pem_public_key(pub_pem)
        public_key.verify(sig, msg, padding.PKCS1v15(), hashes.SHA256())
        return (True, "signature verified (cryptography)")
    except Exception as e:
        return (False, f"signature verification failed (cryptography): {e}")


def verify_signature_with_openssl(pub_key_path: str, sig: bytes, msg_path: str) -> Tuple[bool, str]:
    # openssl expects a binary signature file
    try:
        with tempfile.NamedTemporaryFile(delete=False) as tf:
            tf.write(sig)
            sig_path = tf.name

        # Verify PKCS#1 v1.5 signature over SHA-256 digest of msg file
        # Command: openssl dgst -sha256 -verify public_key.pem -signature sig.bin manifest.sha256
        cmd = ["openssl", "dgst", "-sha256", "-verify", pub_key_path, "-signature", sig_path, msg_path]
        proc = subprocess.run(cmd, capture_output=True, text=True)
        out = (proc.stdout or "").strip()
        err = (proc.stderr or "").strip()

        if proc.returncode == 0 and "Verified OK" in out:
            return (True, "signature verified (openssl)")
        # Some OpenSSL builds write to stdout without that exact string.
        if proc.returncode == 0:
            return (True, f"signature verified (openssl): {out or 'OK'}")
        return (False, f"signature verification failed (openssl): {out or err or 'non-zero exit'}")
    except FileNotFoundError:
        return (False, "openssl not found")
    except Exception as e:
        return (False, f"openssl verification error: {e}")
    finally:
        try:
            if 'sig_path' in locals() and os.path.exists(sig_path):
                os.unlink(sig_path)
        except Exception:
            pass


def main() -> int:
    manifest_path = os.path.join(ROOT, "manifest.json")
    manifest_sha_path = os.path.join(ROOT, "manifest.sha256")
    signature_path = os.path.join(ROOT, "signature.sig")
    public_key_path = os.path.join(ROOT, "public_key.pem")

    missing = [p for p in [manifest_path, manifest_sha_path, signature_path, public_key_path] if not os.path.exists(p)]
    if missing:
        print("FAIL: required integrity files are missing:")
        for p in missing:
            print(f"  - {p}")
        return 2

    # Load manifest
    try:
        manifest = json.loads(load_text(manifest_path))
    except Exception as e:
        print(f"FAIL: could not parse manifest.json: {e}")
        return 2

    files = manifest.get("files", [])
    if not isinstance(files, list) or not files:
        print("FAIL: manifest.json does not contain a valid 'files' list.")
        return 2

    # Verify file hashes
    hash_failures: List[str] = []
    missing_files: List[str] = []

    for entry in files:
        rel = entry.get("path")
        expected = entry.get("sha256")
        if not rel or not expected:
            continue
        abs_path = os.path.join(ROOT, rel)
        if not os.path.exists(abs_path):
            missing_files.append(rel)
            continue
        actual = sha256_file(abs_path)
        if actual.lower() != str(expected).lower():
            hash_failures.append(rel)

    # Verify manifest.sha256 matches manifest.json
    expected_manifest_sha = load_text(manifest_sha_path).strip()
    actual_manifest_sha = sha256_file(manifest_path)
    manifest_hash_ok = (expected_manifest_sha.lower() == actual_manifest_sha.lower())

    # Verify signature over the *bytes* of manifest.sha256 (including trailing newline if present)
    sig_b64 = load_text(signature_path).strip()
    try:
        sig = base64.b64decode(sig_b64)
    except Exception as e:
        print(f"FAIL: signature.sig is not valid base64: {e}")
        return 2

    pub_pem = load_bytes(public_key_path)
    msg_bytes = load_bytes(manifest_sha_path)

    sig_ok, sig_msg = verify_signature_with_cryptography(pub_pem, sig, msg_bytes)
    if not sig_ok:
        # fall back to openssl
        sig_ok, sig_msg = verify_signature_with_openssl(public_key_path, sig, manifest_sha_path)

    # Print summary
    print("Judgement Spine - Integrity Verification")
    print("--------------------------------------")
    print(f"Files in manifest: {len(files)}")
    print(f"Missing files:     {len(missing_files)}")
    print(f"Hash mismatches:   {len(hash_failures)}")
    print(f"Manifest hash:     {'OK' if manifest_hash_ok else 'FAIL'}")
    print(f"Signature:         {'OK' if sig_ok else 'SKIPPED/FAIL'} ({sig_msg})")
    print("")

    if missing_files:
        print("Missing files:")
        for rel in missing_files:
            print(f"  - {rel}")
        print("")

    if hash_failures:
        print("Hash mismatches:")
        for rel in hash_failures:
            print(f"  - {rel}")
        print("")

    if missing_files or hash_failures or not manifest_hash_ok:
        print("FAIL: integrity verification did not pass.")
        return 2

    # If signature cannot be verified, still allow PASS but warn.
    if not sig_ok:
        print("PASS (hashes verified; signature NOT verified - install 'cryptography' or OpenSSL to verify signature).")
        return 0

    print("PASS (hashes + signature verified).")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
