##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################

""" Amazon RDS PostgreSQL provider """

import os
import time
import boto3

from providers._abstract import AbsProvider
from utils.io import debug, error, output
from utils.misc import get_my_ip, get_random_id

DEL_SEC_GROUP_MSG = 'Deleting security group: {}...'


class RdsProvider(AbsProvider):
    def __init__(self):
        self._clients = {}
        self._access_key = None
        self._secret_key = None
        self._session_token = None
        self._database_pass = None
        self._default_region = None

        # Get the credentials
        if 'AWS_ACCESS_KEY_ID' in os.environ:
            self._access_key = os.environ['AWS_ACCESS_KEY_ID']

        if 'AWS_SECRET_ACCESS_KEY' in os.environ:
            self._secret_key = os.environ['AWS_SECRET_ACCESS_KEY']

        if 'AWS_SESSION_TOKEN' in os.environ:
            self._session_token = os.environ['AWS_SESSION_TOKEN']

        if 'AWS_DATABASE_PASSWORD' in os.environ:
            self._database_pass = os.environ['AWS_DATABASE_PASSWORD']

    def init_args(self, parsers):
        """ Create the command line parser for this provider """
        self.parser = parsers.add_parser('aws',
                                         help='Amazon RDS PostgreSQL',
                                         epilog='Credentials are read from '
                                                '~/.aws/config by default and '
                                                'can be overridden in the '
                                                'AWS_ACCESS_KEY_ID and '
                                                'AWS_SECRET_ACCESS_KEY '
                                                'environment variables. '
                                                'The default region is read '
                                                'from ~/.aws/config and will '
                                                'fall back to us-east-1 if '
                                                'not present.')
        self.parser.add_argument('--region', default=self._default_region,
                                 help='name of the AWS region (default: {})'
                                 .format(self._default_region))

        # Create the command sub-parser
        parsers = self.parser.add_subparsers(help='RDS commands',
                                             dest='command')

        # Create the create instance command parser
        parser_create_instance = parsers.add_parser('create-instance',
                                                    help='create a new '
                                                         'instance')
        parser_create_instance.add_argument('--name', required=True,
                                            help='name of the instance')
        parser_create_instance.add_argument('--db-name', default='postgres',
                                            help='name of the default '
                                                 'database '
                                                 '(default: postgres)')
        parser_create_instance.add_argument('--db-password', required=False,
                                            help='password for the database')
        parser_create_instance.add_argument('--db-username',
                                            default='postgres',
                                            help='user name for the database '
                                                 '(default: postgres)')
        parser_create_instance.add_argument('--db-port', type=int,
                                            default=5432,
                                            help='port of the database '
                                                 '(default: 5432)')
        parser_create_instance.add_argument('--db-version',
                                            default='13.3',
                                            help='version of PostgreSQL '
                                                 'to deploy (default: 13.3)')
        parser_create_instance.add_argument('--instance-type', required=True,
                                            help='machine type for the '
                                                 'instance nodes, e.g. '
                                                 'db.m3.large')
        parser_create_instance.add_argument('--storage-iops', type=int,
                                            default=0,
                                            help='storage IOPs to allocate '
                                                 '(default: 0)')
        parser_create_instance.add_argument('--storage-size', type=int,
                                            required=True,
                                            help='storage size in GB')
        parser_create_instance.add_argument('--storage-type', default='gp2',
                                            help='storage type for the data '
                                                 'database (default: gp2)')
        parser_create_instance.add_argument('--high-availability',
                                            default=False)
        parser_create_instance.add_argument('--public-ip', default='127.0.0.1',
                                            help='Public IP '
                                                 '(default: 127.0.0.1)')

        # Create the delete instance command parser
        parser_delete_instance = parsers.add_parser('delete-instance',
                                                    help='delete an instance')
        parser_delete_instance.add_argument('--name', required=True,
                                            help='name of the instance')
        parser_delete_instance.add_argument('--security-group',
                                            help='name of a security group to'
                                                 'delete as well')

    ##########################################################################
    # AWS Helper functions
    ##########################################################################
    def _get_aws_client(self, type, args):
        """ Create/cache/return an AWS client object """
        if type in self._clients:
            return self._clients[type]

        session = boto3.Session(
            aws_access_key_id=self._access_key,
            aws_secret_access_key=self._secret_key,
            aws_session_token=self._session_token
        )

        self._clients['type'] = session.client(type, region_name=args.region)

        return self._clients['type']

    def _create_security_group(self, args):
        """ Create a new security group for the instance """
        ec2 = self._get_aws_client('ec2', args)
        ip = args.public_ip if args.public_ip else get_my_ip()
        ip = ip.split(',')

        # Deploy the security group
        try:
            name = 'pgacloud_{}_{}_{}'.format(args.name,
                                              ip[0].replace('.', '-'),
                                              get_random_id())
            debug('Creating security group: {}...'.format(name))
            output({'Creating': 'Creating security group: {}...'.format(name)})
            response = ec2.create_security_group(
                Description='Inbound access for {} to RDS instance {}'.format(
                    ip[0], args.name),
                GroupName=name
            )
        except Exception as e:
            error(str(e))

        return response['GroupId']

    def _add_ingress_rule(self, args, security_group):
        """ Add a local -> PostgreSQL ingress rule to a security group """
        ec2 = self._get_aws_client('ec2', args)
        ip = args.public_ip if args.public_ip else\
            '{}/32'.format(get_my_ip())
        port = args.db_port or 5432
        ip_ranges = []

        ip = ip.split(',')
        for i in ip:
            ip_ranges.append({
                'CidrIp': i,
                'Description': 'pgcloud client {}'.format(i)
            })
        try:
            output({'Adding': 'Adding ingress rule for: {}...'.format(ip)})
            debug('Adding ingress rule for: {}...'.format(ip))
            ec2.authorize_security_group_ingress(
                GroupId=security_group,
                IpPermissions=[
                    {
                        'FromPort': port,
                        'ToPort': port,
                        'IpProtocol': 'tcp',
                        'IpRanges': ip_ranges
                    },
                ]
            )
        except Exception as e:
            error(e)

    def _create_rds_instance(self, args, security_group):
        """ Create an RDS instance """
        ec2 = self._get_aws_client('ec2', args)
        rds = self._get_aws_client('rds', args)

        db_password = self._database_pass if self._database_pass is not None\
            else args.db_password

        try:
            debug('Creating RDS instance: {}...'.format(args.name))
            rds.create_db_instance(DBInstanceIdentifier=args.name,
                                   AllocatedStorage=args.storage_size,
                                   DBName=args.db_name,
                                   Engine='postgres',
                                   Port=args.db_port,
                                   EngineVersion=args.db_version,
                                   StorageType=args.storage_type,
                                   StorageEncrypted=True,
                                   AutoMinorVersionUpgrade=True,
                                   MultiAZ=bool(args.high_availability),
                                   MasterUsername=args.db_username,
                                   MasterUserPassword=db_password,
                                   DBInstanceClass=args.instance_type,
                                   VpcSecurityGroupIds=[
                                       security_group,
                                   ], **({"Iops": args.storage_iops}
                                         if args.storage_iops else {}))

        except rds.exceptions.DBInstanceAlreadyExistsFault as e:
            try:
                debug(DEL_SEC_GROUP_MSG.format(security_group))
                ec2.delete_security_group(GroupId=security_group)
            except Exception:
                pass
            error('RDS instance {} already exists.'.format(args.name))
        except Exception as e:
            try:
                debug(DEL_SEC_GROUP_MSG.format(security_group))
                ec2.delete_security_group(GroupId=security_group)
            except Exception:
                pass
            error(str(e))

        # Wait for completion
        running = True
        while running:
            response = rds.describe_db_instances(
                DBInstanceIdentifier=args.name)

            db_instance = response['DBInstances'][0]
            status = db_instance['DBInstanceStatus']

            if status != 'creating' and status != 'backing-up':
                running = False

            if running:
                time.sleep(5)

        return response['DBInstances']

    def _delete_rds_instance(self, args, name):
        """ Delete an RDS instance """
        rds = self._get_aws_client('rds', args)

        debug('Deleting RDS instance: {}...'.format(name))
        try:
            rds.delete_db_instance(
                DBInstanceIdentifier=name,
                SkipFinalSnapshot=True,
                DeleteAutomatedBackups=True
            )
        except Exception as e:
            error(str(e))

        # Wait for completion
        while True:
            try:
                rds.describe_db_instances(DBInstanceIdentifier=args.name)
            except rds.exceptions.DBInstanceNotFoundFault:
                return
            except Exception as e:
                error(str(e))

            time.sleep(5)

    def _delete_security_group(self, args, id):
        """ Delete a security group """
        ec2 = self._get_aws_client('ec2', args)

        debug('Deleting security group: {}...'.format(id))
        try:
            ec2.delete_security_group(
                GroupId=id
            )
        except Exception as e:
            error(str(e))

    ##########################################################################
    # User commands
    ##########################################################################
    def cmd_create_instance(self, args):
        """ Create an RDS instance and security group """
        security_group = self._create_security_group(args)
        self._add_ingress_rule(args, security_group)
        instance = self._create_rds_instance(args, security_group)

        data = {'instance': {
            'Id': instance[0]['DBInstanceIdentifier'],
            'Location': instance[0]['AvailabilityZone'],
            'SecurityGroupId': security_group,
            'Hostname': instance[0]['Endpoint']['Address'],
            'Port': instance[0]['Endpoint']['Port'],
            'Database': instance[0]['DBName'],
            'Username': instance[0]['MasterUsername']
        }}

        output(data)

    def cmd_delete_instance(self, args):
        """ Delete an RDS instance and (optionally) a security group """
        self._delete_rds_instance(args, args.name)

        if args.security_group is not None:
            self._delete_security_group(args, args.security_group)


def load():
    """ Loads the current provider """
    return RdsProvider()
