"""Helpers for maintaining a VOMS proxy."""

from datetime import datetime
from configparser import ConfigParser
import logging
import os
import subprocess
import time
from functools import cached_property
from typing import Any, NoReturn, Optional, Protocol
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from arcnagios import confargparse
from arcnagios.nagutils import UNKNOWN

log = logging.getLogger(__name__)

class ProxyInitError(EnvironmentError):
    def __init__(self, msg: str, details: Optional[str] = None):
        EnvironmentError.__init__(self, msg)
        self.details = details

def _x509_certificate_not_valid_after(cert_path: str):
    with open(cert_path, mode='rb') as cert_fh:
        data = cert_fh.read()
    backend = default_backend()
    return x509.load_pem_x509_certificate(data, backend).not_valid_after

def _require_voms_proxy(voms: Optional[str],
                        key_path: Optional[str],
                        cert_path: Optional[str],
                        proxy_path: str,
                        min_proxy_lifetime: Optional[float],
                        max_proxy_age: Optional[float]):

    # Check for an existing proxy with sufficient time left.
    log.debug('Checking %s', proxy_path)
    if not os.path.exists(proxy_path):
        must_renew = True
    else:
        must_renew = False
        if not min_proxy_lifetime is None:
            t_xpr = _x509_certificate_not_valid_after(proxy_path)
            t_rem = t_xpr - datetime.now()
            if t_rem.total_seconds() >= min_proxy_lifetime:
                log.debug('The proxy certificate is valid until %s.', t_xpr)
            else:
                log.debug('The proxy certificate expired %s.', t_xpr)
                must_renew = True
        if not max_proxy_age is None:
            age = time.time() - os.stat(proxy_path).st_mtime
            if age > max_proxy_age:
                must_renew = True

    # Renew if needed.
    if must_renew:
        # Make sure the directory exists.
        proxy_dir = os.path.dirname(proxy_path)
        if not os.path.exists(proxy_dir):
            try:
                log.debug('Creading directory %s.', proxy_dir)
                os.makedirs(proxy_dir)
            except OSError as exn:
                raise ProxyInitError('Cannot create parent directory %s for '
                                     'storing X509 proxy: %s'
                                     % (proxy_dir, exn)) from exn
        log.debug('Renewing proxy.')
        cmd = ['arcproxy']
        if not key_path is None:
            cmd.extend(['-K', key_path])
        if not cert_path is None:
            cmd.extend(['-C', cert_path])
        if not voms is None:
            cmd.extend(['-S', voms])
        if not proxy_path is None:
            cmd.extend(['-P', proxy_path])
        with subprocess.Popen(
                cmd,
                stdin=subprocess.DEVNULL,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                encoding='utf-8') as process:
            output, _ = process.communicate()
            if process.returncode:
                raise ProxyInitError(
                        'Failed to initialize proxy: arcproxy exited with %d.'
                            % process.returncode,
                        details = 'Output from %s:\n%s' % (cmd, output))

class NagiosPluginVomsProtocol(Protocol):
    argparser: confargparse.ConfigArgumentParser
    opts: Any
    log: logging.Logger

    @cached_property
    def config(self) -> ConfigParser:
        ...

    def tmpdir(self) -> str:
        ...

    def nagios_exit(self, status_code: int,
                    status_message: Optional[str] = None,
                    subject: Optional[str] = None) -> NoReturn:
        ...

class NagiosPluginVomsMixin(NagiosPluginVomsProtocol):

    def __init__(self):
        super().__init__()
        argp = self.argparser.add_argument_group('VOMS Proxy Options')
        argp.add_argument('--user-proxy', dest = 'user_proxy',
                help = 'Path to a possibly pre-initialized VOMS proxy.')
        argp.add_argument('--user-cert', dest = 'user_cert',
                help = 'Certificate to use for obtaining VOMS proxy.')
        argp.add_argument('--user-key', dest = 'user_key',
                help = 'Certificate key to use for obtaining VOMS proxy.')
        argp.add_argument('--voms', dest = 'voms',
                help = 'VOMS server for Proxy initialization.')
        argp.add_argument('--min-proxy-lifetime', dest = 'min_proxy_lifetime',
                # The default should be strictly larger than the time before
                # jobs are cancelled.  We need at least the time between
                # monitor runs plus grace time.
                default = 7 * 3600,
                help =
                    'The minimum lifetime in seconds of the proxy '
                    'certificate before it is renewed. '
                    'X.509 extensions are not considered, so if they have '
                    'a significatly different lifetime then the certificate '
                    'itself, consider using --max-proxy-age with a '
                    'conservative value. '
                    'This is only effective if a user key and certificate is '
                    'provided. '
                    'This is also only effective if the platform supports '
                    'the cryptography module, otherwise use '
                    '--max-proxy-age.')
        argp.add_argument('--max-proxy-age', dest = 'max_proxy_age',
                help =
                    'The maximum age of the proxy certificate file before '
                    'renewing the proxy. '
                    'If set, this will trigger a proxy renewal independent of '
                    '--min-proxy-lifetime. '
                    'By default this is unset, but if the cryptography is '
                    'missing, 4 * 3600 is assumed where needed.')

    @property
    def voms(self) -> Optional[str]:
        if self.opts.voms:
            return self.opts.voms
        if self.config.has_option('gridproxy', 'default_voms'):
            return self.config.get('gridproxy', 'default_voms')
        return None

    def voms_suffixed(self, name: str, ext: str = ''):
        if self.voms:
            return name + '-' + self.voms + ext
        else:
            return name + ext

    def require_voms_proxy(self) -> Optional[str]:
        key_path = self.opts.user_key
        cert_path = self.opts.user_cert
        proxy_path = self.opts.user_proxy
        voms = self.voms
        for section in ['gridproxy.%s' % voms, 'gridproxy']:
            if not key_path and self.config.has_option(section, 'user_key'):
                key_path = self.config.get(section, 'user_key')
            if not cert_path and self.config.has_option(section, 'user_cert'):
                cert_path = self.config.get(section, 'user_cert')
            if not proxy_path and self.config.has_option(section, 'user_proxy'):
                proxy_path = self.config.get(section, 'user_proxy')

        if key_path or cert_path:
            if not proxy_path:
                proxy_path = os.path.join(self.tmpdir(),
                                self.voms_suffixed('proxy', '.pem'))
            try:
                _require_voms_proxy(
                        voms = voms,
                        key_path = key_path,
                        cert_path = cert_path,
                        proxy_path = proxy_path,
                        min_proxy_lifetime = self.opts.min_proxy_lifetime,
                        max_proxy_age = self.opts.max_proxy_age)
            except ProxyInitError as exn:
                if exn.details:
                    self.log.error(exn.details)
                self.nagios_exit(UNKNOWN, str(exn))

        if proxy_path:
            os.environ['X509_USER_PROXY'] = proxy_path
            return proxy_path
        else:
            return os.getenv('X509_USER_PROXY')
