# frozen_string_literal: true

require 'litany/mixins'
require 'litany/resource'

module Litany
  class RDSInstance < Resource
    include Taggable

    cfn_type 'AWS::RDS::DBInstance'
    parent_resource :rds

    flag :publicly_accessible, false, inherited: true

    property :allow_major_version_upgrade, false, [true, false]
    property :allow_minor_version_upgrade, true, [true, false]
    property :backup_retention, 7, [nil, 1..35] # Supports 0, but changing to/from 0 can be disruptive and means there's no backups.
    property :backup_window, nil, [nil, String] do |value|
      raise 'Backup window must be at least 30 minutes and formatted as hh24:mi-hh24:mi' unless value.nil? || /[0-9]{2}:[0-9]{2}-[0-9]{2}:[0-9]{2}/.match(value)
      value
    end
    property :certificate_authority, 'rds-ca-2019', ['rds-ca-2019', 'rds-ca-2015']
    property :character_set, 'UTF8', [nil, String]
    property :engine, nil, [:mysql, :mariadb, :oracle_se1, :oracle_se2, :oracle_se, :oracle_ee, :sqlserver_ee, :sqlserver_se, :sqlserver_ex, :sqlserver_web, :postgres, :aurora, :aurora_mysql, :aurora_postgresql], inherited: true
    property :encrypted, false, [true, false]
    property :instance_type, 'db.r3.large', [String], inherited: true do |value|
      raise "Invalid instance type #{value}. Must match `db.[class].[size]`" unless /db\.(?:t|m|r)[1-5]\.(?:[2-8]|10)?(?:micro|small|medium|large|xlarge)/.match(value)
      value
    end
    property :iops, nil, [1000..30_000, nil], inherited: true
    property :license_model, nil, [nil, :license_included, :bring_your_own_license, :general_public_license], inherited: true
    property :maintenance_window, nil, [nil, String] do |value|
      raise 'Maintenance window must be at least 30 minutes and formatted as hh24:mi-hh24:mi' unless value.nil? || /(Mon|Tue|Wed|Thu|Fri|Sat|Sun):[0-9]{2}:[0-9]{2}-(Mon|Tue|Wed|Thu|Fri|Sat|Sun):[0-9]{2}:[0-9]{2}/.match(value)
      value
    end
    property :multi_az, true, [true, false]
    property :password, nil, [nil, String], inherited: true, secret: true
    property :port, nil, [nil, 1150..65_535] do |value|
      invalid_ports = [1434, 3389, 47_001] + (49_152..49_156).to_a
      raise 'SQLServer does not allow usage of ports 1434, 3389, 47001, or 49152-49156.' if mssql? && invalid_ports.include?(value)
      value
    end
    property :snapshot, nil, [nil, String]
    property :storage_size, '100', [String, 10..16_384, nil], inherited: true, &:to_s
    property :storage_type, :gp2, [:gp2, :io1, nil], inherited: true
    property :tag_snapshots, true, [true, false]
    property :username, nil, [nil, String], inherited: true
    property :version, nil, [nil, String], inherited: true

    resource_reference KMSKey, :kms_key, inherited: true, required: false
    resource_reference RDSSubnetGroup, :subnet_group, inherited: true
    resource_reference RDSCluster, :cluster, required: false, inherited: true
    resource_reference RDSInstance, :source_instance, required: false
    resource_reference RDSInstanceParameterGroup, :instance_parameters, required: false
    resource_reference RDSOptionsGroup, :options_group, required: false, inherited: true

    resource_references SecurityGroup, :security_group, required: false, inherited: true

    validator :aurora do
      if aurora?
        raise '`cluster` must be set if engine type is :aurora' if cluster.nil?
        self.backup_retention = nil
        self.backup_window = nil
        self.character_set = nil
        self.maintenance_window = nil
        self.password = nil
        self.port = nil
        self.source_instance = nil
        self.storage_size = nil
        self.storage_type = nil
        self.username = nil

        # Setting override
        # engine :aurora
        multi_az false

        raise 'You may not set backup_window, maintenance_window, password, port, username, or source_instance if :aurora is set.' unless [backup_window, maintenance_window, password, port, source_instance, username].all?(&:nil?)
      end
    end

    validator :encryption_setup do
      encrypted true if set_kms_key?
    end

    validator :login do
      if source_instance.nil? && snapshot.nil?
        raise 'You must set both username and password when creating a new instance.' if (username.nil? || password.nil?) && cluster.nil?
      else
        raise 'You must set either username and password or either source_instance or snapshot.' unless username.nil? && password.nil?
      end
    end

    validator :source_settings do
      raise 'You may not set both `source_instance` and `snapshot`.' unless source_instance.nil? || snapshot.nil?
    end

    validator :storage_configuration do
      if set_iops?(false) && set_storage_size?
        valid_values = (3..10).collect { |ratio| ratio * storage_size.to_i }
        raise "IOPs must be a ratio of 3:1-10:1 of your `storage_size`. Valid values for #{storage_size}GB: #{valid_values}" unless valid_values.include? iops
        storage_type :io1
      end
    end

    def aurora?
      engine.to_s.start_with?('aurora')
    end

    def mssql?
      engine.to_s.start_with?('sqlserver')
    end

    def oracle?
      engine.to_s.start_with?('oracle')
    end

    def properties
      # TODO: Domain, DomainIAMRoleName, MonitoringInterval, MonitoringRoleArn
      # Not implementing: DBInstanceIdentifier, DBSecurityGroups, AvailabilityZone
      props = {
        AllowMajorVersionUpgrade: allow_major_version_upgrade,
        AutoMinorVersionUpgrade: allow_minor_version_upgrade,
        CACertificateIdentifier: certificate_authority,
        CopyTagsToSnapshot: tag_snapshots,
        DBInstanceClass: instance_type,
        DBSubnetGroupName: ref(subnet_group),
        Engine: engine.tr('_', '-'),
        MultiAZ: multi_az,
        PubliclyAccessible: publicly_accessible?,
        StorageEncrypted: encrypted,
      }

      props[:AllocatedStorage] = storage_size unless storage_size.nil?
      props[:BackupRetentionPeriod] = backup_retention unless backup_retention.nil?
      props[:CharacterSetName] = character_set if oracle?
      props[:DBClusterIdentifier] = ref(cluster) unless cluster.nil?
      props[:DBParameterGroupName] = ref(instance_parameters) unless instance_parameters.nil?
      props[:DBSnapshotIdentifier] = snapshot unless snapshot.nil?
      props[:EngineVersion] = version unless version.nil?
      props[:Iops] = iops unless iops.nil?
      props[:KmsKeyId] = ref(kms_key, :Arn) if set_kms_key?
      props[:LicenseModel] = license_model.tr('_', '-').to_sym unless license_model.nil?
      props[:MasterUsername] = username unless username.nil?
      props[:MasterUserPassword] = password unless password.nil?
      props[:Port] = port unless port.nil?
      props[:PreferredBackupWindow] = backup_window unless backup_window.nil?
      props[:PreferredMaintenanceWindow] = maintenance_window unless maintenance_window.nil?
      props[:OptionGroupName] = ref(options_group) if set_options_group?
      props[:SourceDBInstanceIdentifier] = ref(source_instance) unless source_instance.nil?
      props[:StorageType] = storage_type unless storage_type.nil?
      props[:Timezone] = :UTC if mssql?
      props[:VPCSecurityGroups] = ref(security_groups) unless aurora? || security_groups.empty?

      props
    end

    def default_tags
      {
        LitanyRDSInstanceName: self.name,
      }
    end
  end
end
