#!/usr/bin/env python3
"""Pyvorin Edge Sign CLI.

Sign and verify Edge bundles using Ed25519 signatures and SHA-256 checksums.
"""

import argparse
import base64
import hashlib
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

try:
    from cryptography.hazmat.primitives.asymmetric.ed25519 import (
        Ed25519PrivateKey,
        Ed25519PublicKey,
    )
    from cryptography.hazmat.primitives import serialization

    _HAS_CRYPTO = True
except Exception:  # pragma: no cover
    _HAS_CRYPTO = False


def _hash_file(path: Path) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        while True:
            chunk = f.read(65536)
            if not chunk:
                break
            h.update(chunk)
    return h.hexdigest()


def _walk_bundle(bundle_dir: Path) -> Dict[str, str]:
    files: Dict[str, str] = {}
    for root, _, filenames in os.walk(bundle_dir):
        for fname in filenames:
            fpath = Path(root) / fname
            rel = fpath.relative_to(bundle_dir).as_posix()
            files[rel] = _hash_file(fpath)
    return files


def _sign_payload(files: Dict[str, str], private_key: Any) -> str:
    payload = json.dumps(
        {"files": files}, sort_keys=True, separators=(",", ":")
    ).encode()
    sig = private_key.sign(payload)
    return base64.b64encode(sig).decode()


def _public_key_b64(private_key: Any) -> str:
    pub = private_key.public_key()
    raw = pub.public_bytes(
        encoding=serialization.Encoding.Raw,
        format=serialization.PublicFormat.Raw,
    )
    return base64.b64encode(raw).decode()


def cmd_create_key(args: argparse.Namespace) -> int:
    if not _HAS_CRYPTO:
        print("Error: cryptography library not available", file=sys.stderr)
        return 1
    output_dir = Path(args.output)
    output_dir.mkdir(parents=True, exist_ok=True)
    private_key = Ed25519PrivateKey.generate()
    priv_pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption(),
    )
    pub_pem = private_key.public_key().public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    (output_dir / "private.pem").write_bytes(priv_pem)
    (output_dir / "public.pem").write_bytes(pub_pem)
    print(f"Keys written to {output_dir}")
    return 0


def cmd_sign(args: argparse.Namespace) -> int:
    if not _HAS_CRYPTO:
        print("Error: cryptography library not available", file=sys.stderr)
        return 1
    bundle_dir = Path(args.bundle)
    keyfile = Path(args.key)
    if not bundle_dir.is_dir():
        print(f"Error: bundle not found: {bundle_dir}", file=sys.stderr)
        return 1
    if not keyfile.exists():
        print(f"Error: key file not found: {keyfile}", file=sys.stderr)
        return 1
    private_key = serialization.load_pem_private_key(
        keyfile.read_bytes(), password=None
    )
    if not isinstance(private_key, Ed25519PrivateKey):
        print("Error: key is not Ed25519", file=sys.stderr)
        return 1
    files = _walk_bundle(bundle_dir)
    signature = _sign_payload(files, private_key)
    manifest: Dict[str, Any] = {
        "version": "1.0",
        "created": datetime.now(timezone.utc).isoformat(),
        "files": files,
        "signature": signature,
        "public_key": _public_key_b64(private_key),
    }
    out_path = Path(args.output)
    out_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
    print(f"Manifest written to {out_path}")
    return 0


def cmd_verify(args: argparse.Namespace) -> int:
    if not _HAS_CRYPTO:
        print("Error: cryptography library not available", file=sys.stderr)
        return 1
    bundle_dir = Path(args.bundle)
    manifest_path = Path(args.manifest)
    if not bundle_dir.is_dir():
        print(f"Error: bundle not found: {bundle_dir}", file=sys.stderr)
        return 1
    if not manifest_path.exists():
        print(f"Error: manifest not found: {manifest_path}", file=sys.stderr)
        return 1
    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    try:
        pub_raw = base64.b64decode(manifest["public_key"])
        signature = base64.b64decode(manifest["signature"])
        public_key = Ed25519PublicKey.from_public_bytes(pub_raw)
    except Exception as exc:
        print(f"Error: invalid manifest keys: {exc}", file=sys.stderr)
        return 1
    payload = json.dumps(
        {"files": manifest["files"]}, sort_keys=True, separators=(",", ":")
    ).encode()
    try:
        public_key.verify(signature, payload)
    except Exception:
        print("Error: signature verification failed", file=sys.stderr)
        return 1
    files = _walk_bundle(bundle_dir)
    if files != manifest["files"]:
        print("Error: bundle contents do not match manifest", file=sys.stderr)
        all_paths = set(files.keys()) | set(manifest["files"].keys())
        for p in sorted(all_paths):
            if files.get(p) != manifest["files"].get(p):
                print(f"  mismatch: {p}", file=sys.stderr)
        return 1
    print("Bundle verified successfully")
    return 0


def cmd_check(args: argparse.Namespace) -> int:
    bundle_dir = Path(args.bundle)
    if not bundle_dir.is_dir():
        print(f"Error: bundle not found: {bundle_dir}", file=sys.stderr)
        return 1
    files = _walk_bundle(bundle_dir)
    for rel, digest in sorted(files.items()):
        print(f"{digest}  {rel}")
    return 0


def main(argv: Optional[List[str]] = None) -> int:
    parser = argparse.ArgumentParser(
        prog="pyv-edge-sign",
        description="Pyvorin Edge Sign CLI",
    )
    sub = parser.add_subparsers(dest="command", required=True)

    p_create = sub.add_parser("create-key", help="Generate Ed25519 signing key pair")
    p_create.add_argument(
        "--output", required=True, help="Output directory for keys"
    )
    p_create.set_defaults(func=cmd_create_key)

    p_sign = sub.add_parser("sign", help="Sign a bundle directory")
    p_sign.add_argument("--bundle", required=True, help="Bundle directory")
    p_sign.add_argument("--key", required=True, help="Private key PEM file")
    p_sign.add_argument("--output", required=True, help="Output manifest JSON path")
    p_sign.set_defaults(func=cmd_sign)

    p_verify = sub.add_parser("verify", help="Verify a signed bundle")
    p_verify.add_argument("--bundle", required=True, help="Bundle directory")
    p_verify.add_argument("--manifest", required=True, help="Manifest JSON path")
    p_verify.set_defaults(func=cmd_verify)

    p_check = sub.add_parser("check", help="Check bundle integrity (hash all files)")
    p_check.add_argument("--bundle", required=True, help="Bundle directory")
    p_check.set_defaults(func=cmd_check)

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


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