# frozen_string_literal: true

require "faraday"
require "json"

module SynorCompute
  # Synor Compute SDK Client
  #
  # @example
  #   client = SynorCompute::Client.new(api_key: 'your-api-key')
  #
  #   # Matrix multiplication
  #   result = client.matmul(tensor_a, tensor_b, processor: :gpu)
  #
  #   # LLM inference
  #   response = client.inference('llama-3-70b', 'Hello!')
  #
  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 :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
    end

    # ==================== Matrix Operations ====================

    def matmul(a, b, precision: nil, processor: nil, priority: nil)
      check_closed!

      body = {
        operation: "matmul",
        a: tensor_to_hash(a),
        b: tensor_to_hash(b),
        precision: (precision || @config.default_precision).to_s,
        processor: (processor || @config.default_processor).to_s,
        priority: (priority || @config.default_priority).to_s
      }

      response = @conn.post("/compute", body)
      parse_job_result(response.body)
    end

    def conv2d(input, kernel, stride: [1, 1], padding: [0, 0], precision: nil, processor: nil)
      check_closed!

      body = {
        operation: "conv2d",
        input: tensor_to_hash(input),
        kernel: tensor_to_hash(kernel),
        stride: stride,
        padding: padding,
        precision: (precision || @config.default_precision).to_s
      }

      response = @conn.post("/compute", body)
      parse_job_result(response.body)
    end

    def attention(query, key, value, num_heads: 8, flash: true, precision: nil, processor: nil)
      check_closed!

      body = {
        operation: "attention",
        query: tensor_to_hash(query),
        key: tensor_to_hash(key),
        value: tensor_to_hash(value),
        num_heads: num_heads,
        flash: flash,
        precision: (precision || Precision::FP16).to_s
      }

      response = @conn.post("/compute", body)
      parse_job_result(response.body)
    end

    # ==================== LLM Inference ====================

    def inference(model, prompt, max_tokens: 256, temperature: 0.7, top_p: 0.9, top_k: 50, processor: nil)
      check_closed!

      body = {
        operation: "inference",
        model: model,
        prompt: prompt,
        max_tokens: max_tokens,
        temperature: temperature,
        top_p: top_p,
        top_k: top_k
      }
      body[:processor] = processor.to_s if processor

      response = @conn.post("/inference", body)
      parse_job_result(response.body)
    end

    def inference_stream(model, prompt, max_tokens: 256, temperature: 0.7, &block)
      check_closed!
      raise ArgumentError, "Block required for streaming" unless block_given?

      body = {
        operation: "inference",
        model: model,
        prompt: prompt,
        max_tokens: max_tokens,
        temperature: temperature,
        stream: true
      }

      @conn.post("/inference/stream", body) do |req|
        req.options.on_data = proc do |chunk, _|
          chunk.each_line do |line|
            next unless line.start_with?("data: ")

            data = line[6..].strip
            break if data == "[DONE]"

            begin
              json = JSON.parse(data)
              yield json["token"] if json["token"]
            rescue JSON::ParserError
              # Skip malformed JSON
            end
          end
        end
      end
    end

    # ==================== Model Registry ====================

    def list_models(category: nil)
      check_closed!

      path = category ? "/models?category=#{category}" : "/models"
      response = @conn.get(path)
      response.body["models"].map { |m| ModelInfo.from_hash(m) }
    end

    def get_model(model_id)
      check_closed!

      response = @conn.get("/models/#{model_id}")
      ModelInfo.from_hash(response.body)
    end

    def search_models(query)
      check_closed!

      response = @conn.get("/models/search", q: query)
      response.body["models"].map { |m| ModelInfo.from_hash(m) }
    end

    # ==================== Pricing & Usage ====================

    def get_pricing
      check_closed!

      response = @conn.get("/pricing")
      response.body["pricing"].map { |p| PricingInfo.from_hash(p) }
    end

    def get_usage
      check_closed!

      response = @conn.get("/usage")
      UsageStats.from_hash(response.body)
    end

    # ==================== Health Check ====================

    def health_check
      response = @conn.get("/health")
      response.body["status"] == "healthy"
    rescue StandardError
      false
    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 tensor_to_hash(tensor)
      {
        shape: tensor.shape,
        data: tensor.data,
        dtype: tensor.dtype.to_s
      }
    end

    def parse_job_result(body)
      JobResult.from_hash(body)
    end
  end
end
