# frozen_string_literal: true

require "cose/key/base"
require "openssl"

module COSE
  module Key
    class RSA < Base
      LABEL_N = -1
      LABEL_E = -2
      LABEL_D = -3
      LABEL_P = -4
      LABEL_Q = -5
      LABEL_DP = -6
      LABEL_DQ = -7
      LABEL_QINV = -8

      KTY_RSA = 3

      def self.enforce_type(map)
        if map[LABEL_KTY] != KTY_RSA
          raise "Not an RSA key"
        end
      end

      def self.from_pkey(pkey)
        params = pkey.params

        attributes = {
          n: params["n"].to_s(2),
          e: params["e"].to_s(2)
        }

        if pkey.private?
          attributes.merge!(
            d: params["d"].to_s(2),
            p: params["p"].to_s(2),
            q: params["q"].to_s(2),
            dp: params["dmp1"].to_s(2),
            dq: params["dmq1"].to_s(2),
            qinv: params["iqmp"].to_s(2)
          )
        end

        new(**attributes)
      end

      attr_reader :n, :e, :d, :p, :q, :dp, :dq, :qinv

      def initialize(n:, e:, d: nil, p: nil, q: nil, dp: nil, dq: nil, qinv: nil, **keyword_arguments) # rubocop:disable Naming/MethodParameterName
        super(**keyword_arguments)

        if !n
          raise ArgumentError, "Required public field n is missing"
        elsif !e
          raise ArgumentError, "Required public field e is missing"
        else
          private_fields = { d: d, p: p, q: q, dp: dp, dq: dq, qinv: qinv }

          if private_fields.values.all?(&:nil?) || private_fields.values.none?(&:nil?)
            @n = n
            @e = e
            @d = d
            @p = p
            @q = q
            @dp = dp
            @dq = dq
            @qinv = qinv
          else
            missing = private_fields.detect { |_k, v| v.nil? }[0]
            raise ArgumentError, "Incomplete private fields, #{missing} is missing"
          end
        end
      end

      def map
        super.merge(
          Base::LABEL_KTY => KTY_RSA,
          LABEL_N => n,
          LABEL_E => e,
          LABEL_D => d,
          LABEL_P => p,
          LABEL_Q => q,
          LABEL_DP => dp,
          LABEL_DQ => dq,
          LABEL_QINV => qinv
        ).compact
      end

      def to_pkey
        pkey = OpenSSL::PKey::RSA.new

        if pkey.respond_to?(:set_key)
          pkey.set_key(bn(n), bn(e), bn(d))
        else
          pkey.n = bn(n)
          pkey.e = bn(e)
          pkey.d = bn(d)
        end

        if private?
          if pkey.respond_to?(:set_factors)
            pkey.set_factors(bn(p), bn(q))
          else
            pkey.p = bn(p)
            pkey.q = bn(q)
          end

          if pkey.respond_to?(:set_crt_params)
            pkey.set_crt_params(bn(dp), bn(dq), bn(qinv))
          else
            pkey.dmp1 = bn(dp)
            pkey.dmq1 = bn(dq)
            pkey.iqmp = bn(qinv)
          end
        end

        pkey
      end

      def self.keyword_arguments_for_initialize(map)
        {
          n: map[LABEL_N],
          e: map[LABEL_E],
          d: map[LABEL_D],
          p: map[LABEL_P],
          q: map[LABEL_Q],
          dp: map[LABEL_DP],
          dq: map[LABEL_DQ],
          qinv: map[LABEL_QINV]
        }
      end

      private

      def private?
        [d, p, q, dp, dq, qinv].none?(&:nil?)
      end

      def bn(data)
        if data
          OpenSSL::BN.new(data, 2)
        end
      end
    end
  end
end
