# frozen_string_literal: true

require "faraday"
require "faraday/multipart"
require "json"
require "base64"

module SynorStorage
  # Synor Storage SDK Client
  #
  # @example
  #   client = SynorStorage::Client.new(api_key: 'your-api-key')
  #
  #   # Upload file
  #   result = client.upload(data, name: 'file.txt')
  #
  #   # Download file
  #   data = client.download(cid)
  #
  class Client
    attr_reader :config

    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 :multipart
        f.request :url_encoded
        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
    end

    # ==================== Upload Operations ====================

    # Upload data to storage
    #
    # @param data [String, IO] Data to upload
    # @param options [UploadOptions, Hash] Upload options
    # @return [UploadResponse]
    def upload(data, name: nil, content_type: nil, pin: true, metadata: nil, wrap_with_directory: false)
      check_closed!

      payload = {
        file: Faraday::Multipart::FilePart.new(
          StringIO.new(data.is_a?(String) ? data : data.read),
          content_type || "application/octet-stream",
          name || "file"
        ),
        pin: pin.to_s,
        wrap_with_directory: wrap_with_directory.to_s
      }

      if metadata
        metadata.each do |key, value|
          payload["metadata[#{key}]"] = value
        end
      end

      response = with_retry { @conn.post("/upload", payload) }
      UploadResponse.from_hash(response.body)
    end

    # Upload a directory of files
    #
    # @param files [Array<FileEntry>] Files to upload
    # @param directory_name [String, nil] Optional directory name
    # @return [UploadResponse]
    def upload_directory(files, directory_name: nil)
      check_closed!

      payload = {}
      files.each_with_index do |file, i|
        payload["files[#{i}]"] = Faraday::Multipart::FilePart.new(
          StringIO.new(file.content),
          file.content_type || "application/octet-stream",
          file.path
        )
      end

      payload["directory_name"] = directory_name if directory_name

      response = with_retry { @conn.post("/upload/directory", payload) }
      UploadResponse.from_hash(response.body)
    end

    # ==================== Download Operations ====================

    # Download content by CID
    #
    # @param cid [String]
    # @return [String] Binary data
    def download(cid)
      check_closed!

      response = with_retry do
        @conn.get("/download/#{cid}") do |req|
          req.headers["Accept"] = "*/*"
        end
      end

      response.body
    end

    # Download content with streaming
    #
    # @param cid [String]
    # @yield [String] Chunks of data
    def download_stream(cid, &block)
      check_closed!
      raise ArgumentError, "Block required for streaming" unless block_given?

      @conn.get("/download/#{cid}") do |req|
        req.headers["Accept"] = "*/*"
        req.options.on_data = proc do |chunk, _|
          block.call(chunk)
        end
      end
    end

    # Get gateway URL for a CID
    #
    # @param cid [String]
    # @param path [String, nil] Optional path within the CID
    # @return [String]
    def get_gateway_url(cid, path: nil)
      url = "#{@config.gateway}/ipfs/#{cid}"
      url += "/#{path.gsub(%r{^/}, '')}" if path
      url
    end

    # ==================== Pinning Operations ====================

    # Pin content by CID
    #
    # @param request [PinRequest, Hash] Pin request
    # @return [Pin]
    def pin(request)
      check_closed!

      body = request.is_a?(PinRequest) ? request.to_h : request
      response = with_retry { post_json("/pins", body) }
      Pin.from_hash(response.body)
    end

    # Unpin content
    #
    # @param cid [String]
    def unpin(cid)
      check_closed!

      with_retry { @conn.delete("/pins/#{cid}") }
      nil
    end

    # Get pin status
    #
    # @param cid [String]
    # @return [Pin]
    def get_pin_status(cid)
      check_closed!

      response = with_retry { @conn.get("/pins/#{cid}") }
      Pin.from_hash(response.body)
    end

    # List pins
    #
    # @param status [String, nil] Filter by status
    # @param limit [Integer]
    # @param offset [Integer]
    # @return [Array<Pin>]
    def list_pins(status: nil, limit: 20, offset: 0)
      check_closed!

      params = { limit: limit, offset: offset }
      params[:status] = status if status

      response = with_retry { @conn.get("/pins", params) }
      response.body.map { |p| Pin.from_hash(p) }
    end

    # ==================== CAR File Operations ====================

    # Create a CAR file from entries
    #
    # @param entries [Array<FileEntry>]
    # @return [CarFile]
    def create_car(entries)
      check_closed!

      body = entries.map do |e|
        {
          path: e.path,
          content: Base64.strict_encode64(e.content),
          content_type: e.content_type
        }
      end

      response = with_retry { post_json("/car/create", body) }
      CarFile.from_hash(response.body)
    end

    # Import a CAR file
    #
    # @param car_data [String] Binary CAR data
    # @return [Array<String>] CIDs
    def import_car(car_data)
      check_closed!

      response = with_retry do
        @conn.post("/car/import") do |req|
          req.headers["Content-Type"] = "application/vnd.ipld.car"
          req.body = car_data
        end
      end

      response.body["cids"]
    end

    # Export content as a CAR file
    #
    # @param cid [String]
    # @return [String] Binary CAR data
    def export_car(cid)
      check_closed!

      response = with_retry do
        @conn.get("/car/export/#{cid}") do |req|
          req.headers["Accept"] = "application/vnd.ipld.car"
        end
      end

      response.body
    end

    # ==================== Directory Operations ====================

    # List directory contents
    #
    # @param cid [String]
    # @return [Array<DirectoryEntry>]
    def list_directory(cid)
      check_closed!

      response = with_retry { @conn.get("/directory/#{cid}") }
      response.body.map { |e| DirectoryEntry.from_hash(e) }
    end

    # ==================== Statistics ====================

    # Get storage statistics
    #
    # @return [StorageStats]
    def get_stats
      check_closed!

      response = with_retry { @conn.get("/stats") }
      StorageStats.from_hash(response.body)
    end

    # ==================== Lifecycle ====================

    def close
      @closed = true
      @conn.close if @conn.respond_to?(:close)
    end

    def closed?
      @closed
    end

    private

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

    def post_json(path, body)
      @conn.post(path) do |req|
        req.headers["Content-Type"] = "application/json"
        req.body = JSON.generate(body)
      end
    end

    def with_retry
      attempts = 0
      begin
        attempts += 1
        response = yield
        handle_response(response)
        response
      rescue Faraday::Error, ApiError => e
        if attempts < @config.retries
          sleep(attempts)
          retry
        end
        raise
      end
    end

    def handle_response(response)
      return if response.success?

      error_message = if response.body.is_a?(Hash)
                        response.body["error"] || response.body["message"] || "Unknown error"
                      else
                        "HTTP #{response.status}"
                      end
      raise ApiError.new(error_message, status_code: response.status)
    end
  end
end
