from aws_cdk import (
  aws_iam as iam,
  aws_rds as rds,
  aws_sqs as sqs,
  aws_sns as sns,
  aws_ec2 as ec2,
  aws_s3  as s3,
  aws_logs as logs,
  aws_kms as kms,
  aws_cloudwatch         as cloudwatch,
  aws_cloudwatch_actions as cloudwatch_actions,
  aws_secretsmanager    as secretsmanager,
  aws_s3_notifications  as s3n,
  aws_sns_subscriptions as subs,
  aws_lambda            as lfn,
  Aspects, CfnOutput, Stack, SecretValue, Tags, Fn, Aws, CfnMapping, Duration, RemovalPolicy,
  App, RemovalPolicy
)

from constructs import Construct

from cdk_nag import ( AwsSolutionsChecks, NagSuppressions )

class Oracle(Stack):

  def __init__(self, scope:Construct, id:str,
                vpc_id:str,                 ## vpc id
                subnet_ids:list,            ## list of subnet ids
                db_name:str,                ## database name
                instance_type = ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.LARGE), ## ec2.InstanceType
                ##
                ## https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_rds/OracleEngineVersion.html#aws_cdk.aws_rds.OracleEngineVersion
                ##
                engine_version = rds.OracleEngineVersion.VER_19_0_0_0_2021_04_R1,
                oracle_username:str="dbadmin",
                backup_retention_days:int=14,
                backup_window:str="00:15-01:15",
                preferred_maintenance_window:str="Sun:23:45-Mon:00:15",
                ingress_sources:list=[],    ## A security group object or a network subnet
                                            ##   ec2.Peer.ipv4("0.0.0.0/0")
                                            ##   ec2.SecurityGroup
                **kwargs) -> None:
    super().__init__(scope, id, **kwargs)



    ############################################
    ##
    ## CDK Nag - https://pypi.org/project/cdk-nag/
    ##           https://github.com/cdklabs/cdk-nag
    ##
    ## CDK Nag Checks for AWS Engagement Solutions Secuirty Rules:
    ##   https://github.com/cdklabs/cdk-nag/blob/main/RULES.md#awssolutions
    ## Also checks for:
    ##   HIPAA Security
    ##   NIST 800-53 rev 4
    ##   NIST 800-53 rev 5
    ##
    ############################################
    Aspects.of(self).add(AwsSolutionsChecks())
    ##
    ## Supressed Errors
    ##
    NagSuppressions.add_stack_suppressions(self, [{"id":"AwsSolutions-IAM4", "reason":"TODO: Stop using AWS managed policies."}])
    NagSuppressions.add_stack_suppressions(self, [{"id":"AwsSolutions-IAM5", "reason":"TODO: Remove Wildcards in IAM roles."}])
    NagSuppressions.add_stack_suppressions(self, [{"id":"AwsSolutions-RDS11","reason":"Default Oracle ports is fine."}])
    ##
    ## Supressed Warnings
    ##
    NagSuppressions.add_stack_suppressions(self, [{"id":"AwsSolutions-RDS16", "reason":"parameter referencing an intrinsic function"}])



    azs = Fn.get_azs()

    vpc = ec2.Vpc.from_vpc_attributes(self, 'ExistingVPC', availability_zones=azs, vpc_id=vpc_id)
    subnets = list()
    for subnet_id in subnet_ids:
      subnets.append(ec2.Subnet.from_subnet_attributes(self, subnet_id.replace("-", "").replace("_", "").replace(" ", ""), subnet_id=subnet_id))

    vpc_subnets = ec2.SubnetSelection(subnets=subnets)

    allAll  = ec2.Port(protocol=ec2.Protocol("ALL"), string_representation="ALL")
    tcp1521 = ec2.Port(protocol=ec2.Protocol("TCP"), from_port=1521, to_port=1521, string_representation="tcp1521 Oracle")
    tcp1526 = ec2.Port(protocol=ec2.Protocol("TCP"), from_port=1526, to_port=1526, string_representation="tcp1526 Oracle")
    tcp1575 = ec2.Port(protocol=ec2.Protocol("TCP"), from_port=1575, to_port=1575, string_representation="tcp1575 Oracle")


    dbsg = ec2.SecurityGroup(self, "DatabaseSecurityGroup",
             vpc                 = vpc,
             allow_all_outbound  = True,
             description         = id + " Database",
             security_group_name = id + " Database",
           )
    dbsg.add_ingress_rule(
      peer       =dbsg,
      connection =allAll,
      description="all from self"
    )
    dbsg.add_egress_rule(
      peer       =ec2.Peer.ipv4("0.0.0.0/0"),
      connection =allAll,
      description="all out"
    )

    oracle_connection_ports = [
      {"port":tcp1521, "description":"tcp1521 Oracle"},
      {"port":tcp1526, "description":"tcp1526 Oracle"},
      {"port":tcp1575, "description":"tcp1575 Oracle"},
    ]
    db_subnet_group = None
    for ingress_source in ingress_sources:
      for c in oracle_connection_ports:
        dbsg.add_ingress_rule(
          peer       =ingress_source,
          connection =c["port"],
          description=c["description"]
        )

      db_subnet_group = rds.SubnetGroup(self,
                         id          = "DatabaseSubnetGroup",
                         vpc         = vpc,
                         description = id + " subnet group",
                         vpc_subnets = vpc_subnets,
                         subnet_group_name=id + "subnet group"
      )

    ##
    ## Oracle Database
    ##
    oracle_secret = secretsmanager.Secret(self, "OracleCredentials",
    secret_name           =db_name + "OracleCredentials",
    description           =db_name + " Oracle Database Credentials",
    generate_secret_string=secretsmanager.SecretStringGenerator(
      exclude_characters    ="\"@/\\ '",
      generate_string_key   ="password",
      password_length       =30,
      secret_string_template='{"username":"'+ oracle_username+'"}'),
    )

    oracle_credentials = rds.Credentials.from_secret(oracle_secret, oracle_username)

    ##
    ## https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_rds/ParameterGroup.html
    ##
    db_parameter_group = rds.ParameterGroup(self, "ParameterGroup",
      engine=rds.DatabaseInstanceEngine.oracle_ee(version=engine_version),
      parameters={"open_cursors": "2500"}
    )

    ##
    ## https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_rds/OptionGroup.html
    ##
    db_option_group = rds.OptionGroup(self, "OptionGroup",
      engine=rds.DatabaseInstanceEngine.oracle_ee(version=engine_version),
      configurations=[
        rds.OptionConfiguration(name="LOCATOR"),
        rds.OptionConfiguration(name="OEM",port=1158,vpc=vpc)
      ]
    )

    ##
    ## https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_rds/DatabaseInstance.html
    ##
    oracle_instance = rds.DatabaseInstance(self, "OracleDatabase",
      database_name               = db_name,
      instance_identifier         = db_name,
      credentials                 = oracle_credentials,
      engine                      = rds.DatabaseInstanceEngine.oracle_ee(version=engine_version),
      backup_retention            = Duration.days(7),
      allocated_storage           = 20,
      security_groups             = [dbsg],
      license_model               = rds.LicenseModel.BRING_YOUR_OWN_LICENSE,
      allow_major_version_upgrade = True,
      auto_minor_version_upgrade  = True,
      instance_type               = instance_type,
      vpc_subnets                 = vpc_subnets,
      vpc                         = vpc,
      removal_policy              = RemovalPolicy.RETAIN,
      multi_az                    = True,
      storage_encrypted           = True,
      monitoring_interval         = Duration.seconds(60),
      enable_performance_insights = True,
      cloudwatch_logs_exports     = ["trace", "audit", "alert", "listener"],
      cloudwatch_logs_retention   = logs.RetentionDays.ONE_MONTH,
      #option_group                = db_option_group,
      parameter_group             = db_parameter_group,
      subnet_group                = db_subnet_group,
      preferred_backup_window     = backup_window,
      preferred_maintenance_window= preferred_maintenance_window,
      publicly_accessible         = False,
    ) ## rds.DatabaseInstance


    # Rotate the master user password every 30 days
    oracle_instance.add_rotation_single_user()



    Tags.of(oracle_instance).add("Name", "OracleDatabase", priority=300)



    CfnOutput(self, "OracleEndpoint",   
      export_name="OracleEndpoint",   
      value      =oracle_instance.db_instance_endpoint_address)
    CfnOutput(self, "OracleUsername",   
      export_name="OracleUsername",   
      value      =oracle_username)
    CfnOutput(self, "OracleDbName",   
      export_name="OracleDbName",   
      value      =db_name)



class LavaPlainsOfMustafar(Stack):

  def __init__(self, scope:Construct, id:str, **kwargs) -> None:
    super().__init__(scope, id, **kwargs)

    vpc = ec2.Vpc(self, "LavaPlainsVpc",
      cidr                 = "10.99.0.0/16",
      max_azs              = 3,
      enable_dns_hostnames = True,
      enable_dns_support   = True,
      subnet_configuration = [
        ec2.SubnetConfiguration(
          cidr_mask   = 24,
          name        = 'public1',
          subnet_type = ec2.SubnetType.PUBLIC,
        ),
        ec2.SubnetConfiguration(
          cidr_mask   = 24,
          name        = 'public2',
          subnet_type = ec2.SubnetType.PUBLIC,
        ),
        ec2.SubnetConfiguration(
          cidr_mask   = 24,
          name        = 'public3',
          subnet_type = ec2.SubnetType.PUBLIC,
        )
      ]
    )

    vpc_subnets = vpc.select_subnets(
      subnet_type=ec2.SubnetType.PUBLIC,
      one_per_az =True
    )

    subnet_ids = []
    for subnet in vpc_subnets.subnets:
      subnet_ids.append(subnet.subnet_id)

    vpc_id = vpc.vpc_id

    Oracle(self, "LavaMiningDb",
      db_name="LavaMining",
      ingress_sources=[ec2.Peer.ipv4("10.10.10.10/32")],
      vpc_id=vpc_id,
      subnet_ids=subnet_ids,
      env={'region': 'us-east-1'},
      description="Lava Mining DB")


app = App()
# Call the stack on its own
Oracle(app, "Oracle", env={"region":"us-east-1"}, description="Oracle Instance",
  vpc_id    = "vpc-aaaaaaaa",
  subnet_ids=["subnet-xxxxxxxx", "subnet-yyyyyyyy", "subnet-zzzzzzzz"],
  db_name="sampledb"
)
# Use the construct in a sample stack
LavaPlainsOfMustafar(app, "LavaPlainsOfMustafar", env={"region":"us-east-1"}, description="Mustafar")
app.synth()