require 'json'
require 'ejs'
require_dependency 'carto/table_utils'

module CartoDB
  # Natural naming would be "Layer", but it collides with Layer class
  module LayerModule
    class Presenter
      include Carto::TableUtils
      include ::LoggerHelper

      EMPTY_CSS = '#dummy{}'

      TORQUE_ATTRS = %w(
        table_name
        user_name
        property
        blendmode
        resolution
        countby
        torque-duration
        torque-steps
        torque-blend-mode
        query
        source
        tile_style
        named_map
        visible
      )

      INFOWINDOW_KEYS = %w(
        fields template_name template alternative_names width maxHeight
      )

      def initialize(layer, options={}, configuration={}, decoration_data={})
        @layer            = layer
        @options          = options
        @configuration    = configuration
        @decoration_data  = decoration_data
      end

      def to_vizjson_v2
        if base?(layer)
          with_kind_as_type(layer.public_values).symbolize_keys
        elsif torque?(layer)
          as_torque
        else
          {
            id:         layer.id,
            type:       'CartoDB',
            infowindow: infowindow_data_v2,
            tooltip:    tooltip_data_v2,
            legend:     layer.legend,
            order:      layer.order,
            visible:    layer.public_values['options']['visible'],
            options:    options_data_v2
          }
        end
      end

      # Old layers_controller directly does layer.to_json, but to be uniform with new controller,
      # always call through the presenter at least in tests
      def to_json(*args)
        layer.to_json(*args)
      end

      def to_vizjson_v1
        return layer.public_values.symbolize_keys if base?(layer)
        {
          id:         layer.id,
          kind:       'CartoDB',
          infowindow: infowindow_data_v1,
          order:      layer.order,
          options:    options_data_v1
        }
      end

      def to_poro
        # .merge left for backwards compatibility
        poro = layer.public_values

        return poro unless poro['options']

        # INFO changed to support new  presenter's way of sending owner
        if @options[:user] && !poro['options']['user_name'].present?
          user_name = @options[:user].username
          schema_name = @options[:user].sql_safe_database_schema
        elsif poro['options']['user_name'].present?
          user_name = poro['options']['user_name']
          schema_name = poro['options']['user_name']
        end

        if options[:viewer_user] && user_name && poro['options']['table_name']
          # if the table_name already have a schema don't add another one
          # this case happens when you share a layer already shared with you
          if user_name != options[:viewer_user].username && !poro['options']['table_name'].include?('.')
            poro['options']['table_name'] = safe_schema_and_table_quoting(schema_name, poro['options']['table_name'])
          end
        end
        poro
      end

      private

      attr_reader :layer, :options, :configuration

      # Decorates the layer presentation with data if needed. nils on the decoration act as removing the field
      def decorate_with_data(source_hash, decoration_data)
        decoration_data.each { |key, value|
          source_hash[key] = value
          source_hash.delete_if { |k, v|
            v.nil?
          }
        }
        source_hash
      end

      def base?(layer)
        ['tiled', 'background', 'gmapsbase', 'wms'].include? layer.kind
      end

      def torque?(layer)
        layer.kind == 'torque'
      end

      def with_kind_as_type(attributes)
        decorate_with_data(attributes.merge(type: attributes.delete('kind')), @decoration_data)
      end

      def as_torque
        api_templates_type = options.fetch(:https_request, false) ? 'private' : 'public'
        layer_options = decorate_with_data(
            # Make torque always have a SQL query too (as vizjson v2)
            layer.options.merge({ 'query' => wrap(sql_from(layer.options), layer.options) }),
            @decoration_data
          )

        {
          id:         layer.id,
          type:       'torque',
          order:      layer.order,
          legend:     layer.legend,
          options:    {
            stat_tag:           options.fetch(:visualization_id),
            maps_api_template: fix_torque_maps_api_template(ApplicationHelper.maps_api_template(api_templates_type)),
            sql_api_template: ApplicationHelper.sql_api_template(api_templates_type),
            # tiler_* is kept for backwards compatibility
            tiler_protocol:     (configuration[:tiler]["public"]["protocol"] rescue nil),
            tiler_domain:       (configuration[:tiler]["public"]["domain"] rescue nil),
            tiler_port:         (configuration[:tiler]["public"]["port"] rescue nil),
            # sql_api_* is kept for backwards compatibility
            sql_api_protocol:   (configuration[:sql_api]["public"]["protocol"] rescue nil),
            sql_api_domain:     (configuration[:sql_api]["public"]["domain"] rescue nil),
            sql_api_endpoint:   (configuration[:sql_api]["public"]["endpoint"] rescue nil),
            sql_api_port:       (configuration[:sql_api]["public"]["port"] rescue nil),
            layer_name:         name_for(layer),
          }.merge(
            layer_options.select { |k| TORQUE_ATTRS.include? k })
        }
      end

      def infowindow_data_v1
        with_template(layer.infowindow, layer.infowindow_template_path)
      rescue StandardError => e
        log_error(exception: e)
        throw e
      end

      def infowindow_data_v2
        whitelisted_infowindow(with_template(layer.infowindow, layer.infowindow_template_path))
      rescue StandardError => e
        log_error(exception: e)
        throw e
      end

      def tooltip_data_v2
        whitelisted_infowindow(with_template(layer.tooltip, layer.tooltip_template_path))
      rescue StandardError => e
        log_error(exception: e)
        throw e
      end

      def with_template(infowindow, path)
        # Careful with this logic:
        # - nil means absolutely no infowindow (e.g. a torque)
        # - path = nil or template filled: either pre-filled or custom infowindow, nothing to do here
        # - template and path not nil but template not filled: stay and fill
        return nil if infowindow.nil?
        template = infowindow['template']
        return infowindow if (!template.nil? && !template.empty?) || path.nil?

        infowindow.merge!(template: File.read(path))
        infowindow
      end

      def options_data_v2
        if options[:full]
          decorate_with_data(layer.options, @decoration_data)
        else
          layer_options = layer.options
          data = {
            layer_name:         name_for(layer),
            cartocss:           css_from(layer.options),
            cartocss_version:   layer.options.fetch('style_version'),
            interactivity:      layer.options.fetch('interactivity')
          }
          source = layer.options['source']
          if options[:for_named_map] && source
            data[:source] = { id: source }
          else
            data[:sql] = wrap(sql_from(layer.options), layer.options)
          end

          if layer_options['style_properties'] && (layer_options['style_properties']['autogenerated'] == false || layer_options['style_properties']['autogenerated'].nil?)
            sql_wrap = layer_options['sql_wrap'] || layer_options['query_wrapper']
            data['sql_wrap'] = sql_wrap if sql_wrap.present?
          end

          data = decorate_with_data(data, @decoration_data)

          viewer = options[:viewer_user]
          if viewer
            unless data['user_name'] == viewer.username
              data['table_name'] = "\"#{data['user_name']}\".#{data['table_name']}"
            end
          end
          data
        end
      end

      def name_for(layer)
        layer_alias = layer.options.fetch('table_name_alias', nil)
        table_name  = layer.options.fetch('table_name')

        return table_name unless layer_alias && !layer_alias.empty?
        layer_alias
      end

      def options_data_v1
        return layer.options if options[:full]
        layer.options.select { |key, value| public_options.include?(key.to_s) }
      end

      def sql_from(options)
        query = options.fetch('query', '')
        return default_query_for(options) if query.nil? || query.empty?
        query
      end

      def css_from(options)
        style = options.include?('tile_style') ? options['tile_style'] : nil
        (style.nil? || style.strip.empty?) ? EMPTY_CSS : options.fetch('tile_style')
      end

      def wrap(query, options)
        wrapper = options.fetch('query_wrapper', nil)
        return query if wrapper.nil? || wrapper.empty?
        EJS.evaluate(wrapper, sql: query)
      end

      def default_query_for(layer_options)
        if options[:viewer_user]
          unless layer_options['user_name'] == options[:viewer_user].username
            name = if layer_options['user_name'] && layer_options['user_name'].include?('-')
                     "\"#{layer_options['user_name']}\""
                   else
                     layer_options['user_name']
                   end

            return "select * from #{name}.#{safe_table_name_quoting(layer_options['table_name'])}"
          end
        end
        "select * from #{safe_table_name_quoting(layer_options.fetch('table_name'))}"
      end

      def public_options
        return configuration if configuration.empty?
        configuration.fetch(:layer_opts).fetch("public_opts")
      end

      def whitelisted_infowindow(infowindow)
        infowindow.nil? ? nil : infowindow.select { |key, value|
                                                    INFOWINDOW_KEYS.include?(key) || INFOWINDOW_KEYS.include?(key.to_s)
                                                  }
      end

      def fix_torque_maps_api_template(maps_api_template)
        if visualization_owner_is_table_owner?
          maps_api_template
        else
          # This fixes #9017 avoding Torque request to table owner named map instead of visualization owner.
          # See https://github.com/CartoDB/torque/blob/8a14fe546ac411829b5f52bb575526ad5ecb79f8/lib/torque/provider/windshaft.js#L361
          maps_api_template.gsub('{user}', layer.user.username)
        end
      end

      def visualization_owner_is_table_owner?
        layer.options.nil? || layer.options['user_name'].nil? || layer.user.nil? ||
          layer.options['user_name'] == layer.user.username
      end
    end
  end
end
