module Cenit
  class Control
    include CanCan::Ability

    attr_reader :app, :action, :controller
    attr_accessor :view

    def initialize(app, controller, action)
      @app = app
      @controller = controller
      params = @controller.request.params.merge(action.path_params).with_indifferent_access

      {
        controller: ['app'],
        action: ['index'],
        id_or_ns: [app.slug_id, app.get_identifier, app.ns_slug],
        app_slug: [app.slug]
      }.each do |key, value|
        params.delete(key) if value.include?(params[key])
      end

      action_hash = {
        http_method: action.method,
        path: action.request_path,
        params: params,
        query_parameters: @controller.request.query_parameters,
        body: @controller.request.body,
        content_type: @controller.request.content_type,
        content_length: @controller.request.content_length,
        name: nil
      }

      Struct.new('AppControlAction', *action_hash.keys) unless defined? Struct::AppControlAction
      @action = Struct::AppControlAction.new(*action_hash.values)

      setup_abilities
    end

    def identifier
      @app.slug_id
    end

    def secret_token
      @app.secret_token
    end

    def send_data(*args)
      fail 'Double-rendering' if done?
      @render_called = true
      if args.length == 1 && (res = args[0]).is_a?(Setup::Webhook::HttpResponse)
        @controller.send_data res.body, content_type: res.content_type, status: res.code
      else
        @controller.send_data(*args)
      end
    end

    def render(*args)
      fail 'Re-calling render' if done?
      @render_called = true
      if args.length == 1 && (res = args[0]).is_a?(Setup::Webhook::HttpResponse)
        if res.headers['content-transfer-encoding']
          @controller.send_data res.body, content_type: res.content_type, status: res.code
        else
          @controller.render text: res.body, content_type: res.content_type, status: res.code
        end
      else
        @controller.render(*args)
      end
    end

    def method_missing(symbol, *args)
      if (match = symbol.to_s.match(/\Arender_(.+)\Z/))
        render "cenit/#{match[1]}", locals: args[0] || {}, layout: 'cenit'
      else
        super
      end
    end

    def respond_to?(*args)
      args[0].to_s =~ /\Arender_.+\Z/ || super
    end

    def respond_to(&block)
      @controller.respond_to do |format|
        block.call(format)
      end
    end

    def render_called?
      @render_called ||= false
    end

    def base_path
      "/app/#{app.slug_id}"
    end

    def redirect_to(*args)
      fail 'Re-calling redirect_to' if redirect_to_called?
      fail 'Double-rendering' if done?
      @redirect_to_called = true
      path = args.first
      if URI.parse(path).relative?
        path = "#{base_path}/#{path}".gsub(/\/+/, '/')
        args[0] = "#{Cenit.homepage}#{path}"
      end
      @controller.redirect_to(*args)
    end

    def redirect_to_called?
      @redirect_to_called ||= false
    end

    def done?
      redirect_to_called? || render_called?
    end

    def authorize(auth, parameters = {})
      case auth
      when Setup::CallbackAuthorization
        if auth.save
          cenit_token = CallbackAuthorizationToken.create(app_id: app.application_id, authorization: auth, data: {})
          parameters[:cenit_token] = cenit_token
          auth_url = auth.authorize_url(parameters)
          cenit_token.save
          controller.session[:oauth_state] = cenit_token.token
          redirect_to auth_url
        else
          fail "Unable to authorize #{auth.custom_title}: #{auth.errors.full_messages.to_sentence}"
        end
      else
        authorize_path = @controller.authorization_authorize_path(id: auth.id.to_s)
        redirect_to "#{Cenit.homepage}#{authorize_path}"
      end
    end

    def app_params
      unless @app_params
        @app_params = {}
        @app.parameters.each { |p| @app_params[p.name] = p.value }
      end
      @app_params
    end

    def [](key)
      v = @app.configuration[key]
      v = request_headers[key.to_s] if v.nil?
      v = get_instance_var(key) if v.nil?
      v
    end

    def []=(key, value)
      set_instance_var(key, value)
    end

    def generate_access_token(options = {})
      unless (app_id = app.application_id)
        fail 'Invalid App, the app identifier ref is broken!'
      end
      unless (access_grant = Cenit::OauthAccessGrant.where(application_id_id: app_id.id).first)
        fail 'No access granted for this App'
      end
      unless (oauth_scope = access_grant.oauth_scope).auth?
        fail 'Granted access does not include the auth scope'
      end
      fail 'Granted access does not include the offline_access scope' unless oauth_scope.offline_access?
      if options[:session_token]
        Cenit::OauthSessionAccessToken
      else
        Cenit::OauthAccessToken
      end.for(app_id, access_grant.scope, User.current)
    end

    def xhr?
      @controller.request.xhr?
    end

    def flash
      @controller.flash
    end

    def cache
      @cache_store ||= ActiveSupport::Cache.lookup_store(:file_store, "#{Rails.root}/tmp/cache/#{@app.slug_id}")
    end

    def logger
      Rails.logger
    end

    def fail(*several_variants)
      super
    end

    def application_title
      alg = algorithm(:application_title, false)
      result = alg ? alg.run(self) : false
      result == false ? @app.name : result
    end

    def render_template(name, layout = nil, locals = {})
      if layout.is_a?(Hash)
        locals = layout
        layout = nil
      end
      locals = locals.to_h.symbolize_keys
      layout ||= locals.delete(:layout)

      locals.merge!(control: self)
      content = get_resource(:translator, name).run(locals)
      layout ? get_resource(:translator, layout).run(locals) : content
    end

    def data_type(name, throw = true)
      get_resource(:data_type, name, throw)
    end

    def resource(name, throw = true)
      get_resource(:resource, name, throw)
    end

    def algorithm(name, throw = true)
      get_resource(:algorithm, name, throw)
    end

    def connection(name, throw = true)
      get_resource(:connection, name, throw)
    end

    def data_file(name, throw = true)
      data_type('Files', throw).where(filename: name).first
    end

    def current_user
      current_user = @controller.current_user
      alg = algorithm(:current_user, false)
      alg ? alg.run([self, current_user]) : current_user
    end

    def app_url(path = nil, params = nil)
      query = params.is_a?(Hash) ? params.to_query() : params.to_s
      url = "#{Cenit.homepage}"
      url << "/app/#{@controller.request.params[:id_or_ns]}"
      url << "/#{path.gsub(/^\/+|\/+$/, '')}" unless path.blank?
      url << "?#{query}" unless query.blank?
      url
    end

    def sign_in_url(return_to = nil)
      return_to = app_url(return_to) if return_to && URI.parse(return_to).relative?
      return_to ||= app_url(@action.path) if @action.http_method == :get
      sign_in_url = @controller.new_user_session_url(return_to: return_to)
      alg = algorithm(:sign_in_url, false)
      alg ? alg.run([self, sign_in_url]) : sign_in_url
    end

    def sign_out_url(return_to = nil)
      return_to = app_url(return_to) if return_to && URI.parse(return_to).relative?
      return_to ||= app_url(@action.path) if @action.http_method == :get
      sign_out_url = @controller.destroy_user_session_url(return_to: return_to)
      alg = algorithm(:sign_out_url, false)
      alg ? alg.run([self, sign_out_url]) : sign_out_url
    end

    def get_resource(type, name, throw = true)
      name, ns = parse_resource_name(name)
      item = Cenit.namespace(ns).send(type, name)
      raise "The (#{ns}::#{name}) #{type.to_s.humanize.downcase} was not found." if throw && item.nil?
      item
    end

    def define_helper(name, &block)
      define_singleton_method(name, &block) unless respond_to?(name)
    end

    def instance_var_defined?(name)
      instance_variable_defined?(instance_var_name(name))
    end

    def get_instance_var(name)
      instance_variable_get(instance_var_name(name))
    end

    def set_instance_var(name, value)
      instance_variable_set(instance_var_name(name), value)
    end

    def instance_var(name, &block)
      name = instance_var_name(name)
      instance_variable_set(name, yield) if block_given? && !instance_variable_defined?(name)
      instance_variable_get(name)
    end

    def request_headers
      @controller.request.headers
    end

    def response_headers
      @controller.response.headers
    end

    def session
      @controller.session
    end

    private

    def instance_var_name(name)
      "@_instance_var_#{name.to_s.gsub(/^@/, '')}".to_sym
    end

    def setup_abilities
      alg = algorithm(:setup_abilities, false)
      alg.run([self, current_user]) if alg.present?
    end

    def parse_resource_name(name)
      name, ns = name.to_s.split(/::|\//).reverse
      ns ||= @app.namespace
      [name, ns]
    end
  end
end