"""Offline CSR preparation / issued-leaf verification adapted from PR #3's trust boundary.

Never creates a CA, uploads a key, obtains trust from a peer, or deploys a service.
The Manager administrator signs the CSR; installation requires the verify command.
"""
from __future__ import annotations

import argparse
import ipaddress
import os
from pathlib import Path
import stat
import subprocess


class TLSError(RuntimeError):
    pass


def command(*arguments: str) -> bytes:
    try:
        completed = subprocess.run(['openssl', *arguments], check=True, capture_output=True, timeout=10)
        return completed.stdout
    except (OSError, subprocess.SubprocessError):
        # Do not put key paths, certificate bodies or raw subprocess stderr into logs.
        raise TLSError('TLS_OPENSSL_VALIDATION_FAILED') from None


def real_file(path: Path, *, private: bool = False) -> Path:
    if not path.is_absolute() or path.is_symlink() or not path.is_file():
        raise TLSError('TLS_FILE_INVALID')
    if any(parent.is_symlink() for parent in path.parents):
        raise TLSError('TLS_PARENT_SYMLINK')
    if private and stat.S_IMODE(path.stat().st_mode) & 0o077:
        raise TLSError('TLS_PRIVATE_KEY_PERMISSIONS')
    return path


def public_ca(path: Path) -> Path:
    real_file(path)
    raw = path.read_bytes()
    if len(raw) > 128 * 1024 or b'PRIVATE KEY' in raw:
        raise TLSError('TLS_CA_MUST_BE_PUBLIC')
    # This profile expects the approved Manager signing CA, not an arbitrary server leaf.
    description = command('x509', '-in', str(path), '-noout', '-text')
    if b'CA:TRUE' not in description:
        raise TLSError('TLS_CA_CONSTRAINT_REQUIRED')
    return path


def verify(key: Path, certificate: Path, ca: Path, ip: str) -> None:
    ip = str(ipaddress.ip_address(ip))
    real_file(key, private=True)
    real_file(certificate)
    public_ca(ca)
    if command('pkey', '-in', str(key), '-pubout') != command('x509', '-in', str(certificate), '-pubkey', '-noout'):
        raise TLSError('TLS_KEY_MISMATCH')
    # Require an IP SAN, no CN fallback; require leaf EKU/server use and a non-CA leaf.
    san = command('x509', '-in', str(certificate), '-noout', '-ext', 'subjectAltName')
    if b'IP Address:' not in san:
        raise TLSError('TLS_IP_SAN_REQUIRED')
    basic = command('x509', '-in', str(certificate), '-noout', '-ext', 'basicConstraints')
    if b'CA:TRUE' in basic:
        raise TLSError('TLS_LEAF_IS_CA')
    command('x509', '-in', str(certificate), '-noout', '-checkend', '300')
    command('verify', '-CAfile', str(ca), '-no-CApath', '-purpose', 'sslserver',
            '-verify_ip', ip, str(certificate))


def prepare(directory: Path, ip: str) -> tuple[Path, Path]:
    ip = str(ipaddress.ip_address(ip))
    if not directory.is_absolute() or directory.is_symlink() or any(p.is_symlink() for p in directory.parents):
        raise TLSError('TLS_DIRECTORY_INVALID')
    directory.mkdir(mode=0o700, parents=True, exist_ok=True)
    if not directory.is_dir() or stat.S_IMODE(directory.stat().st_mode) & 0o022:
        raise TLSError('TLS_DIRECTORY_WRITABLE_BY_OTHERS')
    key, csr = directory / 'control-tls.key', directory / 'control-tls.csr'
    # Never overwrite pending signing material. Reuse only after verifying key and SAN.
    if csr.exists() or csr.is_symlink():
        real_file(csr)
        real_file(key, private=True)
        command('req', '-in', str(csr), '-verify', '-noout')
        if command('pkey', '-in', str(key), '-pubout') != command('req', '-in', str(csr), '-pubkey', '-noout'):
            raise TLSError('TLS_CSR_KEY_MISMATCH')
        text = command('req', '-in', str(csr), '-text', '-noout').decode('ascii')
        import re
        sans = re.findall(r'IP Address:([^,\s]+)', text)
        if not any(ipaddress.ip_address(value) == ipaddress.ip_address(ip) for value in sans):
            raise TLSError('TLS_CSR_SAN_MISMATCH')
        return key, csr
    if key.exists() or key.is_symlink():
        real_file(key, private=True)
        command('pkey', '-in', str(key), '-check', '-noout')
    else:
        # O_EXCL protects against a simultaneous operator: never replace a private key.
        with key.open('xb') as stream:
            os.fchmod(stream.fileno(), 0o600)
            stream.write(command('genpkey', '-algorithm', 'RSA', '-pkeyopt', 'rsa_keygen_bits:3072'))
            stream.flush()
            os.fsync(stream.fileno())
    output = command('req', '-new', '-key', str(key), '-subj', '/CN=contextra-control',
                     '-addext', f'subjectAltName=IP:{ip}', '-addext', 'extendedKeyUsage=serverAuth')
    with csr.open('xb') as stream:
        os.fchmod(stream.fileno(), 0o600)
        stream.write(output)
        stream.flush()
        os.fsync(stream.fileno())
    return key, csr


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    commands = parser.add_subparsers(dest='action', required=True)
    p = commands.add_parser('prepare')
    p.add_argument('--directory', type=Path, required=True)
    p.add_argument('--ip', required=True)
    p = commands.add_parser('verify')
    for name in ('key', 'certificate', 'ca'):
        p.add_argument('--'+name, type=Path, required=True)
    p.add_argument('--ip', required=True)
    a = parser.parse_args()
    try:
        if a.action == 'prepare':
            prepare(a.directory, a.ip)
            print('CSR_READY_MANAGER_SIGNATURE_REQUIRED')
        else:
            verify(a.key, a.certificate, a.ca, a.ip)
            print('ISSUED_LEAF_VERIFIED')
    except (TLSError, ValueError, OSError):
        print('TLS_PREPARATION_OR_VALIDATION_FAILED')
        raise SystemExit(1) from None


if __name__ == '__main__':
    main()
