# frozen_string_literal: true

require "faraday"
require "json"
require "cgi"

module SynorDatabase
  # Synor Database SDK Client
  #
  # @example
  #   client = SynorDatabase::Client.new(api_key: 'your-api-key')
  #
  #   # Key-Value operations
  #   client.kv.set("key", "value")
  #   value = client.kv.get("key")
  #
  #   # Document operations
  #   id = client.documents.create("users", { name: "Alice" })
  #   doc = client.documents.get("users", id)
  #
  class Client
    attr_reader :config, :kv, :documents, :vectors, :timeseries

    def initialize(api_key: nil, **options)
      @config = Config.new(api_key: api_key, **options)
      raise ArgumentError, "API key is required" unless @config.api_key

      @conn = Faraday.new(url: @config.base_url) do |f|
        f.request :json
        f.response :json
        f.options.timeout = @config.timeout
        f.headers["Authorization"] = "Bearer #{@config.api_key}"
        f.headers["X-SDK-Version"] = "ruby/#{VERSION}"
      end
      @closed = false

      @kv = KeyValueStore.new(self)
      @documents = DocumentStore.new(self)
      @vectors = VectorStore.new(self)
      @timeseries = TimeSeriesStore.new(self)
    end

    def closed?
      @closed
    end

    def close
      @closed = true
    end

    def health_check
      response = request(:get, "/health")
      response["status"] == "healthy"
    rescue StandardError
      false
    end

    # Key-Value Store
    class KeyValueStore
      def initialize(client)
        @client = client
      end

      def get(key)
        response = @client.request(:get, "/kv/#{CGI.escape(key)}")
        response["value"]
      end

      def set(key, value, ttl: nil)
        body = { key: key, value: value }
        body[:ttl] = ttl if ttl
        @client.request(:put, "/kv/#{CGI.escape(key)}", body)
      end

      def delete(key)
        @client.request(:delete, "/kv/#{CGI.escape(key)}")
      end

      def list(prefix)
        response = @client.request(:get, "/kv?prefix=#{CGI.escape(prefix)}")
        (response["items"] || []).map { |h| KeyValue.from_hash(h) }
      end
    end

    # Document Store
    class DocumentStore
      def initialize(client)
        @client = client
      end

      def create(collection, document)
        response = @client.request(:post, "/collections/#{CGI.escape(collection)}/documents", document)
        response["id"]
      end

      def get(collection, id)
        response = @client.request(:get, "/collections/#{CGI.escape(collection)}/documents/#{CGI.escape(id)}")
        Document.from_hash(response)
      end

      def update(collection, id, update)
        @client.request(:patch, "/collections/#{CGI.escape(collection)}/documents/#{CGI.escape(id)}", update)
      end

      def delete(collection, id)
        @client.request(:delete, "/collections/#{CGI.escape(collection)}/documents/#{CGI.escape(id)}")
      end

      def query(collection, query)
        body = query.to_h.compact
        response = @client.request(:post, "/collections/#{CGI.escape(collection)}/query", body)
        (response["documents"] || []).map { |h| Document.from_hash(h) }
      end
    end

    # Vector Store
    class VectorStore
      def initialize(client)
        @client = client
      end

      def upsert(collection, vectors)
        entries = vectors.map { |v| v.is_a?(Hash) ? v : v.to_h }
        @client.request(:post, "/vectors/#{CGI.escape(collection)}/upsert", { vectors: entries })
      end

      def search(collection, vector, k)
        response = @client.request(:post, "/vectors/#{CGI.escape(collection)}/search", { vector: vector, k: k })
        (response["results"] || []).map { |h| SearchResult.from_hash(h) }
      end

      def delete(collection, ids)
        @client.request(:delete, "/vectors/#{CGI.escape(collection)}", { ids: ids })
      end
    end

    # Time Series Store
    class TimeSeriesStore
      def initialize(client)
        @client = client
      end

      def write(series, points)
        entries = points.map { |p| p.is_a?(Hash) ? p : p.to_h }
        @client.request(:post, "/timeseries/#{CGI.escape(series)}/write", { points: entries })
      end

      def query(series, range, aggregation: nil)
        body = { range: range.to_h }
        body[:aggregation] = aggregation.to_h if aggregation
        response = @client.request(:post, "/timeseries/#{CGI.escape(series)}/query", body)
        (response["points"] || []).map { |h| DataPoint.from_hash(h) }
      end
    end

    # Internal request method
    def request(method, path, body = nil)
      check_closed!

      with_retry do
        response = case method
                   when :get then @conn.get(path)
                   when :post then @conn.post(path, body)
                   when :put then @conn.put(path, body)
                   when :patch then @conn.patch(path, body)
                   when :delete
                     if body
                       @conn.delete(path) { |req| req.body = body.to_json }
                     else
                       @conn.delete(path)
                     end
                   end

        raise_on_error(response) unless response.success?
        response.body || {}
      end
    end

    private

    def check_closed!
      raise ClientClosedError, "Client has been closed" if @closed
    end

    def with_retry
      last_error = nil
      @config.retries.times do |attempt|
        begin
          return yield
        rescue StandardError => e
          last_error = e
          puts "Attempt #{attempt + 1} failed: #{e.message}" if @config.debug
          sleep(2**attempt) if attempt < @config.retries - 1
        end
      end
      raise last_error
    end

    def raise_on_error(response)
      body = response.body || {}
      message = body["message"] || "HTTP #{response.status}"
      code = body["code"]
      raise HttpError.new(message, status_code: response.status, code: code)
    end
  end
end
