#!/usr/bin/env python3
"""
X-Road Member CSR Generator
----------------------------
Guides a member through generating a signing or authentication CSR
using the signer-console utility on an X-Road Security Server.

Usage:
    python3 xroad_generate_csr.py
    python3 xroad_generate_csr.py --non-interactive  # use env vars / args
"""

import argparse
import os
import subprocess
import sys
from datetime import datetime
from pathlib import Path


# ── helpers ──────────────────────────────────────────────────────────────────

def run(cmd: list[str], input_text: str = None) -> tuple[int, str, str]:
    """Run signer-console command, return (returncode, stdout, stderr)."""
    try:
        result = subprocess.run(
            ["signer-console"] + cmd,
            input=input_text,
            capture_output=True,
            text=True,
        )
        return result.returncode, result.stdout.strip(), result.stderr.strip()
    except FileNotFoundError:
        print("[ERROR] 'signer-console' not found. Is xroad-signer installed and running?")
        sys.exit(1)


def require_ok(returncode: int, stdout: str, stderr: str, action: str):
    if returncode != 0:
        print(f"[ERROR] {action} failed.")
        if stderr:
            print(f"        {stderr}")
            if "ClientId$Conf" in stderr:
                print("")
                print("        signer-console accepted the command shape, but rejected the")
                print("        member identifier value.")
                print('        Expected form: "<instance> <class> <code>"')
                print("        Do not include literal quote characters in the value.")
                print("        Use the exact X-Road member identifier configured in the")
                print("        security server. Do not use a subsystem code or server code.")
        sys.exit(1)


def prompt(label: str, default: str = None, secret: bool = False) -> str:
    import getpass
    suffix = f" [{default}]" if default else ""
    display = f"{label}{suffix}: "
    if secret:
        value = getpass.getpass(display)
    else:
        value = input(display).strip()
    if not value and default:
        return default
    if not value:
        print(f"[ERROR] {label} is required.")
        sys.exit(1)
    return value


def section(title: str):
    print(f"\n{'─' * 50}")
    print(f"  {title}")
    print(f"{'─' * 50}")


# ── steps ────────────────────────────────────────────────────────────────────

def check_signer_running():
    """Verify xroad-signer service is active."""
    result = subprocess.run(
        ["systemctl", "is-active", "xroad-signer"],
        capture_output=True, text=True
    )
    if result.stdout.strip() != "active":
        print("[WARNING] xroad-signer service does not appear to be running.")
        print("          Run: sudo systemctl start xroad-signer")
        ans = input("Continue anyway? (y/N): ").strip().lower()
        if ans != "y":
            sys.exit(1)


def list_tokens() -> list[dict]:
    """Parse token list from signer-console."""
    rc, out, err = run(["list-tokens"])
    require_ok(rc, out, err, "list-tokens")

    tokens = []
    for line in out.splitlines():
        line = line.strip()
        if not line:
            continue
        # typical output: "Token: 0 (Software token, status: OK, active: true)"
        parts = line.split()
        if parts and parts[0] == "Token:":
            tokens.append({"id": parts[1], "raw": line})
    return tokens


def login_token(token_id: str, pin: str):
    """Log in to a token with PIN."""
    rc, out, err = run(["login-token", token_id], input_text=pin + "\n")
    require_ok(rc, out, err, f"login-token {token_id}")
    print(f"[OK] Logged in to token {token_id}")


def list_keys() -> str:
    """Return raw key listing."""
    rc, out, err = run(["list-keys"])
    require_ok(rc, out, err, "list-keys")
    return out


def generate_csr(key_id: str, member_id: str, usage: str,
                 subject_dn: str, fmt: str, output_dir: Path) -> Path:
    """
    Generate CSR. Returns path to generated file.
    usage: 's' (signing) or 'a' (authentication)
    fmt:   'pem' or 'der'
    """
    rc, out, err = run([
        "generate-cert-request",
        key_id,
        member_id,
        usage,
        subject_dn,
        fmt,
    ])
    require_ok(rc, out, err, "generate-cert-request")

    # signer-console writes the file to CWD and prints the filename
    # output looks like: "Certificate request saved to: <filename>"
    csr_filename = None
    for line in out.splitlines():
        if line.strip():
            # last non-empty token is usually the filename
            csr_filename = line.strip().split()[-1]
            break

    if not csr_filename or not Path(csr_filename).exists():
        # fallback: find newest .pem/.der in cwd
        pattern = f"*.{fmt}"
        candidates = sorted(Path(".").glob(pattern), key=os.path.getmtime, reverse=True)
        if candidates:
            csr_filename = str(candidates[0])
        else:
            print(f"[ERROR] Could not locate generated CSR file. signer-console output:\n{out}")
            sys.exit(1)

    src = Path(csr_filename)
    dst = output_dir / src.name
    src.rename(dst)
    return dst


# ── main ─────────────────────────────────────────────────────────────────────

def parse_args():
    p = argparse.ArgumentParser(description="X-Road Member CSR Generator")
    p.add_argument("--member-id",  help='Raw X-Road member identifier, e.g. "EE GOV 12345678"')
    p.add_argument("--instance",   help="X-Road instance (e.g. EE, BD-TEST)")
    p.add_argument("--class",      dest="member_class", help="Member class (e.g. COM, GOV)")
    p.add_argument("--code",       help="Member code (e.g. BCC)")
    p.add_argument("--cn",         help="CN for subject DN (defaults to member code)")
    p.add_argument("--key-id",     help="Key ID to use (skip interactive key selection)")
    p.add_argument("--usage",      choices=["s", "a"], default="s",
                                   help="Key usage: s=signing, a=authentication (default: s)")
    p.add_argument("--format",     choices=["pem", "der"], default="pem",
                                   help="CSR format (default: pem)")
    p.add_argument("--token-id",   default="0", help="Token ID to log in to (default: 0)")
    p.add_argument("--output-dir", default=".", help="Directory to save CSR (default: cwd)")
    p.add_argument("--non-interactive", action="store_true",
                   help="Skip prompts; use args/env vars only")
    return p.parse_args()


def main():
    args = parse_args()
    ni = args.non_interactive

    print("\n╔══════════════════════════════════════╗")
    print("║   X-Road Member CSR Generator        ║")
    print("╚══════════════════════════════════════╝")

    # 1. Check signer is up
    check_signer_running()

    # 2. Collect member info
    section("Member Information")
    if args.member_id:
        member_id = args.member_id.strip()
        parts = member_id.split()
        if len(parts) != 3:
            print('[ERROR] --member-id must contain exactly three space-separated parts:')
            print('        "<instance> <class> <code>"')
            sys.exit(1)
        instance, member_class, code = parts
    else:
        instance     = args.instance     or prompt("X-Road instance (e.g. EE, BD-TEST)")
        member_class = args.member_class or prompt("Member class   (e.g. COM, GOV, MUN)")
        code         = args.code         or prompt("Member code    (e.g. BCC)")
        member_id = f"{instance} {member_class} {code}"

    cn = args.cn or (code if ni else prompt("CN (Common Name)", default=code))
    subject_dn = f"C={instance},O={member_class},CN={cn}"
    usage_label = "signing" if args.usage == "s" else "authentication"

    print(f"\n  Member ID  : {member_id}")
    print(f"  Subject DN : {subject_dn}")
    print(f"  Usage      : {usage_label}")

    # 3. Token login
    section("Token Login")
    token_id = args.token_id
    pin = os.environ.get("XROAD_TOKEN_PIN") or prompt("Token PIN", secret=True)
    login_token(token_id, pin)

    # 4. Key selection
    section("Key Selection")
    if args.key_id:
        key_id = args.key_id
        print(f"  Using key: {key_id}")
    else:
        print("Available keys:\n")
        print(list_keys())
        key_id = prompt("Enter Key ID to use for CSR")

    # 5. Output dir
    output_dir = Path(args.output_dir).expanduser().resolve()
    output_dir.mkdir(parents=True, exist_ok=True)

    # 6. Generate CSR
    section("Generating CSR")
    print(f"  Key ID     : {key_id}")
    print(f"  Format     : {args.format.upper()}")
    print(f"  Output dir : {output_dir}")

    csr_path = generate_csr(
        key_id=key_id,
        member_id=member_id,
        usage=args.usage,
        subject_dn=subject_dn,
        fmt=args.format,
        output_dir=output_dir,
    )

    # 7. Done
    section("Done")
    print(f"  CSR saved to: {csr_path}")
    print(f"\n  Next steps:")
    print(f"    1. Send '{csr_path.name}' to your CA for signing")
    print(f"    2. Once you receive the signed cert, import it:")
    print(f"       signer-console import-cert <signed-cert-file>")
    print()


if __name__ == "__main__":
    main()
