import json
import sys

import boto3
from botocore.exceptions import ClientError


class ConfigRules:
    def __init__(self, logging):
        self.logging = logging

        self._client_rds = None
        self._client_s3 = None
        self._client_sts = None

    @property
    def client_rds(self):
        if not self._client_rds:
            self._client_rds = boto3.client("rds")
        return self._client_rds

    @property
    def client_s3(self):
        if not self._client_s3:
            self._client_s3 = boto3.client("s3")
        return self._client_s3

    @property
    def client_sts(self):
        if not self._client_sts:
            self._client_sts = boto3.client("sts")
        return self._client_sts

    @property
    def account_number(self):
        return self.client_sts.get_caller_identity()["Account"]

    @property
    def account_arn(self):
        return self.client_sts.get_caller_identity()["Arn"]

    @property
    def region(self):
        if self.client_sts.meta.region_name != "aws-global":
            return self.client_sts.meta.region_name
        else:
            return "us-east-1"

    def rds_instance_public_access_check(self, resource_id):
        """Sets Publicly Accessible option to False for public RDS Instances
        
        Arguments:
            resource_id {DbiResourceId} -- The AWS Region-unique, immutable identifier for the DB instance
        
        Returns:
            boolean -- True if remediation was successful
        """
        try:
            paginator = self.client_rds.get_paginator("describe_db_instances")
            response = paginator.paginate(DBInstanceIdentifier=resource_id)
        except:
            self.logging.error("Could not describe RDS DB Instances.")
            return False
        else:
            for instance in response["DBInstances"]:
                try:
                    self.client_rds.modify_db_instance(
                        DBInstanceIdentifier=instance["DBInstanceIdentifier"],
                        PubliclyAccessible=False,
                    )
                    self.logging.info(
                        f"Disabled Public Accessibility for RDS Instance '{resource_id}'."
                    )
                    return True
                except:
                    self.logging.error(
                        f"Could not disable Public Accessibility for RDS Instance '{resource_id}'."
                    )
                    self.logging.error(sys.exc_info()[1])
                    return False

    def s3_bucket_server_side_encryption_enabled(self, resource_id):
        """Enables Server-side Encryption for an S3 Bucket
        
        Arguments:
            resource_id {string} -- S3 Bucket name
        
        Returns:
            boolean -- True if remediation is successful
        """
        try:
            self.client_s3.put_bucket_encryption(
                Bucket=resource_id,
                ServerSideEncryptionConfiguration={
                    "Rules": [
                        {
                            "ApplyServerSideEncryptionByDefault": {
                                "SSEAlgorithm": "AES256"
                            }
                        }
                    ]
                },
            )
            self.logging.info(
                f"Enabled Server-side Encryption for S3 Bucket '{resource_id}'."
            )
            return True
        except:
            self.logging.info(
                f"Could not enable Server-side Encryption for S3 Bucket '{resource_id}'."
            )
            self.logging.error(sys.exc_info()[1])
            return False

    def s3_bucket_ssl_requests_only(self, resource_id):
        """Adds Bucket Policy to force SSL only connections
        
        Arguments:
            resource_id {string} -- S3 Bucket name
        
        Returns:
            boolean -- True if remediation was successful
        """

        # get SSL policy
        policy_file = "auto_remediate/data/s3_bucket_ssl_requests_only_policy.json"
        with open(policy_file, "r") as file:
            policy = file.read()

        policy = json.loads(policy.replace("_BUCKET_", resource_id))

        try:
            response = self.client_s3.get_bucket_policy(Bucket=resource_id)
        except ClientError as error:
            if error.response["Error"]["Code"] == "NoSuchBucketPolicy":
                return self.set_bucket_policy(resource_id, json.dumps(policy))
            else:
                self.logging.error(
                    f"Could not set SSL requests only policy to S3 Bucket '{resource_id}'."
                )
                self.logging.error(sys.exc_info()[1])
                return False
        except:
            self.logging.error(
                f"Could not retrieve existing policy to S3 Bucket '{resource_id}'."
            )
        else:
            existing_policy = json.loads(response["Policy"])
            existing_policy["Statement"].append(policy["Statement"][0])

            return self.set_bucket_policy(resource_id, json.dumps(existing_policy))

    def set_bucket_policy(self, bucket, policy):
        """Attempts to set an S3 Bucket Policy. If returned error is Access Denied, 
        then the Public Access Block is removed before placing a new S3 Bucket Policy
        
        Arguments:
            bucket {string} -- S3 Bucket Name
            policy {string} -- S3 Bucket Policy
        
        Returns:
            boolean -- True if S3 Bucket Policy was set
        """
        try:
            self.client_s3.put_bucket_policy(Bucket=bucket, Policy=policy)
            self.logging.info(f"Set SSL requests only policy to S3 Bucket '{bucket}'.")
            return True
        except ClientError as error:
            if error.response["Error"]["Code"] == "AccessDenied":
                try:
                    # disable Public Access Block
                    self.client_s3.put_public_access_block(
                        Bucket=bucket,
                        PublicAccessBlockConfiguration={
                            "BlockPublicPolicy": False,
                            "RestrictPublicBuckets": False,
                        },
                    )

                    # put Bucket Policy
                    self.client_s3.put_bucket_policy(Bucket=bucket, Policy=policy)

                    # enable Public Access Block
                    self.client_s3.put_public_access_block(
                        Bucket=bucket,
                        PublicAccessBlockConfiguration={
                            "BlockPublicPolicy": True,
                            "RestrictPublicBuckets": True,
                        },
                    )

                    self.logging.info(
                        f"Set SSL requests only policy to S3 Bucket '{bucket}'."
                    )
                    return True
                except:
                    self.logging.error(
                        f"Could not set SSL requests only policy to S3 Bucket '{bucket}'."
                    )
                    self.logging.error(sys.exc_info()[1])
                    return False
            else:
                self.logging.error(
                    f"Could not set SSL requests only policy to S3 Bucket '{bucket}'."
                )
                self.logging.error(sys.exc_info()[1])
                return False
        except:
            self.logging.error(
                f"Could not set SSL requests only policy to S3 Bucket '{bucket}'."
            )
            self.logging.error(sys.exc_info()[1])
            return False