# frozen_string_literal: true

require 'anyway/ext/jruby' if defined? JRUBY_VERSION
require 'anyway/ext/deep_dup'
require 'anyway/ext/deep_freeze'
require 'anyway/ext/hash'
require 'anyway/ext/string_serialize'
require 'anyway/option_parser_builder'

module Anyway # :nodoc:
  if defined? JRUBY_VERSION
    using Anyway::Ext::JRuby
  else
    using Anyway::Ext::DeepDup
    using Anyway::Ext::DeepFreeze
    using Anyway::Ext::Hash
  end
  using Anyway::Ext::StringSerialize

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

      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 ignore_options(*args)
        args.each do |name|
          option_parser_descriptors[name.to_s][:ignore] = true
        end
      end

      def describe_options(**hargs)
        hargs.each do |name, desc|
          option_parser_descriptors[name.to_s][:desc] = desc
        end
      end

      def flag_options(*args)
        args.each do |name|
          option_parser_descriptors[name.to_s][:flag] = true
        end
      end

      def extend_options(&block)
        @option_parser_extension = block
      end

      def option_parser_options
        config_attributes.each_with_object({}) do |key, result|
          descriptor = option_parser_descriptors[key.to_s]
          next if descriptor[:ignore] == true

          result[key] = descriptor
        end
      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 option_parser_descriptors
        @option_parser_descriptors ||= Hash.new { |h, k| h[k] = {} }
      end

      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 })
    #
    def initialize(name: nil, load: true, overrides: {})
      @config_name = name || self.class.config_name

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

      @env_prefix = self.class.env_prefix || @config_name

      self.load(overrides) if load
    end

    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.deep_merge!(parse_yml(config_path) || {}) if config_path && File.file?(config_path)
      config
    end

    def load_from_env(config)
      config.deep_merge!(env_part)
      config
    end

    def option_parser
      @option_parser ||= begin
        parser = OptionParserBuilder.call(self.class.option_parser_options) do |key, arg|
          set_value(key, arg.is_a?(String) ? arg.serialize : arg)
        end
        self.class.option_parser_extension&.call(parser, self) || parser
      end
    end

    def parse_options!(options)
      option_parser.parse!(options)
    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 env_part
      Anyway.env.fetch(env_prefix)
    end

    def config_path
      env_part.delete('conf') || default_config_path
    end

    def default_config_path
      "./config/#{config_name}.yml"
    end

    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