require 'json'
require 'net/http'
require 'uri'

require 'oso/version'

module Oso
  class Client
    def initialize(url: 'https://cloud.osohq.com', api_key: nil)
      @url = url
      @api_key = api_key
    end

    def policy(policy)
      POST('policy', { src: policy })
    end

    def authorize(actor, action, resource, context_facts = [])
      actor_typed_id = extract_typed_id actor
      resource_typed_id = extract_typed_id resource
      result = POST('authorize', {
        actor_type: actor_typed_id.type, actor_id: actor_typed_id.id,
        action: action,
        resource_type: resource_typed_id.type, resource_id: resource_typed_id.id,
        context_facts: facts_to_params(context_facts)
      })
      allowed = result['allowed']
      allowed
    end

    def authorize_resources(actor, action, resources, context_facts = [])
      return [] if resources.nil?
      return [] if resources.empty?

      key = lambda do |type, id|
        "#{type}:#{id}"
      end

      resources_extracted = resources.map { |r| extract_typed_id(r) }
      actor_typed_id = extract_typed_id actor
      result = POST('authorize_resources', {
        actor_type: actor_typed_id.type, actor_id: actor_typed_id.id,
        action: action,
        resources: resources_extracted,
        context_facts: facts_to_params(context_facts)
      })

      return [] if result['results'].empty?

      results_lookup = Hash.new
      result['results'].each do |r|
        k = key.call(r['type'], r['id'])
        if results_lookup[k] == nil
          results_lookup[k] = true
        end
      end

      results = resources.select do |r|
        e = extract_typed_id(r)
        exists = results_lookup[key.call(e.type, e.id)]
        exists
      end
      results
    end

    def list(actor, action, resource_type, context_facts = [])
      actor_typed_id = extract_typed_id actor
      result = POST('list', {
        actor_type: actor_typed_id.type, actor_id: actor_typed_id.id,
        action: action,
        resource_type: resource_type,
        context_facts: facts_to_params(context_facts)
      })
      results = result['results']
      results
    end

    def actions(actor, resource, context_facts = [])
      actor_typed_id = extract_typed_id actor
      resource_typed_id = extract_typed_id resource
      result = POST('actions', {
        actor_type: actor_typed_id.type, actor_id: actor_typed_id.id,
        resource_type: resource_typed_id.type, resource_id: resource_typed_id.id,
        context_facts: facts_to_params(context_facts)
      })
      results = result['results']
      results
    end

    def tell(predicate, *args)
      typed_args = args.map { |a| extract_typed_id a}
      POST('facts', { predicate: predicate, args: typed_args })
    end

    def bulk_tell(facts)
      POST('bulk_load', facts_to_params(facts))
    end

    def delete(predicate, *args)
      typed_args = args.map { |a| extract_typed_id a}
      DELETE('facts', { predicate: predicate, args: typed_args })
    end

    def bulk_delete(facts)
      POST('bulk_delete', facts_to_params(facts))
    end

    def get(predicate, *args)
      params = {predicate: predicate}
      args.each_with_index do |arg, i|
        typed_id = extract_arg_query(arg)
        if typed_id
          params["args.#{i}.type"] = typed_id.type
          params["args.#{i}.id"] = typed_id.id
        end
      end

      GET('facts', params)
    end

    private

    def headers()
      {
        "Authorization" => "Basic %s" % @api_key,
        "User-Agent" => "Oso Cloud (ruby)",
        "Accept": "application/json",
        "Content-Type": "application/json"
      }
    end


    def GET(path, params)
      uri = URI("#{@url}/api/#{path}")
      uri.query = URI::encode_www_form(params)
      use_ssl = (uri.scheme == 'https')

      result = Net::HTTP.start(uri.hostname, uri.port, use_ssl: use_ssl ) { |http|
        http.request(Net::HTTP::Get.new(uri, headers)) {|r|
          r.read_body
        }
      }
      handle_result result

    end

    def POST(path, params)
      result = Net::HTTP.post(URI("#{@url}/api/#{path}"), params.to_json, headers)
      handle_result result
    end

    def DELETE(path, params)
      uri = URI("#{@url}/api/#{path}")
      use_ssl = (uri.scheme == 'https')
      result = Net::HTTP.start(uri.hostname, uri.port, use_ssl: use_ssl ) { |http|
        http.request(Net::HTTP::Delete.new(uri, headers), params.to_json) {|r|
          r.read_body
        }
      }
      handle_result result
    end

    def handle_result(result)
      unless result.is_a?(Net::HTTPSuccess)
        raise "Got an unexpected error from Oso Service: #{result.code}\n#{result.body}"
      end

      JSON.parse(result.body)
    end

    def extract_typed_id(x)
      return TypedId.new(type: "String", id: x) if x.is_a? String

      raise "#{x} does not have an 'id' field" unless x.respond_to? :id
      raise "Invalid 'id' field on #{x}: #{x.id}" if x.id.nil?

      TypedId.new(type: x.class.name, id: x.id.to_s)
    end

    def extract_arg_query(x)
      return nil if x.nil?
      extract_typed_id(x)
    end

    def facts_to_params(facts)
      facts.map { |predicate, *args|
        typed_args = args.map { |a| extract_typed_id a}
        { predicate: predicate, args: typed_args }
      }
    end

    TypedId = Struct.new(:type, :id, keyword_init: true) do
      def to_json(*args)
        to_h.to_json(*args)
      end
    end
  end
end