require 'jsonapi/resource_serializer'
require 'action_controller'
require 'jsonapi/exceptions'
require 'jsonapi/error'
require 'jsonapi/error_codes'
require 'jsonapi/request'
require 'jsonapi/operations_processor'
require 'jsonapi/active_record_operations_processor'
require 'csv'

module JSONAPI
  class ResourceController < ActionController::Base
    before_filter :ensure_correct_media_type, only: [:create, :update, :create_association, :update_association]
    before_filter :setup_request
    after_filter :setup_response

    def index
      serializer = JSONAPI::ResourceSerializer.new(resource_klass,
                                                   include: @request.include,
                                                   fields: @request.fields,
                                                   base_url: base_url,
                                                   key_formatter: key_formatter,
                                                   route_formatter: route_formatter)

      resource_records = resource_klass.find(resource_klass.verify_filters(@request.filters, context),
                                             context: context,
                                             sort_criteria: @request.sort_criteria,
                                             paginator: @request.paginator)

      render json: serializer.serialize_to_hash(resource_records)
    rescue => e
      handle_exceptions(e)
    end

    def show
      serializer = JSONAPI::ResourceSerializer.new(resource_klass,
                                                   include: @request.include,
                                                   fields: @request.fields,
                                                   base_url: base_url,
                                                   key_formatter: key_formatter,
                                                   route_formatter: route_formatter)

      key = resource_klass.verify_key(params[resource_klass._primary_key], context)

      resource_record = resource_klass.find_by_key(key, context: context)

      render json: serializer.serialize_to_hash(resource_record)
    rescue => e
      handle_exceptions(e)
    end

    def show_association
      association_type = params[:association]

      parent_key = resource_klass.verify_key(params[resource_klass._as_parent_key], context)

      parent_resource = resource_klass.find_by_key(parent_key, context: context)

      association = resource_klass._association(association_type)

      serializer = JSONAPI::ResourceSerializer.new(resource_klass,
                                                   fields: @request.fields,
                                                   base_url: base_url,
                                                   key_formatter: key_formatter,
                                                   route_formatter: route_formatter)

      render json: serializer.serialize_to_links_hash(parent_resource, association)
    rescue => e
      handle_exceptions(e)
    end

    def create
      process_request_operations
    end

    def create_association
      process_request_operations
    end

    def update_association
      process_request_operations
    end

    def update
      process_request_operations
    end

    def destroy
      process_request_operations
    end

    def destroy_association
      process_request_operations
    end

    def get_related_resource
      association_type = params[:association]
      source_resource = @request.source_klass.find_by_key(@request.source_id, context: context)

      serializer = JSONAPI::ResourceSerializer.new(@request.source_klass,
                                                   include: @request.include,
                                                   fields: @request.fields,
                                                   base_url: base_url,
                                                   key_formatter: key_formatter,
                                                   route_formatter: route_formatter)

      render json: serializer.serialize_to_hash(source_resource.send(association_type))
    end

    def get_related_resources
      association_type = params[:association]
      source_resource = @request.source_klass.find_by_key(@request.source_id, context: context)

      related_resources = source_resource.send(association_type,
                                               {
                                                 filters:  @request.source_klass.verify_filters(@request.filters, context),
                                                 sort_criteria: @request.sort_criteria,
                                                 paginator: @request.paginator
                                               })

      serializer = JSONAPI::ResourceSerializer.new(@request.source_klass,
                                                   include: @request.include,
                                                   fields: @request.fields,
                                                   base_url: base_url,
                                                   key_formatter: key_formatter,
                                                   route_formatter: route_formatter)

      render json: serializer.serialize_to_hash(related_resources)
    end

    # Override this to use another operations processor
    def create_operations_processor
      JSONAPI::ActiveRecordOperationsProcessor.new
    end

    private
    def resource_klass
      @resource_klass ||= resource_klass_name.safe_constantize
    end

    def base_url
      @base_url ||= request.protocol + request.host_with_port
    end

    def resource_klass_name
      @resource_klass_name ||= "#{self.class.name.sub(/Controller$/, '').singularize}Resource"
    end

    def ensure_correct_media_type
      unless request.content_type == JSONAPI::MEDIA_TYPE
        raise JSONAPI::Exceptions::UnsupportedMediaTypeError.new(request.content_type)
      end
    rescue => e
      handle_exceptions(e)
    end

    def setup_request
      @request = JSONAPI::Request.new(params, {
        context: context,
        key_formatter: key_formatter
      })
      render_errors(@request.errors) unless @request.errors.empty?
    rescue => e
      handle_exceptions(e)
    end

    def setup_response
      if response.body.size > 0
        response.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
      end
    end

    # override to set context
    def context
      {}
    end

    # Control by setting in an initializer:
    #     JSONAPI.configuration.json_key_format = :camelized_key
    #     JSONAPI.configuration.route = :camelized_route
    #
    # Override if you want to set a per controller key format.
    # Must return a class derived from KeyFormatter.
    def key_formatter
      JSONAPI.configuration.key_formatter
    end

    def route_formatter
      JSONAPI.configuration.route_formatter
    end

    def render_errors(errors)
      render(json: {errors: errors}, status: errors[0].status)
    end

    def process_request_operations
      results   = create_operations_processor.process(@request)
      errors    = results.select(&:has_errors?).flat_map(&:errors).compact
      resources = results.reject(&:has_errors?).flat_map(&:resource).compact

      status, json = case
        when errors.any?
          [errors[0].status, {errors: errors}]
        when results.any? && resources.any?
          res = resources.length > 1 ? resources : resources[0]
          [results[0].code, processing_serializer.serialize_to_hash(res)]
        else
          [results[0].code, nil]
      end

      render status: status, json: json
    rescue => e
      handle_exceptions(e)
    end

    def processing_serializer
      JSONAPI::ResourceSerializer.new(resource_klass,
                                      include: @request.include,
                                      fields: @request.fields,
                                      base_url: base_url,
                                      key_formatter: key_formatter,
                                      route_formatter: route_formatter)
    end

    # override this to process other exceptions
    # Note: Be sure to either call super(e) or handle JSONAPI::Exceptions::Error and raise unhandled exceptions
    def handle_exceptions(e)
      case e
        when JSONAPI::Exceptions::Error
          render_errors(e.errors)
        else # raise all other exceptions
          # :nocov:
          raise e
          # :nocov:
      end
    end
  end
end