#!/usr/bin/python3
#
# Copyright 2018-2019 InternetNZ Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
#    contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import sys
import os
import re
import types
import requests as http_request
import json
from urllib.parse import parse_qsl
import traceback


parent_dir = os.path.dirname(sys.argv[0])
rest_certificate = 'registry/srs-root-ca.pem'


class ArgumentError(Exception):
    pass


class InternalError(Exception):
    pass


class GetOptions():
    def __init__(self, arguments):
        self.last_option = None
        self.arguments = arguments

    def option(self, allowed = None):
        if not len(self.arguments):
            return None

        option = self.arguments[0]

        if option[0:2] != '--':
            raise ArgumentError('Non-option \'{}\' found'.format(option))

        option = option[2:]

        if allowed is not None and option not in allowed:
            return None

        self.arguments.pop(0)
        self.last_option = option

        return option

    def argument(self, required, pattern = None):
        if len(self.arguments):
            argument = self.arguments[0]
        else:
            argument = None

        if argument is None or argument[0:2] == '--':
            if required:
                if self.last_option:
                    raise ArgumentError('Option --{} requires an argument'.format(self.last_option))

                raise ArgumentError('An argument is required')

            return None

        if pattern and not re.search(pattern, argument):
            if self.last_option:
                raise ArgumentError('Invalid --{} argument: {}'.format(self.last_option, argument))

            raise ArgumentError('Invalid first argument')

        self.arguments.pop(0)

        return argument


def parse_arguments():
    arguments = {}

    getopt = GetOptions(sys.argv[1:])
    action = getopt.argument(False)

    if action not in ('availability', 'droplist', 'help', 'version'):
        raise ArgumentError('Invalid action, run \'{} help\' to see list of available actions'.format(sys.argv[0]))

    if action in ('help', 'version'):
        if action == 'help':
            argument = getopt.argument(False)

            if argument is not None:
                action = argument

        return 'help', {'help': action}

    while True:
        option = getopt.option()

        if option is None:
            break

        # global
        if option == 'server':
            arguments['server_hostname'] = getopt.argument(True)
        elif option == 'certificate':
            arguments['server_certificate'] = getopt.argument(True)
        elif option == 'timeout':
            arguments['timeout'] = int(getopt.argument(True, r'^(?x) [0-9]+ $'))
        elif option == 'json':
            arguments['output_json'] = True

        # availability
        elif option in 'domain':
            arguments['domain_names'] = []
            required = True
                
            while True:
                argument = getopt.argument(required, r'(?ix) ^ [a-z0-9āēīōū\.\-]+ $')

                if argument is None:
                    break

                arguments['domain_names'].append(str(argument.encode('idna'), 'utf-8'))
                required = False
                
        elif option == 'search':
            argument = getopt.argument(True,  r'(?ix) ^ [a-z0-9āēīōū-]+ $')
            arguments['search'] = str(argument.encode('idna'), 'utf-8')
           
        else:
            raise ArgumentError('Unknown option \'{}\' found'.format(option))

    return action, arguments


def check_arguments(action, arguments):
    if 'server_hostname' not in arguments:
        arguments['server_hostname'] = os.environ.get('REST_SERVER')

        if arguments['server_hostname'] is None:
            raise ArgumentError('No --server specified')

    if 'server_certificate' not in arguments:
        arguments['server_certificate'] = os.environ.get('REST_CERTIFICATE')

        if arguments['server_certificate'] is None:
            arguments['server_certificate'] = parent_dir + '/' + rest_certificate

    if action == 'availability':
        if 'domain_names' not in arguments and 'search' not in arguments:
            raise ArgumentError('You must specify either --domain or --search')
        

def check_availability(options, domain_names, search):
    try:
        http_response = http_request.get('https://' + options.server_hostname + '/1.0/availability', verify = options.server_certificate, headers = {'User-Agent': 'rac/0.2'}, data = {'domains[]': domain_names, 'string': search}, timeout = options.timeout)

    except http_request.exceptions.SSLError as error:
        reason = re.sub(r'(?x) ^ \[SSL: \  [A-Z_]+ \] \  (.*) \  \(_ssl.c: [0-9]+ \) $', r'\1', str(error.args[0].reason.args[0]))

        raise InternalError('SSL: ' + reason)

    except http_request.exceptions.Timeout as error:
        raise InternalError('HTTP request timed-out')

    except http_request.exceptions.ConnectionError as error:
        reason = re.sub(r'(?x) ^ < [^:]+ > : \ ', r'', str(error.args[0].reason.args[0]))

        raise InternalError(reason)

    http_response.raise_for_status()

    if http_response.headers['Content-Type'] != 'application/json; charset=utf-8':
        raise InternalError('Wrong content type: {}'.format(http_response.headers['Content-Type']))

    availability_list = json.loads(http_response.text)

    if options.output_json:
        print(json.dumps(availability_list, sort_keys = False, indent = 4))
        return

    for availability in availability_list:
        domain_name = bytes(availability['domain'], 'utf-8').decode('idna')
        status = availability['status']
        
        print('{} is {}'.format(domain_name, status))


def get_droplist(options):
    try:
        http_response = http_request.get('https://' + options.server_hostname + '/1.0/droplist', verify = options.server_certificate, headers = {'User-Agent': 'rac/0.2'}, timeout = options.timeout)

    except http_request.exceptions.SSLError as error:
        reason = re.sub(r'(?x) ^ \[SSL: \  [A-Z_]+ \] \  (.*) \  \(_ssl.c: [0-9]+ \) $', r'\1', str(error.args[0].reason.args[0]))

        raise InternalError('SSL: ' + reason)

    except http_request.exceptions.Timeout as error:
        raise InternalError('HTTP request timed-out')

    except http_request.exceptions.ConnectionError as error:
        reason = re.sub(r'(?x) ^ < [^:]+ > : \ ', r'', str(error.args[0].reason.args[0]))

        raise InternalError(reason)

    http_response.raise_for_status()

    if http_response.headers['Content-Type'] != 'application/json; charset=utf-8':
        raise InternalError('Wrong content type: {}'.format(http_response.headers['Content-Type']))

    drop_list = json.loads(http_response.text)

    if options.output_json:
        print(json.dumps(drop_list, sort_keys = False, indent = 4))
        return

    for drop in drop_list:
        domain_name = bytes(drop['domain'], 'utf-8').decode('idna')
        registered_date = drop['registered']
        cancelled_date = drop['cancel_date']
        release_date = drop['drop_date']
        
        print('{}\n  Registered on {}\n  Cancelled on {}\n  Released after {}'.format(domain_name, registered_date, cancelled_date, release_date))
    
        
def main():
    action, arguments = parse_arguments()

    if action == 'help':
        if arguments['help'] == 'help':
            print('rac ACTION [OPTIONS]\n'
                  '\n'
                  'Check the availability of a domain or the droplist using the REST API.\n',
                  '\n'
                  'Action is one of the following:\n'
                  '\n'
                  '  availability droplist help version\n'
                  '\n'
                  'Global options are:\n'
                  '\n'
                  '  --certificate FILE               File containing the registry SSL certificate\n'
                  '  --server HOSTNAME                Hostname of the server running the REST API\n'
                  '  --timeout SECONDS                Number of seconds to wait for a response\n'
                  '\n'
                  'Environment variables that are the defaults for options are:\n'
                  '\n'
                  '  REST_SERVER                      --server\n'
                  '  REST_CERTIFICATE                 --certificate\n'
                  '\n'
                  'The \'help\' action will display this message.\n'
                  '\n'
                  'Run \'rac help ACTION\' to see which options are available for that action. The options\n'
                  'shown in the example command line are required.\n')
        elif arguments['help'] == 'version':
            print('rac version 0.2\n'
                  '\n'
                  'Copyright 2019 Internet New Zealand Inc.\n'
                  'All rights reserved.\n'
                  '\n'
                  'Redistribution and use in source and binary forms, with or without\n'
                  'modification, are permitted provided that the following conditions are met:\n'
                  '\n'
                  '1. Redistributions of source code must retain the above copyright notice, this\n'
                  '   list of conditions and the following disclaimer.\n'
                  '\n'
                  '2. Redistributions in binary form must reproduce the above copyright notice,\n'
                  '   this list of conditions and the following disclaimer in the documentation\n'
                  '   and/or other materials provided with the distribution.\n'
                  '\n'
                  '3. Neither the name of the copyright holder nor the names of its\n'
                  '   contributors may be used to endorse or promote products derived from\n'
                  '   this software without specific prior written permission.\n'
                  '\n'
                  'THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"\n'
                  'AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n'
                  'IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n'
                  'DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n'
                  'FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n'
                  'DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n'
                  'SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n'
                  'CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n'
                  'OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n'
                  'OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n')
        elif arguments['help'] == 'availability':
            print('Check the availability of a domain name.\n'
                  '\n'
                  'rac availability {--domain NAME [NAME]* | --search NAME} [OPTIONS]\n'
                  '\n'
                  '  --domain NAME [NAME]*            Domain name to check\n'
                  '  --search NAME                    Check availability of domain in all 2LDs\n'
                  '  --json                           Output results in JSON format\n')
        elif arguments['help'] == 'droplist':
            print('Get the list of soon to be released domains.\n'
                  '\n'
                  'rac droplist [OPTIONS]\n'
                  '\n'
                  '   --json                          Output results in JSON format\n')
        else:
            print('Sorry, no help avilable for that action.')

        return

    check_arguments(action, arguments)

    options = types.SimpleNamespace()

    options.server_hostname = arguments['server_hostname']
    options.server_certificate = arguments['server_certificate']

    options.timeout = arguments['timeout'] if 'timeout' in arguments else 300
    options.output_json = True if 'output_json' in arguments else False
    
    if action == 'availability':
        domain_names = arguments['domain_names'] if 'domain_names' in arguments else None 
        search = arguments['search'] if 'search' in arguments else None 

        check_availability(options, domain_names, search)
    elif action == 'droplist':
        get_droplist(options)


if __name__ == '__main__':
    try:
        main()
    except (ArgumentError, InternalError) as error:
        print(error, file = sys.stderr)
        exit(1)
    except Exception:
        traceback.print_exc(file = sys.stderr)
        exit(1)

    exit(0)
