# frozen_string_literal: true

require 'anyway/ext/deep_dup'
require 'anyway/ext/deep_freeze'
require 'anyway/ext/hash'

module Anyway # :nodoc:
  using Anyway::Ext::DeepDup
  using Anyway::Ext::DeepFreeze
  using Anyway::Ext::Hash

  # Base config class
  # Provides `attr_config` method to describe
  # configuration parameters and set defaults
  class Config
    class << self
      attr_reader :defaults, :config_attributes

      def attr_config(*args, **hargs)
        @defaults ||= {}
        @config_attributes ||= []

        new_defaults = hargs.deep_dup
        new_defaults.stringify_keys!
        defaults.merge! new_defaults

        new_keys = (args + new_defaults.keys) - config_attributes
        @config_attributes += new_keys
        attr_accessor(*new_keys)
      end

      def config_name(val = nil)
        return (@config_name = val.to_s) unless val.nil?
        @config_name = underscore_name unless defined?(@config_name)
        @config_name
      end

      def env_prefix(val = nil)
        return (@env_prefix = val.to_s) unless val.nil?
        @env_prefix
      end

      # Load config as Hash by any name
      #
      # Example:
      #
      #   my_config = Anyway::Config.for(:my_app)
      #   # will load data from config/my_app.yml, secrets.my_app, ENV["MY_APP_*"]
      def for(name)
        new(name: name, load: false).load_from_sources
      end

      private

      def underscore_name
        return unless name
        word = name[/^(\w+)/]
        word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
        word.downcase!
        word
      end
    end

    attr_reader :config_name, :env_prefix

    # Instantiate config with specified name, loads the data and applies overrides
    #
    # Example:
    #
    #   my_config = Anyway::Config.new(name: :my_app, load: true, overrides: { some: :value })
    #
    # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/LineLength,Metrics/CyclomaticComplexity
    def initialize(name: nil, load: true, overrides: {})
      @config_name = name || self.class.config_name

      raise ArgumentError, "Config name is missing" unless @config_name

      if @config_name.to_s.include?('_') && @env_prefix.nil?
        warn "[Deprecated] As your config_name is #{@config_name}, " \
             "the prefix `#{@config_name.to_s.delete('_').upcase}` " \
             "will be used to parse env variables. " \
             "This behavior is about to change in 1.4.0 (no more deleting underscores). " \
             "Env prefix can be set explicitly with `env_prefix` method now already " \
             "(check out the docs), and it will be used as is."
      end

      @env_prefix = self.class.env_prefix || @config_name.to_s&.delete('_')

      self.load(overrides) if load
    end
    # rubocop:enable Metrics/MethodLength,Metrics/AbcSize,Metrics/LineLength,Metrics/CyclomaticComplexity

    def reload(overrides = {})
      clear
      load(overrides)
      self
    end

    def clear
      self.class.config_attributes.each do |attr|
        send("#{attr}=", nil)
      end
      self
    end

    def load(overrides = {})
      config = load_from_sources((self.class.defaults || {}).deep_dup)

      config.merge!(overrides) unless overrides.nil?
      config.each do |key, val|
        set_value(key, val)
      end
    end

    def load_from_sources(config = {})
      # Handle anonymous configs
      return config unless config_name
      load_from_file(config)
      load_from_env(config)
    end

    def load_from_file(config)
      config_path = Anyway.env.fetch(env_prefix).delete('conf') ||
                    "./config/#{config_name}.yml"
      config.deep_merge!(parse_yml(config_path) || {}) if config_path && File.file?(config_path)
      config
    end

    def load_from_env(config)
      config.deep_merge!(Anyway.env.fetch(env_prefix))
      config
    end

    def to_h
      self.class.config_attributes.each_with_object({}) do |key, obj|
        obj[key.to_sym] = send(key)
      end.deep_dup.deep_freeze
    end

    private

    def set_value(key, val)
      send("#{key}=", val) if respond_to?(key)
    end

    def parse_yml(path)
      require 'yaml'
      if defined?(ERB)
        YAML.safe_load(ERB.new(File.read(path)).result, [], [], true)
      else
        YAML.load_file(path)
      end
    end
  end
end