# frozen_string_literal: true

require 'faraday'
require 'faraday/typhoeus'
require 'json'

require_relative '../components/errors'

module Ollama
  module Controllers
    class Client
      DEFAULT_ADDRESS = 'http://localhost:11434'

      ALLOWED_REQUEST_OPTIONS = %i[timeout open_timeout read_timeout write_timeout].freeze

      DEFAULT_FARADAY_ADAPTER = :typhoeus

      def initialize(config)
        @server_sent_events = config.dig(:options, :server_sent_events)

        @address = if config[:credentials][:address].nil? || config[:credentials][:address].to_s.strip.empty?
                     "#{DEFAULT_ADDRESS}/"
                   else
                     "#{config[:credentials][:address].to_s.sub(%r{/$}, '')}/"
                   end

        @bearer_token = config[:credentials][:bearer_token]

        @request_options = config.dig(:options, :connection, :request)

        @request_options = if @request_options.is_a?(Hash)
                             @request_options.select do |key, _|
                               ALLOWED_REQUEST_OPTIONS.include?(key)
                             end
                           else
                             {}
                           end

        @faraday_adapter = config.dig(:options, :connection, :adapter) || DEFAULT_FARADAY_ADAPTER
      end

      def generate(payload, server_sent_events: nil, &callback)
        request('api/generate', payload, server_sent_events:, &callback)
      end

      def chat(payload, server_sent_events: nil, &callback)
        request('api/chat', payload, server_sent_events:, &callback)
      end

      def create(payload, server_sent_events: nil, &callback)
        request('api/create', payload, server_sent_events:, &callback)
      end

      def tags(server_sent_events: nil, &callback)
        request('api/tags', nil, server_sent_events:, request_method: 'GET', &callback)
      end

      def show(payload, server_sent_events: nil, &callback)
        request('api/show', payload, server_sent_events:, &callback)
      end

      def copy(payload, server_sent_events: nil, &callback)
        request('api/copy', payload, server_sent_events:, &callback)
        true
      end

      def delete(payload, server_sent_events: nil, &callback)
        request('api/delete', payload, server_sent_events:, request_method: 'DELETE', &callback)
        true
      end

      def pull(payload, server_sent_events: nil, &callback)
        request('api/pull', payload, server_sent_events:, &callback)
      end

      def push(payload, server_sent_events: nil, &callback)
        request('api/push', payload, server_sent_events:, &callback)
      end

      def embeddings(payload, server_sent_events: nil, &callback)
        request('api/embeddings', payload, server_sent_events:, &callback)
      end

      def request(path, payload = nil, server_sent_events: nil, request_method: 'POST', &callback)
        server_sent_events_enabled = server_sent_events.nil? ? @server_sent_events : server_sent_events
        url = "#{@address}#{path}"

        if !callback.nil? && !server_sent_events_enabled
          raise Errors::BlockWithoutServerSentEventsError,
                'You are trying to use a block without Server Sent Events (SSE) enabled.'
        end

        results = []

        method_to_call = request_method.to_s.strip.downcase.to_sym

        partial_json = String.new.force_encoding('UTF-8')

        response = Faraday.new(request: @request_options) do |faraday|
          faraday.adapter @faraday_adapter
          faraday.response :raise_error
          faraday.request :authorization, 'Bearer', @bearer_token if @bearer_token
        end.send(method_to_call) do |request|
          request.url url
          request.headers['Content-Type'] = 'application/json'

          request.body = payload.to_json unless payload.nil?

          if server_sent_events_enabled
            request.options.on_data = proc do |chunk, bytes, env|
              if env && env.status != 200
                raise_error = Faraday::Response::RaiseError.new
                raise_error.on_complete(env.merge(body: chunk))
              end

              utf8_chunk = chunk.force_encoding('UTF-8')

              partial_json += if utf8_chunk.valid_encoding?
                                utf8_chunk
                              else
                                utf8_chunk.encode('UTF-8', invalid: :replace, undef: :replace)
                              end

              parsed_json = safe_parse_json(partial_json)

              if parsed_json
                result = { event: parsed_json, raw: { chunk:, bytes:, env: } }

                callback.call(result[:event], result[:raw]) unless callback.nil?

                results << result

                partial_json = String.new.force_encoding('UTF-8')
              end
            end
          end
        end

        return safe_parse_jsonl(response.body) unless server_sent_events_enabled

        results.map { |result| result[:event] }
      rescue Faraday::Error => e
        raise Errors::RequestError.new(e.message, request: e, payload:)
      end

      def safe_parse_json(raw)
        raw.to_s.lstrip.start_with?('{', '[') ? JSON.parse(raw) : nil
      rescue JSON::ParserError
        nil
      end

      def safe_parse_jsonl(raw)
        raw.to_s.lstrip.start_with?('{', '[') ? raw.split("\n").map { |line| JSON.parse(line) } : raw
      rescue JSON::ParserError
        raw
      end
    end
  end
end
