# -*- coding: utf-8 -*-
# ***********************************************************************
# ******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
# *************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
#
#  (c) 2016.                            (c) 2016.
#  Government of Canada                 Gouvernement du Canada
#  National Research Council            Conseil national de recherches
#  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
#  All rights reserved                  Tous droits réservés
#
#  NRC disclaims any warranties,        Le CNRC dénie toute garantie
#  expressed, implied, or               énoncée, implicite ou légale,
#  statutory, of any kind with          de quelque nature que ce
#  respect to the software,             soit, concernant le logiciel,
#  including without limitation         y compris sans restriction
#  any warranty of merchantability      toute garantie de valeur
#  or fitness for a particular          marchande ou de pertinence
#  purpose. NRC shall not be            pour un usage particulier.
#  liable in any event for any          Le CNRC ne pourra en aucun cas
#  damages, whether direct or           être tenu responsable de tout
#  indirect, special or general,        dommage, direct ou indirect,
#  consequential or incidental,         particulier ou général,
#  arising from the use of the          accessoire ou fortuit, résultant
#  software.  Neither the name          de l'utilisation du logiciel. Ni
#  of the National Research             le nom du Conseil National de
#  Council of Canada nor the            Recherches du Canada ni les noms
#  names of its contributors may        de ses  participants ne peuvent
#  be used to endorse or promote        être utilisés pour approuver ou
#  products derived from this           promouvoir les produits dérivés
#  software without specific prior      de ce logiciel sans autorisation
#  written permission.                  préalable et particulière
#                                       par écrit.
#
#  This file is part of the             Ce fichier fait partie du projet
#  OpenCADC project.                    OpenCADC.
#
#  OpenCADC is free software:           OpenCADC est un logiciel libre ;
#  you can redistribute it and/or       vous pouvez le redistribuer ou le
#  modify it under the terms of         modifier suivant les termes de
#  the GNU Affero General Public        la “GNU Affero General Public
#  License as published by the          License” telle que publiée
#  Free Software Foundation,            par la Free Software Foundation
#  either version 3 of the              : soit la version 3 de cette
#  License, or (at your option)         licence, soit (à votre gré)
#  any later version.                   toute version ultérieure.
#
#  OpenCADC is distributed in the       OpenCADC est distribué
#  hope that it will be useful,         dans l’espoir qu’il vous
#  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
#  without even the implied             GARANTIE : sans même la garantie
#  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
#  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
#  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
#  General Public License for           Générale Publique GNU Affero
#  more details.                        pour plus de détails.
#
#  You should have received             Vous devriez avoir reçu une
#  a copy of the GNU Affero             copie de la Licence Générale
#  General Public License along         Publique GNU Affero avec
#  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
#  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
#                                       <http://www.gnu.org/licenses/>.
#
#
# ***********************************************************************

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import getpass
import netrc as netrclib
import argparse
import os
import signal
import sys
import requests
import logging

CERT_ENDPOINT = "/cred/proxyCert"
CERT_SERVER = "www.canfar.phys.uvic.ca"

__all__ = ['get_cert', 'Subject']


class Subject(object):
    """
    Class that stores user authentication information. For now, it includes: username,
        X509 certificate file or netrc file that stores user/password.
    """

    def __init__(self, username=None, certificate=None, netrc=False):
        """
            The subject is anonymous if neither of this arguments is set
        :param username: user name
        :param certificate: name of the X509 certificate file
        :param netrc: use information from .netrc. Value can be True (use default $HOME/.netrc)
                    or the name of the netrc file to use.
        """
        self.logger = logging.getLogger('Subject')
        self.username = username
        self._hosts_auth = {}
        self._certificate = None
        self.certificate = certificate
        self._netrc = None
        self.netrc = netrc


    @property
    def certificate(self):
        """
        X509 certificate associated with the subject
        :return: name of the X509 certificate file
        """
        return self._certificate

    @certificate.setter
    def certificate(self, value):
        if value is not None:
            assert value is not '' and os.path.isfile(value)
            self._certificate = value

    @property
    def netrc(self):
        """
        netrc file containing user/passwds credentials (if used by subject)
        :return: Name of netrc file
        """
        return self._netrc

    @netrc.setter
    def netrc(self, value):
        if value is not False:
            hosts = netrclib.netrc(value if value is not True else None).hosts
            if value is True:
                self._netrc = os.path.join(os.environ['HOME'], ".netrc")
            else:
                self._netrc = value
            for host_name in hosts:
                self._hosts_auth[host_name] = (hosts[host_name][0], hosts[host_name][2])

    @property
    def anon(self):
        """
        Is this an anonymous subject (has no authentication means)?
        :return:
        """
        return (self.certificate is None) and (self.netrc is None) \
        and (self.username is None)

    @staticmethod
    def from_cmd_line_args(args):
        """
        Instantiates a subject based on attributes of a command line. It works with the base parser in cadcutils.
        and uses the following command line arguments:
            args.user: username
            args.cert: x509 certificate location
            args.n: use netrc files for authentication info
            args.netrc_file: use this netrc file for authentication info
        :param args: argparse command line arguments
        :return: corresponding subject
        """
        return Subject(username=args.user, certificate=args.cert,
                       netrc=(args.netrc_file if args.netrc_file is not None else args.n))

    def get_auth(self, realm):
        """
        Returns a user/password touple for the given realm. Note that this function prompts for the
        password on stdout when the username of the subject is known but no correponding password can be found

        :param realm: realm for the authentication
        :return: (username, password) touple or None if subject is anonymous or password not found.
        """
        if self.anon:
            return None
        if self.username is None:
            if realm in self._hosts_auth:
                return self._hosts_auth[realm]
            else:
                msg = 'No user/password for {}'.format(realm)
                if self.netrc is not False:
                    msg = '{} in {}'.format(msg, self.netrc if self.netrc is not True else '$HOME/.netrc')
                self.logger.error(msg)
                return None
        else:
            if realm in self._hosts_auth \
                    and self.username == self._hosts_auth[realm][0]:
                return self._hosts_auth[realm]
            sys.stdout.write("Password for {}@{}: ".format(self.username, realm))
            self._hosts_auth[realm] = (self.username, getpass.getpass().strip('\n'))
            return self._hosts_auth[realm]


def get_cert(subject, cert_server=None,
             cert_endpoint=None, **kwargs):
    """Access the CADC Certificate Delegation Protocol (CDP) server and retrieve
       a X509 proxy certificate.

    :param: subject: subject performing the action
    :ptype: cadcutils.subject
    :param cert_server: the http server that will provide the certificate
    :ptype cert_server: str
    :param cert_endpoint: the endpoint on the server where the certificate service is
    :ptype cert_endpoint: str
    :param kwargs: not really any, but maybe daysValid.
    :ptype daysValid: int

    :return content of the certificate

    """

    cert_server = cert_server is None and CERT_SERVER or cert_server
    cert_endpoint = cert_endpoint is None and CERT_ENDPOINT or cert_endpoint
    if subject.username is None:
        sys.stdout.write("CADC user ID: ")
        subject.username = sys.stdin.readline().strip('\n')

    username, passwd = subject.get_auth(cert_server)

    url = "http://{0}/{1}".format(cert_server, cert_endpoint)
    resp = requests.get(url, params=kwargs, auth=(username, passwd))
    resp.raise_for_status()
    logging.getLogger('get_Cert').debug('Saved user {} certificate'.format(username))
    return resp.content

def get_cert_main():
    """ Client to download an X509 certificate and save it in users home directory"""

    def _signal_handler(signal, frame):
        sys.stderr.write("\n")
        sys.exit(-1)

    signal.signal(signal.SIGINT, _signal_handler)

    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,
                                     description=("Retrieve a security certificate for interaction with a Web service "
                                                  "such as VOSpace. Certificate will be valid for daysValid and stored "
                                                  "as local file cert_filename. First looks for an entry in the users "
                                                  ".netrc  matching the realm {0}, the user is prompted for a username "
                                                  "and password if no entry is found.".format(CERT_SERVER)))

    parser.add_argument('--cert-filename',
                        default=os.path.join(os.getenv('HOME', '/tmp'), '.ssl/cadcproxy.pem'),
                        help="Filesystem location to store the proxy certificate.")
    parser.add_argument('--cert-server',
                        default=CERT_SERVER,
                        help="Certificate server network address.")
    parser.add_argument('--daysValid', type=int, default=10, help='Number of days the certificate should be valid.')
    parser.add_argument('-u', '--user', type=str, help='CADC user ID associated with the certificate',
                        required=False)

    args = parser.parse_args()

    dirname = os.path.dirname(args.cert_filename)
    try:
        os.makedirs(dirname)
    except OSError as oex:
        if os.path.isdir(dirname):
            pass
        elif oex.errno == 20 or oex.errno == 17:
            sys.stderr.write("%s : %s\n" % (str(oex), dirname))
            sys.stderr.write("Expected %s to be a directory.\n" % dirname)
            sys.exit(oex.errno)
        else:
            raise oex

    retry = True
    while retry:
        try:
            subject = Subject(netrc=True, username=args.user)
            cert = get_cert(subject, cert_server=args.cert_server,
                            daysValid=args.daysValid)
            with open(args.cert_filename, 'w') as w:
                w.write(cert)
            retry = False
        except OSError as ose:
            if ose.errno != 401:
                sys.stderr.write(str(ose))
                return getattr(ose, 'errno', 1)
            else:
                sys.stderr.write("Access denied\n")
        except Exception as ex:
            sys.stderr.write(str(ex))
            return getattr(ex, 'errno', 1)
