require 'ipaddr'
require 'resolv'

class Proxy < ActiveRecord::Base
  ECHO_API_HOST = 'echo-api.3scale.net'.freeze

  belongs_to :service, touch: true
  attr_readonly :service_id

  has_many :proxy_rules, dependent: :destroy, inverse_of: :proxy
  has_many :proxy_configs, dependent: :delete_all, inverse_of: :proxy

  validates :api_backend, :error_status_no_match, :error_status_auth_missing, :error_status_auth_failed, presence: true

  uri_pattern = URI::DEFAULT_PARSER.pattern

  URI_OPTIONAL_PORT = /\Ahttps?:\/\/[a-zA-Z0-9._-]*(:\d+)?\Z/
  URI_OR_LOCALHOST  = /\A(https?:\/\/([a-zA-Z0-9._:\/?-])+|.*localhost.*)\Z/
  OPTIONAL_QUERY_FORMAT = "(?:\\?(#{uri_pattern.fetch(:QUERY)}))?"
  URI_PATH_PART = Regexp.new('\A' + uri_pattern.fetch(:ABS_PATH) + OPTIONAL_QUERY_FORMAT + '\z')
  HOSTNAME = Regexp.new('\A' + uri_pattern.fetch(:HOSTNAME) + '\z')

  OAUTH_PARAMS = /(\?|&)(scope=|state=|tok=)/

  APP_OR_USER_KEY = /\A[\w\d_-]+\Z/

  HTTP_HEADER =  /\A[{}\[\]\d,.;@#~%&()?\w_"= \/\\:-]+\Z/

  validates :api_backend,      format: { with: URI_OPTIONAL_PORT,  allow_nil: true }
  validates :api_test_path,    format: { with: URI_PATH_PART,      allow_nil: true, allow_blank: true }
  validates :endpoint,         format: { with: URI_OPTIONAL_PORT,  allow_nil: true, allow_blank: true }
  validates :sandbox_endpoint, format: { with: URI_OPTIONAL_PORT , allow_nil: true }

  validates :hostname_rewrite, format: { with: HOSTNAME,           allow_nil: true, allow_blank: true }

  validates :oauth_login_url, format: { with: URI_OR_LOCALHOST,    allow_nil: true, allow_blank: true }

  validates :oauth_login_url, format: { without: OAUTH_PARAMS }, length: { maximum: 255 }

  validates :credentials_location, inclusion: { in: %w(headers query), allow_nil: false }

  validates :error_status_no_match, :error_status_auth_missing, :error_status_auth_failed,
                            numericality: { greater_than_or_equal_to: 200, less_than: 600 }

  validates :auth_app_id, :auth_app_key, :auth_user_key, :secret_token,
                      format: { with: APP_OR_USER_KEY, allow_nil: false }

  validates :error_headers_auth_failed,
                      :error_headers_auth_missing, :error_headers_no_match,
                      :error_auth_failed,
                      :error_auth_missing, :error_no_match,
                      format: { with: HTTP_HEADER }

  validates :api_test_path, length: { maximum: 8192 }
  validates :endpoint, :api_backend, :auth_app_key, :auth_app_id, :auth_user_key,
            :credentials_location, :error_auth_failed, :error_auth_missing,
            :error_headers_auth_failed, :error_headers_auth_missing, :error_no_match,
            :error_headers_no_match, :secret_token, :hostname_rewrite, :sandbox_endpoint,
            length: { maximum: 255 }

  validate :api_backend_not_localhost

  accepts_nested_attributes_for :proxy_rules, allow_destroy: true

  before_validation :set_api_test_path, :set_api_backend, :create_default_secret_token, :set_port_api_backend,  :set_port_sandbox_endpoint, :set_port_endpoint
  after_create :create_default_proxy_rule

  before_create :set_sandbox_endpoint
  before_create :set_production_endpoint

  validates :sandbox_endpoint, presence: true, on: :update

  validates :endpoint, presence: true, on: :update, unless: :saas_self_managed?

  # saas self managed deployment option is called 'on_premise'
  def saas_self_managed?
    service.deployment_option == 'on_premise'
  end

  def self.credentials_collection
    I18n.t('proxy.credentials_location').to_a.map(&:reverse)
  end

  def self.config
    System::Application.config.three_scale.sandbox_proxy
  end

  def save_and_async_deploy(attrs, user)
    saved = update_attributes(attrs)

    analytics.track('Sandbox Proxy updated', analytics_attributes.merge(success: saved))

    saved && async_deploy(user)
  end

  def async_deploy(user)
    ProviderProxyDeploymentService.async_deploy(user, self)
  end

  def hosts
    [endpoint, sandbox_endpoint].map do |endpoint|
      begin
        URI(endpoint || ''.freeze).host
      rescue ArgumentError, URI::InvalidURIError
        'localhost'.freeze
      end
    end.compact.uniq
  end

  def backend
    config = self.class.config
    old_endpoint_config = "#{config.backend_scheme}://#{config.backend_host}"
    endpoint = URI(config.backend_endpoint.presence || old_endpoint_config)

    {
      endpoint: endpoint.to_s,
      host: endpoint.host
    }
  end

  def save_and_deploy(attrs = {})
    saved = update_attributes(attrs)

    analytics.track('Sandbox Proxy updated', analytics_attributes.merge(success: saved))

    saved && deploy
  end

  def deploy!
    deploy
  end

  def authentication_params_for_proxy(opts = {})
    params = service.plugin_authentication_params
    keys_to_proxy_args = {app_key: :auth_app_key,
                          app_id: :auth_app_id,
                          user_key: :auth_user_key,  }

    # {'app_id' => 'foo', 'app_key_mine' => 'bar'}
    params.keys.map do |x|
       param_name = opts[:original_names] ? x.to_s : send(keys_to_proxy_args[x])
       [ param_name, params[x]  ]
    end.to_h
  end


  def send_api_test_request!
    proxy_test_service = ProxyTestService.new(self)

    return true if proxy_test_service.disabled?

    if self.service.oauth?
      update_column(:api_test_success, nil)
      return true
    end

    result = proxy_test_service.perform

    success = result.success?

    update_column(:api_test_success, success)

    error, description = result.error

    if error && description
      errors.add(:api_test_path, "<b>#{error}</b>: #{CGI.escapeHTML(description)}")
    end

    analytics.track 'Sandbox Proxy Test Request',
                    analytics_attributes.merge(
                        success: success,
                        uri: result.uri.to_s,
                        status: result.code
                    )

    success
  end

  def enabled
    !!self.deployed_at
  end

  def sandbox_deployed?
    proxy_log = provider.proxy_logs.latest_first.first or return sandbox_config_saved?
    proxy_log.created_at > self.created_at && proxy_log.status == ProviderProxyDeploymentService::SUCCESS_MESSAGE
  end

  def sandbox_config_saved?
    proxy_configs.sandbox.exists?
  end

  def endpoint_port
    URI(endpoint.presence).port
  rescue ArgumentError, URI::InvalidURIError
    URI::HTTP::DEFAULT_PORT
  end

  def hostname_rewrite_for_sandbox
    hostname_rewrite.presence ||
      (self.api_backend ? URI(self.api_backend).host : 'none')
  end

  def deploy_production
    if provider.provider_can_use?(:apicast_v2)
      deploy_production_v2
    elsif ready_to_deploy?
      provider.deploy_production_apicast
    end
  end

  def ready_to_deploy?
    api_test_success
  end

  def deploy_v2
    deployment = ApicastV2DeploymentService.new(self)

    deployment.call(environment: 'sandbox'.freeze)
  end

  def deploy_production_v2
    newest_sandbox_config = proxy_configs.sandbox.newest_first.first

    newest_sandbox_config.clone_to(environment: :production) if newest_sandbox_config
  end

  def set_correct_endpoint(deployment_option)
    case deployment_option
    when 'on_premise'.freeze
      update_attribute(:endpoint, nil)
    when 'on_3scale'.freeze
      update_attribute(:endpoint, hosted_proxy_endpoint)
    end
  end

  # Used for production
  def hosted_proxy_endpoint
    generate_apicast_endpoint_url(:hosted_proxy_endpoint)
  end

  def hosted_proxy_staging_endpoint
    generate_apicast_endpoint_url(:sandbox_endpoint)
  end

  delegate :backend_version, to: :service, prefix: true

  def default_api_backend
    "https://#{ECHO_API_HOST}:443"
  end

  delegate :provider_key, to: :provider

  def sandbox_host
    URI(sandbox_endpoint || set_sandbox_endpoint).host
  end

  def provider
    @provider ||= self.service.account
  end

  protected

  def create_default_secret_token
    unless secret_token
      self.secret_token = "Shared_secret_sent_from_proxy_to_API_backend_#{SecureRandom.hex(8)}"
    end
  end

  def api_backend_not_localhost
    return unless errors.blank?
    return if Rails.env.test?

    return if self.api_backend.blank?

    begin
      uri = URI.parse(self.api_backend)

    rescue URI::InvalidURIError => e
      errors.add(:api_backend, "Invalid domain")
      return
    end

    if uri.host.blank?
      errors.add(:api_backend, "incorrect domain")
      return
    end

    if uri.host =~ /\A(localhost|127\.0\.0\.1)\Z/
      errors.add(:api_backend, "Sorry, this domain is protected.")
      return
    end
  end

  def analytics
    ThreeScale::Analytics.current_user
  end

  def analytics_attributes
    { api_backend: api_backend, api_test_path: api_test_path }
  end

  def create_default_proxy_rule
    proxy_rules.create(http_method: 'GET', pattern: '/', delta: 1, metric: service.metrics.first)
  end

  def deploy
    provider.provider_can_use?(:apicast_v2) ? deploy_v2 : deploy_v1
  end

  def deploy_v1
    deployment = ProviderProxyDeploymentService.new(provider)

    success = deployment.deploy(self)

    analytics.track('Sandbox Proxy Deploy', success: success)

    success
  end

  def set_api_backend
    self.api_backend ||= self.default_api_backend
  end

  def set_api_test_path
    self.api_test_path ||= '/'
  end

  def proxy_env
     {preview: 'pre.', production: ''}[Rails.env.to_sym] or ''
  end

  def proxy_port
    Rails.env.test? ? '44432' : '443'
  end

  def set_sandbox_endpoint
    url = hosted_proxy_staging_endpoint
    return if url.blank?
    self.sandbox_endpoint = url
  end

  def set_production_endpoint
    url = hosted_proxy_endpoint
    return if url.blank?
    self.endpoint = url
  end

  def set_port_api_backend
    generate_port(:api_backend)
  end

  def set_port_sandbox_endpoint
    generate_port(:sandbox_endpoint)
  end

  def set_port_endpoint
    generate_port(:endpoint)
  end

  def generate_port(proxy_attribute)
    proxy_attribute_value = self[proxy_attribute]
    return if proxy_attribute_value.blank?

    begin
      uri = URI.parse(proxy_attribute_value)
      value = URI::Generic.new(uri.scheme, uri.userinfo, uri.host, uri.port, uri.registry, uri.path, uri.opaque, uri.query, uri.fragment).to_s
      self[proxy_attribute] = value
    rescue URI::InvalidURIError => e
      errors.add(proxy_attribute, "Invalid domain")
    end
  end

  def generate_apicast_endpoint_url(endpoint_config)
    template = self.class.config[endpoint_config.to_sym] or return
    template % {
      system_name: service.parameterized_system_name, account_id: service.account_id,
      env: proxy_env, port: proxy_port
    }
  end
end
