# frozen_string_literal: true

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

module Litany
  class Project < Base
    extend ResourceDSL
    include Metable
    include Taggable

    # Returns the active workspace for this project instance.
    # @return [ProjectWorkspace]
    attr_reader :workspace

    child_resource Redirects, :redirects, automatic: :on_access

    flag :alias_compat_mode, false

    property :key_pair, nil, [nil, String, Symbol]

    resource_collection Alarms, :alarms, required: false
    resource_collection ACMCertificates, :acm, required: false
    resource_collection Beanstalk, :beanstalk, required: false
    resource_collection CloudFrontDistribution, :cloudfront, required: false
    resource_collection DNSZoneReference, :dns_zone_reference, required: false
    resource_collection ECR, :ecr, required: false
    resource_collection ECS, :ecs, required: false
    resource_collection ECSServices, :ecs_services, required: false
    resource_collection ElasticFileSystem, :efs, required: false
    resource_collection ElastiCacheReplicationGroup, :elasticache, required: false
    resource_collection ElasticsearchDomain, :elasticsearch, required: false
    resource_collection Environment, :environment, required: false
    resource_collection IAMPermissions, :iam, required: false
    resource_collection Image, :image, required: false
    resource_collection LambdaService, :lambda_service, required: false
    resource_collection Route53Zone, :dns_zone, required: false
    resource_collection Redshift, :redshift, required: false
    resource_collection RDSAurora, :rds_aurora, required: false
    resource_collection RDSMssql, :rds_mssql, required: false
    resource_collection RDSMysql, :rds_mysql, required: false
    resource_collection S3Bucket, :bucket, required: false
    resource_collection Service, :service, required: false
    resource_collection SNS, :sns, required: false
    resource_collection VPC, :vpc, required: false
    resource_collection VPCReference, :vpc_reference, required: false

    validator(:environment_availability) do
      count = environments.count
      defaults = environments.select(&:default?).count

      raise 'You must have at least one environment declared in your project.' if count.zero?
      raise 'You may only declare a single environment as the default.' if defaults > 1
      raise 'If you declare multiple environments you must declare one as your default.' if count > 1 && defaults.zero?
    end

    # @param workspace: [ProjectWorkspace]
    # @param active_environment: [Symbol]
    def initialize(workspace:, active_environment: nil)
      raise 'InvalidWorkspace' unless workspace.valid?

      MetaResource.descendants.each(&:reset_instances)

      @workspace = workspace
      @active_environment = Environment.new(active_environment) unless active_environment.nil?
      @builtin_lambda_template_directory = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lambda'))

      Litany.load_plugins(workspace)
      Litany.finalize_resource_definitions

      workspace.settings_files.each { |path| load_file(path) }
      workspace.litany_files.each { |path| load_file(path) }

      raise 'Unable to validate your project.' unless self.class.validators.all? { |validator| send(validator) }

      finalize unless active_environment.nil? # most projects will not pass validation if active_environment was not set during parsing.
    end

    # @group Shortcuts and Helpers

    # Returns the name of the project via the workspace
    # @return [String]
    def name
      workspace['project.name']
    end

    # Returns `self`, this is used to provide a consistent interface since {Project} is a pseudo resource.
    # @return [Project]
    def project
      self
    end

    # Returns the active stage of the project via the workspace.
    # @return [Symbol]
    def stage
      workspace.stage
    end

    # Loads a specific file
    # @param path [String] the path of the file to load
    private def load_file(path)
      instance_eval(File.read(path), path)
    end

    # @endgroup

    # @group Environment Helpers

    # Returns the environment that is active for this project instance.
    # @return [Environment]
    attr_reader :active_environment

    # Returns the default environment for this project.
    # @return [Environment]
    def default_environment
      @default_environment ||= environments.select(&:default?).first || environments.first
    end

    # @endgroup

    # @group Compilation
    def compile
      raise 'You cannot compile a project that does not have an active_environment set.' if active_environment.nil?
      return if @has_compiled

      info "Compiling for stage: #{workspace.stage}, environment: #{active_environment.name}"

      stacks.each { |name, stack| compiled_stacks[name] = stack.compile_stack }

      SecretPropertyValue.disable_redaction
      workspace.cache_output(active_environment.name, compiled_stacks)

      SecretPropertyValue.enable_redaction
      workspace.store_output(active_environment.name, compiled_stacks)

      info "Successfully compiled #{compiled_stacks.count} stacks!".indent(1)

      @has_compiled = true
    end

    # Returns a hash of compiled stacks. These stacks are still ruby hashes, and not compiled down to json.
    # @return Hash<Symbol, Hash>
    def compiled_stacks
      @compiled_stack ||= {}
    end

    def output(stack_name)
      workspace.cached_output(active_environment.name, stack_name)
    end

    # @endgroup

    # @group Finalization

    # Finalizes the project. Validates the project and prepares for compilation.
    # @return [void]
    def finalize
      @active_environment ||= default_environment
      tag(:project_name, name.pascal_case)

      active_environment.run_finalization
      raise "Unable to validate `active_environment`: #{active_environment.inspect}." unless active_environment.run_validation

      stacks.each_value(&:finalize_stack)
      stacks.select! { |_name, stack| stack.in_active_environment? }

      finalized_resources = 0
      loop do
        all_resources = child_resources + stacks.values
        all_resources += all_resources.collect { |resource| gather_child_resources(resource) }.flatten

        finalized_resources >= all_resources.count ? break : finalized_resources = all_resources.count

        all_resources.each { |resource| resource.run_finalization if resource.respond_to?(:run_finalization) }
      end

      raise 'Unable to validate non-stack resources.' unless gather_child_resources(self).collect(&:run_validation).all?

      stacks.each { |name, stack| raise "Unable to validate stack #{name}." unless stack.validate_stack }
      ResourceReference.instances.each(&:finalize)
    end

    # @endgroup

    def cfn_client
      workspace.cfn_client(region: active_environment.region.to_s)
    end

    def child_resources
      @resources ||= Set.new
    end

    def child_resource_hook(resource)
      child_resources << resource unless resource.subclass_of?(Stack)
      stacks[resource.resource_name] = resource if resource.subclass_of?(Stack)
    end

    def chef_attributes
      @chef_attributes ||= Indicium.new { |h, k| h[k] = Indicium.new(&h.default_proc) }
    end

    def gather_child_resources(resource)
      children = resource.child_resources.to_a.flatten
      (children + children.collect { |child| gather_child_resources child }).flatten
    end

    def get_userdata_template(name)
      @templates ||= {}
      @templates[name] ||= Tilt.new(workspace.userdata_template(name), pretty: true)
    end

    def get_lambda_template(name)
      @templates ||= {}
      template_file = workspace.lambda_template(name)
      template_file = File.join(@builtin_lambda_template_directory, name) unless File.exist?(template_file)
      @templates[name] ||= Tilt.new(template_file, pretty: true)
    end

    def security_logging_bucket(&block)
      bucket :security_logging do
        environment active_environment unless environments.include?(active_environment)
        expire_days 365 unless set_expire_days?
        futures_unique_naming
        access :log_delivery_write

        instance_eval(&block) if block_given?
      end
    end

    def settings
      @settings ||= self.class.settings.sub_scope
    end

    def stacks
      @stacks ||= {}
    end
  end
end
