# frozen_string_literal: true

# Generated by forge 1.0.0 from NovaPay Payout API 1.0.0 (spec.yaml).
# Do not edit by hand: regenerate with `bin/forge generate`; adjust decisions in overrides.yml.

require 'json'
require 'openssl'

module Provider
  class NovapayService < BaseService
    BASE_URL = ENV.fetch('NOVAPAY_BASE_URL', 'https://api.sandbox.novapay.example/v1')
    PRODUCTION_URL = 'https://api.novapay.example/v1'

    STATUS_MAP = {
      'pending'    => 'in_progress',
      'processing' => 'in_progress',
      'completed'  => 'approved',
      'failed'     => 'rejected',
      'cancelled'  => 'rejected'
    }.freeze

    EVENT_MAP = {
      'payout.completed'  => 'approved',
      'payout.failed'     => 'rejected',
      'payout.processing' => 'in_progress',
      'payout.cancelled'  => 'rejected'
    }.freeze

    ERROR_MAP = {
      400 => 'validation_error',
      401 => 'invalid_credentials',
      402 => 'insufficient_balance',
      404 => 'not_found',
      422 => 'validation_error',
      429 => 'rate_limit',
      500 => 'internal_error'
    }.freeze

    SUCCESS_STATUSES = [201, 409].freeze # 409: duplicate by Idempotency-Key returns the payout
    REQUISITE_TYPES = %w[sbp card].freeze
    STATUS_METHODS = %w[status check].freeze
    SUPPORTED_CURRENCIES = %w[RUB].freeze
    AMOUNT_MULTIPLIER = 100 # amount is sent in minor units (kopecks)
    MIN_AMOUNT = 1000 # RUB; schema minimum 100000 minor units
    SIGNATURE_HEADER = 'X-NovaPay-Signature'

    def check_conditions(operation, request_method)
      base_result = super
      return base_result if base_result.failed?
      return failure(:unprocessable_entity, 'amount_too_low') if operation.amount < MIN_AMOUNT
      return failure(:unprocessable_entity, 'currency_not_supported') unless SUPPORTED_CURRENCIES.include?(operation.currency)
      return failure(:unprocessable_entity, 'external_id_too_long') if operation.id.to_s.length > 64
      return failure(:unprocessable_entity, 'requisite_missing') unless requisite_type_for(operation, request_method)

      phone = requisite_for(operation, requisite_type_for(operation, request_method))['phone']
      return failure(:unprocessable_entity, 'phone_invalid') unless phone.to_s.match?(/\A7\d{10}\z/)

      success
    rescue StandardError => e # кривой реквизит или override-выражение → отклонить операцию, а не уронить воркер
      failure(:unprocessable_entity, 'requisite_invalid', message: e.message)
    end

    def create_request(operation, request_method = 'create')
      return fetch_status(operation) if STATUS_METHODS.include?(request_method)

      requisite_type = requisite_type_for(operation, request_method)
      return failure(:unprocessable_entity, 'requisite_missing') unless requisite_type

      payload = build_payout_payload(operation, requisite_type)
      response = client.post("#{BASE_URL}/payouts", json: payload,
                                                    headers: auth_headers.merge(idempotency_headers(operation)))
      parse_create_response(operation, response)
    rescue Provider::RateLimitError => e
      failure(:too_many_requests, 'provider.rate_limit', retry_after: e.retry_after)
    rescue Provider::UnauthorizedError
      failure(:unauthorized, 'provider.invalid_credentials')
    rescue Provider::ServerError, Provider::ConnectionError => e
      failure(:bad_gateway, 'provider.unavailable', message: e.message)
    end

    def fetch_status(operation)
      response = client.get("#{BASE_URL}/payouts/#{operation.provider_operation_key}", headers: auth_headers)
      return failure(http_symbol(response.status), "provider.#{error_code_for(response)}") unless response.status == 200

      apply_status(operation, response.body['status'], strict: true)
    rescue Provider::RateLimitError => e
      failure(:too_many_requests, 'provider.rate_limit', retry_after: e.retry_after)
    rescue Provider::UnauthorizedError
      failure(:unauthorized, 'provider.invalid_credentials')
    rescue Provider::ServerError, Provider::ConnectionError => e
      failure(:bad_gateway, 'provider.unavailable', message: e.message)
    end

    def process_callback(payload, raw_body: nil, headers: {})
      verify_signature!(raw_body || JSON.generate(payload), headers)
      internal_status = EVENT_MAP[payload['event']] || STATUS_MAP[payload['status']]
      return failure(:unprocessable_entity, 'unknown_event', event: payload['event']) unless internal_status

      case internal_status
      when 'approved' then approve_operation(payload['payout_id'])
      when 'rejected' then reject_operation(payload['payout_id'], payload.dig('error', 'code'))
      else mark_in_progress(payload['payout_id'])
      end
    rescue Provider::SignatureError
      failure(:unauthorized, 'invalid_signature')
    end

    # Endpoints outside the BaseService contract (cancel, balance)
    # are generated as optional helpers in novapay_extras.rb (NovapayExtras < NovapayService).

    private

    def auth_headers
      { 'X-API-Key' => credentials.fetch('api_key') }
    end

    def idempotency_headers(operation)
      { 'Idempotency-Key' => operation.idempotency_key }
    end

    def requisite_type_for(operation, request_method)
      return request_method if REQUISITE_TYPES.include?(request_method)

      (operation.payout_requisite.keys & REQUISITE_TYPES).first || ('card' if operation.payout_requisite.key?('card_number'))
    end

    # Реквизиты на платформе: вложенные (payout_requisite['sbp'] = {…}) или плоские (payout_requisite['card_number']).
    def requisite_for(operation, requisite_type)
      operation.payout_requisite.fetch(requisite_type) { operation.payout_requisite }
    end

    def build_payout_payload(operation, requisite_type)
      deep_compact(
        {
          amount: to_minor_units(operation.amount),
          currency: operation.currency,
          external_id: operation.id.to_s,
          recipient: build_recipient(operation, requisite_type)
        }
      )
    end

    # Убирает nil, пустые Hash/Array и объекты из одних nil (поля без источника — TODO выше).
    def deep_compact(value)
      case value
      when Hash then value.transform_values { |v| deep_compact(v) }.reject { |_k, v| v.nil? || v == {} || v == [] }
      when Array then value.map { |v| deep_compact(v) }.compact
      else value
      end
    end

    def build_recipient(operation, requisite_type)
      requisite = requisite_for(operation, requisite_type)
      base = { type: requisite_type, phone: requisite['phone'] }
      case requisite_type
      when 'sbp' then base.merge(bank_code: requisite['bank_code'], bank_name: requisite['bank_name']).compact # bank_code required for type=sbp (from description)
      when 'card' then base.merge(card_number: requisite['card_number']).compact # card_number required for type=card (from description)
      else base
      end
    end

    def parse_create_response(operation, response)
      unless SUCCESS_STATUSES.include?(response.status)
        return failure(http_symbol(response.status), "provider.#{error_code_for(response)}",
                       provider_code: response.body.dig('error', 'code'), message: response.body.dig('error', 'message'))
      end

      operations.update(operation.id, provider_operation_key: response.body['id'])
      apply_status(operation, response.body['status'])
    end

    # Успешный ответ без распознаваемого статуса (нет поля или значение вне STATUS_MAP) → in_progress:
    # операция создана, итог придёт через fetch_status / webhook. unknown_provider_status — только для fetch_status.
    def apply_status(operation, provider_status, strict: false)
      status = STATUS_MAP[provider_status.to_s]
      if status.nil?
        return failure(:unprocessable_entity, 'unknown_provider_status', provider_status: provider_status) if strict

        status = 'in_progress'
      end

      transition(operation, status, provider_status: provider_status)
    end

    def error_code_for(response)
      ERROR_MAP.fetch(response.status) { response.body.dig('error', 'code') || 'unknown_error' }
    end

    # HMAC-SHA256 over the raw request body, hex-encoded (encoding assumed: not stated in the spec).
    def verify_signature!(raw_body, headers)
      given = header_value(headers, SIGNATURE_HEADER)
      expected = OpenSSL::HMAC.hexdigest('SHA256', credentials.fetch('callback_secret'), raw_body)
      raise Provider::SignatureError, "invalid #{SIGNATURE_HEADER}" unless secure_compare(given, expected)
    end

    def to_minor_units(amount)
      (amount * AMOUNT_MULTIPLIER).round.to_i
    end
  end
end
