<?php

namespace Devikit\Sellsy\Api;

use Devikit\Sellsy\Admin\Settings;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Sellsy API v2 client (per-merchant Personal OAuth client_credentials).
 *
 * Token URL: https://login.sellsy.com/oauth2/access-tokens
 * API base:  https://api.sellsy.com/v2/
 */
class Client {

	const API_BASE  = 'https://api.sellsy.com/v2/';
	const TOKEN_URL = 'https://login.sellsy.com/oauth2/access-tokens';

	const TOKENS_OPTION = 'devikit_sellsy_oauth_tokens';

	const ERROR_AUTH       = 'auth';
	const ERROR_VALIDATION = 'validation';
	const ERROR_RATE_LIMIT = 'rate_limit';
	const ERROR_TEMPORARY  = 'temporary';
	const ERROR_UNKNOWN    = 'unknown';

	const MIN_REQUEST_INTERVAL = 0.35;

	const USER_AGENT = 'Invoicing Integration for Sellsy and WooCommerce by Devikit';

	/**
	 * @var Settings
	 */
	private $settings;

	/**
	 * @var string
	 */
	private $access_token = '';

	/**
	 * @var bool
	 */
	private $retried_auth = false;

	/**
	 * @param Settings $settings
	 */
	public function __construct( Settings $settings ) {
		$this->settings = $settings;
	}

	/**
	 * @return bool
	 */
	public function is_connected() {
		$tokens = get_option( self::TOKENS_OPTION, [] );
		return is_array( $tokens ) && ! empty( $tokens['access_token'] );
	}

	/**
	 * Clear cached API data (taxes, units, etc.).
	 */
	public static function clear_cache() {
		delete_transient( 'devikit_sellsy_taxes' );
		delete_transient( 'devikit_sellsy_units' );
		delete_transient( 'devikit_sellsy_rate_categories' );
		delete_transient( 'devikit_sellsy_languages' );
		delete_transient( 'devikit_sellsy_payment_terms' );
	}

	/**
	 * Deep-link into the Sellsy web app for a document.
	 *
	 * @param int|string $document_id
	 * @param string     $type invoice|credit_note
	 * @return string
	 */
	public static function get_app_document_url( $document_id, $type = 'invoice' ) {
		$id = absint( $document_id );
		if ( ! $id ) {
			return 'https://www.sellsy.com/';
		}

		$screen = ( $type === 'credit_note' || $type === 'credit-note' || $type === 'avoir' )
			? 'creditNoteOverview'
			: 'invoiceOverview';

		return add_query_arg(
			[
				'_f' => $screen,
				'id' => $id,
			],
			'https://www.sellsy.com/'
		);
	}

	/**
	 * Clear stored access token.
	 */
	public function disconnect() {
		delete_option( self::TOKENS_OPTION );
		$this->access_token = '';
		self::clear_cache();
	}

	/**
	 * Request a new access token via client_credentials.
	 *
	 * @param string|null $client_id
	 * @param string|null $client_secret
	 * @return true|\WP_Error
	 */
	public function request_access_token( $client_id = null, $client_secret = null ) {
		$client_id     = $client_id !== null ? (string) $client_id : (string) $this->settings->get_option( 'client_id' );
		$client_secret = $client_secret !== null ? (string) $client_secret : (string) $this->settings->get_option( 'client_secret' );

		if ( $client_id === '' || $client_secret === '' ) {
			return new \WP_Error(
				'sellsy_missing_credentials',
				__( 'Sellsy Client ID and Client Secret are required. Create a Personal API access in your Sellsy account.', 'invoicing-integration-for-sellsy-and-woocommerce' )
			);
		}

		$response = wp_remote_post(
			self::TOKEN_URL,
			[
				'timeout' => 30,
				'headers' => [
					'Content-Type' => 'application/x-www-form-urlencoded',
					'Accept'       => 'application/json',
					'User-Agent'   => self::USER_AGENT,
				],
				'body'    => [
					'grant_type'    => 'client_credentials',
					'client_id'     => $client_id,
					'client_secret' => $client_secret,
				],
			]
		);

		if ( is_wp_error( $response ) ) {
			$this->log( 'Token request error: ' . $response->get_error_message(), 'error' );
			return $response;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = json_decode( (string) wp_remote_retrieve_body( $response ), true );

		if ( $code !== 200 || empty( $body['access_token'] ) ) {
			$msg = $this->extract_oauth_error( $body, $code );
			$this->log( 'Token request failed (HTTP ' . $code . '): ' . $msg, 'error' );
			return new \WP_Error( 'sellsy_token_request', $msg );
		}

		$this->store_tokens( $body );
		return true;
	}

	/**
	 * @param array $body
	 */
	private function store_tokens( array $body ) {
		$expires_in = isset( $body['expires_in'] ) ? (int) $body['expires_in'] : 86400;
		if ( $expires_in < 60 ) {
			$expires_in = 3600;
		}

		$tokens = [
			'access_token' => (string) $body['access_token'],
			'expires_at'   => time() + $expires_in,
			'token_type'   => isset( $body['token_type'] ) ? (string) $body['token_type'] : 'Bearer',
		];

		// Personal client_credentials typically does not return a refresh_token.
		if ( ! empty( $body['refresh_token'] ) ) {
			$tokens['refresh_token'] = (string) $body['refresh_token'];
		}

		update_option( self::TOKENS_OPTION, $tokens, false );
		$this->access_token = $tokens['access_token'];
	}

	/**
	 * @return string|\WP_Error
	 */
	public function get_valid_access_token() {
		if ( $this->access_token !== '' ) {
			$tokens = get_option( self::TOKENS_OPTION, [] );
			if ( is_array( $tokens ) && ! empty( $tokens['expires_at'] ) && time() < ( (int) $tokens['expires_at'] - 60 ) ) {
				return $this->access_token;
			}
		}

		$tokens = get_option( self::TOKENS_OPTION, [] );
		if ( ! is_array( $tokens ) ) {
			$tokens = [];
		}

		$needs_new = empty( $tokens['access_token'] )
			|| empty( $tokens['expires_at'] )
			|| time() >= ( (int) $tokens['expires_at'] - 60 );

		if ( $needs_new ) {
			$result = $this->request_access_token();
			if ( is_wp_error( $result ) ) {
				return $result;
			}
			$tokens = get_option( self::TOKENS_OPTION, [] );
		}

		if ( empty( $tokens['access_token'] ) ) {
			return new \WP_Error(
				'sellsy_not_connected',
				__( 'Sellsy is not connected. Save your Personal Client ID and Client Secret, then test the connection.', 'invoicing-integration-for-sellsy-and-woocommerce' )
			);
		}

		$this->access_token = (string) $tokens['access_token'];
		return $this->access_token;
	}

	/**
	 * @param string|null $client_id
	 * @param string|null $client_secret
	 * @return true|\WP_Error
	 */
	public function test_connection( $client_id = null, $client_secret = null ) {
		if ( $client_id !== null || $client_secret !== null ) {
			$result = $this->request_access_token( $client_id, $client_secret );
			if ( is_wp_error( $result ) ) {
				return $result;
			}
		}

		$response = $this->get( 'taxes', [ 'limit' => 1 ] );
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		return true;
	}

	/**
	 * @return array|\WP_Error
	 */
	public function list_taxes() {
		$cached = get_transient( 'devikit_sellsy_taxes' );
		if ( is_array( $cached ) ) {
			return $cached;
		}

		$items = [];
		$offset = null;
		do {
			$query = [ 'limit' => 100 ];
			if ( $offset ) {
				$query['offset'] = $offset;
			}
			$response = $this->get( 'taxes', $query );
			if ( is_wp_error( $response ) ) {
				return $response;
			}
			$batch = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : [];
			$items = array_merge( $items, $batch );
			$offset = isset( $response['pagination']['offset'] ) ? $response['pagination']['offset'] : null;
			$total  = isset( $response['pagination']['total'] ) ? (int) $response['pagination']['total'] : count( $items );
		} while ( $offset && count( $items ) < $total && count( $batch ) > 0 );

		set_transient( 'devikit_sellsy_taxes', $items, 12 * HOUR_IN_SECONDS );
		return $items;
	}

	/**
	 * @return array|\WP_Error
	 */
	public function list_units() {
		$cached = get_transient( 'devikit_sellsy_units' );
		if ( is_array( $cached ) ) {
			return $cached;
		}

		$response = $this->get( 'units', [ 'limit' => 100 ] );
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$items = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : [];
		set_transient( 'devikit_sellsy_units', $items, 12 * HOUR_IN_SECONDS );
		return $items;
	}

	/**
	 * @return array|\WP_Error
	 */
	public function list_rate_categories() {
		$cached = get_transient( 'devikit_sellsy_rate_categories' );
		if ( is_array( $cached ) ) {
			return $cached;
		}

		$response = $this->get( 'rate-categories', [ 'limit' => 100 ] );
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$items = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : [];
		set_transient( 'devikit_sellsy_rate_categories', $items, 12 * HOUR_IN_SECONDS );
		return $items;
	}

	/**
	 * @return array|\WP_Error
	 */
	public function list_payment_terms() {
		$cached = get_transient( 'devikit_sellsy_payment_terms' );
		if ( is_array( $cached ) && ! empty( $cached ) ) {
			return $cached;
		}

		$response = $this->get( 'payment-terms', [ 'limit' => 100 ] );
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$items = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : [];
		if ( ! empty( $items ) ) {
			set_transient( 'devikit_sellsy_payment_terms', $items, 12 * HOUR_IN_SECONDS );
		} else {
			set_transient( 'devikit_sellsy_payment_terms', [], 5 * MINUTE_IN_SECONDS );
		}
		return $items;
	}

	/**
	 * Resolve the Sellsy "X days" payment term id for custom deadlines.
	 *
	 * @return int
	 */
	public function get_xdays_payment_term_id() {
		$terms = $this->list_payment_terms();
		if ( is_wp_error( $terms ) || ! is_array( $terms ) ) {
			return 0;
		}

		foreach ( $terms as $term ) {
			if ( ! is_array( $term ) ) {
				continue;
			}
			$code = isset( $term['code'] ) ? (string) $term['code'] : '';
			if ( $code === 'xdays' && ! empty( $term['id'] ) ) {
				return absint( $term['id'] );
			}
		}

		return 0;
	}

	/**
	 * @return array|\WP_Error
	 */
	public function list_languages() {
		$cached = get_transient( 'devikit_sellsy_languages' );
		// Do not treat an empty cached list as authoritative — languages may be
		// added in Sellsy after the first (empty) fetch.
		if ( is_array( $cached ) && ! empty( $cached ) ) {
			return $cached;
		}

		$response = $this->get( 'languages', [ 'limit' => 100 ] );
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$items = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : [];
		if ( ! empty( $items ) ) {
			set_transient( 'devikit_sellsy_languages', $items, 12 * HOUR_IN_SECONDS );
		} else {
			// Short negative cache to avoid hammering the API when none exist yet.
			set_transient( 'devikit_sellsy_languages', [], 5 * MINUTE_IN_SECONDS );
		}
		return $items;
	}

	/**
	 * @param string $email
	 * @return array|\WP_Error
	 */
	public function search_companies_by_email( $email ) {
		$email = sanitize_email( (string) $email );
		if ( $email === '' ) {
			return [];
		}

		$response = $this->post( 'companies/search?limit=20', [ 'filters' => [ 'email' => $email ] ] );
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		return isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : [];
	}

	/**
	 * @param string $email
	 * @return array|\WP_Error
	 */
	public function search_individuals_by_email( $email ) {
		$email = sanitize_email( (string) $email );
		if ( $email === '' ) {
			return [];
		}

		$response = $this->post( 'individuals/search?limit=20', [ 'filters' => [ 'email' => $email ] ] );
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		return isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : [];
	}

	/**
	 * @param array $payload
	 * @return array|\WP_Error
	 */
	public function create_company( array $payload ) {
		return $this->post( 'companies', $payload );
	}

	/**
	 * @param int   $id
	 * @param array $payload
	 * @return array|\WP_Error
	 */
	public function update_company( $id, array $payload ) {
		return $this->put( 'companies/' . absint( $id ), $payload );
	}

	/**
	 * @param array $payload
	 * @return array|\WP_Error
	 */
	public function create_individual( array $payload ) {
		return $this->post( 'individuals', $payload );
	}

	/**
	 * @param int   $id
	 * @param array $payload
	 * @return array|\WP_Error
	 */
	public function update_individual( $id, array $payload ) {
		return $this->put( 'individuals/' . absint( $id ), $payload );
	}

	/**
	 * @param string $type company|individual
	 * @param int    $id
	 * @param array  $payload
	 * @return array|\WP_Error
	 */
	public function create_address( $type, $id, array $payload ) {
		$type = $type === 'individual' ? 'individuals' : 'companies';
		return $this->post( $type . '/' . absint( $id ) . '/addresses', $payload );
	}

	/**
	 * @param array $payload
	 * @return array|\WP_Error
	 */
	public function create_invoice( array $payload ) {
		return $this->post( 'invoices', $payload );
	}

	/**
	 * @param int|string $invoice_id
	 * @return array|\WP_Error
	 */
	public function validate_invoice( $invoice_id ) {
		return $this->post( 'invoices/' . absint( $invoice_id ) . '/validate', [] );
	}

	/**
	 * @param int|string $invoice_id
	 * @return array|\WP_Error
	 */
	public function get_invoice( $invoice_id ) {
		return $this->get( 'invoices/' . absint( $invoice_id ) );
	}

	/**
	 * @param array $payload
	 * @return array|\WP_Error
	 */
	public function create_credit_note( array $payload ) {
		return $this->post( 'credit-notes', $payload );
	}

	/**
	 * @param int|string $credit_note_id
	 * @return array|\WP_Error
	 */
	public function validate_credit_note( $credit_note_id ) {
		return $this->post( 'credit-notes/' . absint( $credit_note_id ) . '/validate', [] );
	}

	/**
	 * @param int|string $credit_note_id
	 * @return array|\WP_Error
	 */
	public function get_credit_note( $credit_note_id ) {
		return $this->get( 'credit-notes/' . absint( $credit_note_id ) );
	}

	/**
	 * Link a credit note to an invoice (amount as decimal string).
	 *
	 * @param int|string $invoice_id
	 * @param int|string $credit_note_id
	 * @param string     $amount
	 * @return array|\WP_Error
	 */
	public function link_credit_note_to_invoice( $invoice_id, $credit_note_id, $amount ) {
		return $this->post(
			'invoices/' . absint( $invoice_id ) . '/credit-notes/' . absint( $credit_note_id ),
			[ 'amount' => (string) $amount ]
		);
	}

	/**
	 * Download PDF bytes for an invoice (via Sellsy pdf_link).
	 *
	 * @param int|string $invoice_id
	 * @return string|\WP_Error Binary PDF content.
	 */
	public function get_invoice_pdf( $invoice_id ) {
		$attempts = 0;
		$last     = null;

		while ( $attempts < 4 ) {
			++$attempts;
			$invoice = $this->get_invoice( $invoice_id );
			if ( is_wp_error( $invoice ) ) {
				return $invoice;
			}

			$last = $this->download_pdf_from_document( $invoice );
			if ( ! is_wp_error( $last ) ) {
				return $last;
			}

			// PDF link/file can lag briefly after validate.
			usleep( 400000 );
		}

		return $last;
	}

	/**
	 * @param int|string $credit_note_id
	 * @return string|\WP_Error
	 */
	public function get_credit_note_pdf( $credit_note_id ) {
		$attempts = 0;
		$last     = null;

		while ( $attempts < 4 ) {
			++$attempts;
			$doc = $this->get_credit_note( $credit_note_id );
			if ( is_wp_error( $doc ) ) {
				return $doc;
			}

			$last = $this->download_pdf_from_document( $doc );
			if ( ! is_wp_error( $last ) ) {
				return $last;
			}

			usleep( 400000 );
		}

		return $last;
	}

	/**
	 * @param array $document
	 * @return string|\WP_Error
	 */
	public function download_pdf_from_document( array $document ) {
		$pdf_link = isset( $document['pdf_link'] ) ? (string) $document['pdf_link'] : '';
		if ( $pdf_link === '' || ! filter_var( $pdf_link, FILTER_VALIDATE_URL ) ) {
			return new \WP_Error(
				'sellsy_pdf_missing',
				__( 'Sellsy did not return a PDF link for this document.', 'invoicing-integration-for-sellsy-and-woocommerce' )
			);
		}

		$response = wp_remote_get(
			$pdf_link,
			[
				'timeout' => 60,
				'headers' => [
					'User-Agent' => self::USER_AGENT,
					'Accept'     => 'application/pdf,*/*',
				],
			]
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = (string) wp_remote_retrieve_body( $response );

		if ( $code < 200 || $code >= 300 || $body === '' || strncmp( $body, '%PDF', 4 ) !== 0 ) {
			return new \WP_Error(
				'sellsy_pdf_download',
				__( 'Failed to download the Sellsy PDF.', 'invoicing-integration-for-sellsy-and-woocommerce' )
			);
		}

		return $body;
	}

	/**
	 * Compatibility aliases used by InvoiceManager / PRO layers.
	 *
	 * @param array $payload
	 * @return array|\WP_Error
	 */
	public function create_document( array $payload ) {
		return $this->create_invoice( $payload );
	}

	/**
	 * @param int|string $invoice_id
	 * @return array|\WP_Error
	 */
	public function complete_document( $invoice_id ) {
		return $this->validate_invoice( $invoice_id );
	}

	/**
	 * @param int|string $invoice_id
	 * @return array|\WP_Error
	 */
	public function get_document( $invoice_id ) {
		return $this->get_invoice( $invoice_id );
	}

	/**
	 * @param int|string $invoice_id
	 * @return string|\WP_Error
	 */
	public function get_document_pdf( $invoice_id ) {
		return $this->get_invoice_pdf( $invoice_id );
	}

	/**
	 * @param string               $path
	 * @param array<string,mixed>  $query
	 * @return array|\WP_Error
	 */
	public function get( $path, array $query = [] ) {
		return $this->request( 'GET', $path, null, $query );
	}

	/**
	 * @param string              $path
	 * @param array<string,mixed> $body
	 * @return array|\WP_Error
	 */
	public function post( $path, array $body = [] ) {
		return $this->request( 'POST', $path, $body );
	}

	/**
	 * @param string              $path
	 * @param array<string,mixed> $body
	 * @return array|\WP_Error
	 */
	public function put( $path, array $body = [] ) {
		return $this->request( 'PUT', $path, $body );
	}

	/**
	 * @param string                   $method
	 * @param string                   $path
	 * @param array<string,mixed>|null $body
	 * @param array<string,mixed>      $query
	 * @return array|\WP_Error
	 */
	private function request( $method, $path, $body = null, array $query = [] ) {
		$token = $this->get_valid_access_token();
		if ( is_wp_error( $token ) ) {
			return $token;
		}

		$this->throttle();

		$url = $this->build_url( $path, $query );

		$args = [
			'method'  => strtoupper( $method ),
			'timeout' => 45,
			'headers' => [
				'Authorization' => 'Bearer ' . $token,
				'Accept'        => 'application/json',
				'Content-Type'  => 'application/json',
				'User-Agent'    => self::USER_AGENT,
			],
		];

		if ( $body !== null ) {
			$args['body'] = wp_json_encode( $body );
		}

		$this->log( strtoupper( $method ) . ' ' . $path, 'debug' );

		$response = wp_remote_request( $url, $args );
		$parsed   = $this->parse_response( $response, $method, $path );

		if ( is_wp_error( $parsed ) && $parsed->get_error_code() === 'sellsy_auth_error' && ! $this->retried_auth ) {
			$this->retried_auth = true;
			$this->access_token = '';
			delete_option( self::TOKENS_OPTION );
			$refresh = $this->request_access_token();
			if ( is_wp_error( $refresh ) ) {
				return $refresh;
			}
			$result = $this->request( $method, $path, $body, $query );
			$this->retried_auth = false;
			return $result;
		}

		return $parsed;
	}

	/**
	 * @param string              $path
	 * @param array<string,mixed> $query
	 * @return string
	 */
	private function build_url( $path, array $query = [] ) {
		$path = ltrim( (string) $path, '/' );
		$url  = self::API_BASE . $path;

		if ( ! empty( $query ) ) {
			$url = add_query_arg( $query, $url );
		}

		return $url;
	}

	/**
	 * @param array|\WP_Error $response
	 * @param string          $method
	 * @param string          $path
	 * @return array|\WP_Error
	 */
	private function parse_response( $response, $method, $path ) {
		if ( is_wp_error( $response ) ) {
			$this->log( 'HTTP error on ' . $method . ' ' . $path . ': ' . $response->get_error_message(), 'error' );
			return $response;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$raw  = (string) wp_remote_retrieve_body( $response );

		if ( $code === 204 || ( $code >= 200 && $code < 300 && $raw === '' ) ) {
			return [];
		}

		$data = json_decode( $raw, true );

		if ( $code >= 400 ) {
			$message = $this->extract_api_error( $data, $code );
			$this->log( 'API error ' . $code . ' on ' . $method . ' ' . $path . ': ' . $message, 'error' );

			if ( $code === 401 ) {
				return new \WP_Error( 'sellsy_auth_error', $message, [ 'type' => self::ERROR_AUTH, 'status' => $code ] );
			}
			if ( $code === 403 ) {
				return new \WP_Error(
					'sellsy_forbidden',
					__( 'Sellsy refused this request. Check that your Personal API access includes the required scopes (companies, individuals, invoices, credit notes).', 'invoicing-integration-for-sellsy-and-woocommerce' ) . ' ' . $message,
					[ 'type' => self::ERROR_AUTH, 'status' => $code ]
				);
			}
			if ( $code === 429 ) {
				return new \WP_Error( 'sellsy_rate_limit', $message, [ 'type' => self::ERROR_RATE_LIMIT, 'status' => $code ] );
			}
			if ( $code >= 500 ) {
				return new \WP_Error( 'sellsy_server_error', $message, [ 'type' => self::ERROR_TEMPORARY, 'status' => $code ] );
			}

			return new \WP_Error( 'sellsy_api_error', $message, [ 'type' => self::ERROR_VALIDATION, 'status' => $code, 'response' => $data ] );
		}

		return is_array( $data ) ? $data : [];
	}

	/**
	 * @param mixed $body
	 * @param int   $code
	 * @return string
	 */
	private function extract_oauth_error( $body, $code ) {
		if ( is_array( $body ) ) {
			if ( ! empty( $body['error_description'] ) ) {
				return (string) $body['error_description'];
			}
			if ( ! empty( $body['error'] ) && is_string( $body['error'] ) ) {
				return (string) $body['error'];
			}
			if ( ! empty( $body['message'] ) ) {
				return (string) $body['message'];
			}
		}

		if ( $code === 401 || $code === 400 ) {
			return __( 'Invalid Sellsy Client ID or Client Secret. Create a Personal API access in your Sellsy developer portal and try again.', 'invoicing-integration-for-sellsy-and-woocommerce' );
		}

		return sprintf(
			/* translators: %d: HTTP status code */
			__( 'Sellsy token request failed (HTTP %d).', 'invoicing-integration-for-sellsy-and-woocommerce' ),
			(int) $code
		);
	}

	/**
	 * @param mixed $data
	 * @param int   $code
	 * @return string
	 */
	private function extract_api_error( $data, $code ) {
		if ( is_array( $data ) ) {
			if ( isset( $data['error']['message'] ) ) {
				$msg = (string) $data['error']['message'];
				if ( ! empty( $data['error']['details'] ) && is_array( $data['error']['details'] ) ) {
					$bits = [];
					foreach ( $data['error']['details'] as $field => $detail ) {
						$bits[] = $field . ': ' . ( is_scalar( $detail ) ? (string) $detail : wp_json_encode( $detail ) );
					}
					if ( $bits ) {
						$msg .= ' (' . implode( '; ', $bits ) . ')';
					}
				}
				return $msg;
			}
			if ( ! empty( $data['message'] ) ) {
				return (string) $data['message'];
			}
		}

		return sprintf(
			/* translators: %d: HTTP status code */
			__( 'Sellsy API error (HTTP %d).', 'invoicing-integration-for-sellsy-and-woocommerce' ),
			(int) $code
		);
	}

	private function throttle() {
		$last = (float) get_transient( 'devikit_sellsy_last_request' );
		$now  = microtime( true );
		if ( $last > 0 ) {
			$elapsed = $now - $last;
			if ( $elapsed < self::MIN_REQUEST_INTERVAL ) {
				usleep( (int) ( ( self::MIN_REQUEST_INTERVAL - $elapsed ) * 1000000 ) );
			}
		}
		set_transient( 'devikit_sellsy_last_request', (string) microtime( true ), MINUTE_IN_SECONDS );
	}

	/**
	 * @param string $message
	 * @param string $level
	 */
	public function log( $message, $level = 'info' ) {
		if ( ! function_exists( 'wc_get_logger' ) ) {
			return;
		}

		$message = $this->redact_secrets( (string) $message );
		$logger  = wc_get_logger();
		$debug   = $this->settings->get_option( 'enable_debug_logging' ) === 'yes';

		if ( $level === 'debug' && ! $debug ) {
			return;
		}

		$context = [ 'source' => $debug ? 'devikit_sellsy_debug' : 'invoicing-integration-for-sellsy-and-woocommerce' ];

		if ( $level === 'error' ) {
			$logger->error( $message, $context );
		} elseif ( $level === 'warning' ) {
			$logger->warning( $message, $context );
		} elseif ( $level === 'debug' ) {
			$logger->debug( $message, $context );
		} else {
			$logger->info( $message, $context );
		}
	}

	/**
	 * Soften technical API messages for order notes / admin UI.
	 *
	 * @param string $message
	 * @return string
	 */
	public static function humanize_business_error( $message ) {
		$message = (string) $message;
		if ( stripos( $message, 'scope' ) !== false || stripos( $message, 'Forbidden' ) !== false ) {
			return __( 'Sellsy rejected the request. Verify Personal API scopes for companies/individuals/invoices.', 'invoicing-integration-for-sellsy-and-woocommerce' ) . ' ' . $message;
		}
		return $message;
	}

	/**
	 * @param string $message
	 * @return string
	 */
	private function redact_secrets( $message ) {
		$tokens = get_option( self::TOKENS_OPTION, [] );
		if ( is_array( $tokens ) ) {
			foreach ( [ 'access_token', 'refresh_token' ] as $key ) {
				if ( ! empty( $tokens[ $key ] ) && is_string( $tokens[ $key ] ) ) {
					$message = str_replace( $tokens[ $key ], '[REDACTED]', $message );
				}
			}
		}

		$secret = (string) $this->settings->get_option( 'client_secret' );
		if ( $secret !== '' ) {
			$message = str_replace( $secret, '[REDACTED]', $message );
		}

		return $message;
	}
}
