<?php
/**
 * sevDesk API Client
 *
 * Handles all communication with the sevDesk REST API.
 *
 * @package KursBooking
 * @since   1.0.0
 */

declare(strict_types=1);

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

/**
 * Class Kurs_Booking_SevDesk
 *
 * Provides methods for interacting with the sevDesk API including
 * connection testing, contact management, and invoice creation.
 */
class Kurs_Booking_SevDesk {

	/**
	 * sevDesk API base URL.
	 * Note: sevDesk uses X-Version header for resource versioning, not separate URLs.
	 */
	private const API_BASE_URL = 'https://my.sevdesk.de/api/v1';

	/**
	 * Available API resource versions.
	 * sevDesk uses X-Version header to select resource versions per endpoint.
	 */
	private const API_VERSIONS = array( '1.0', '2.0' );

	/**
	 * Default API resource version.
	 */
	private const DEFAULT_API_VERSION = '2.0';

	/**
	 * Endpoints that do NOT support X-Version header.
	 * These Factory endpoints reject any explicit version and require no header.
	 *
	 * @see https://tech.sevdesk.com/api_news/
	 */
	private const NO_VERSION_ENDPOINTS = array(
		'/Invoice/Factory/saveInvoice',
		'/Invoice/Factory/getNextInvoiceNumber',
		'/CreditNote/Factory/saveCreditNote',
	);

	/**
	 * API request timeout in seconds
	 */
	private const TIMEOUT = 30;

	/**
	 * Unity IDs for invoice positions (Einheiten)
	 *
	 * @see https://api.sevdesk.de/#tag/Unity
	 */
	public const UNITY_PIECE = 1;   // Stück.
	public const UNITY_HOUR  = 9;   // Stunden.
	public const UNITY_DAY   = 12;  // Tag(e).

	/**
	 * Contact category IDs
	 *
	 * @see https://api.sevdesk.de/#tag/Category
	 */
	public const CATEGORY_CUSTOMER = 3;  // Kunde.

	/**
	 * Contact address category IDs
	 *
	 * @see https://api.sevdesk.de/#tag/ContactAddress
	 */
	public const ADDRESS_INVOICE = 47;  // Rechnungsadresse.

	/**
	 * Invoice status codes
	 *
	 * @see https://api.sevdesk.de/#tag/Invoice
	 */
	public const INVOICE_STATUS_DRAFT    = 100;  // Entwurf.
	public const INVOICE_STATUS_OPEN     = 200;  // Offen.
	public const INVOICE_STATUS_PAYED    = 1000; // Bezahlt.
	public const INVOICE_STATUS_CANCELED = 750;  // Storniert.

	/**
	 * Get configured API resource version for X-Version header.
	 *
	 * @param string $endpoint Optional endpoint to check for no-version restriction.
	 * @return string|null API version (1.0 or 2.0), or null if endpoint doesn't support versioning.
	 */
	public static function get_api_version( string $endpoint = '' ): ?string {
		// Check if endpoint does NOT support X-Version header at all.
		if ( $endpoint ) {
			foreach ( self::NO_VERSION_ENDPOINTS as $no_version_endpoint ) {
				if ( str_contains( $endpoint, $no_version_endpoint ) ) {
					return null; // No X-Version header for Factory endpoints.
				}
			}
		}

		$version = get_option( 'kurs_booking_sevdesk_api_version', self::DEFAULT_API_VERSION );
		return in_array( $version, self::API_VERSIONS, true ) ? $version : self::DEFAULT_API_VERSION;
	}

	/**
	 * Make API request to sevDesk.
	 *
	 * @param string               $endpoint API endpoint (without base URL).
	 * @param string               $method   HTTP method (GET, POST, PUT, DELETE).
	 * @param array<string, mixed> $data     Request data.
	 * @return array<string, mixed>|WP_Error Response data or error.
	 */
	private static function api_request(
		string $endpoint,
		string $method = 'GET',
		array $data = array()
	): array|WP_Error {
		$settings = Kurs_Booking_Settings::get_sevdesk_settings();

		if ( empty( $settings['api_token'] ) ) {
			return new WP_Error(
				'no_api_token',
				__( 'sevDesk API-Token nicht konfiguriert.', 'kurs-booking' )
			);
		}

		$url = self::API_BASE_URL . $endpoint;

		// Get API version - some Factory endpoints don't support X-Version header.
		$api_version = self::get_api_version( $endpoint );

		$headers = array(
			'Authorization' => $settings['api_token'],
			'Content-Type'  => 'application/json',
			'Accept'        => 'application/json',
		);

		// Only add X-Version header if endpoint supports it.
		if ( null !== $api_version ) {
			$headers['X-Version'] = $api_version;
		}

		$args = array(
			'method'  => $method,
			'timeout' => self::TIMEOUT,
			'headers' => $headers,
		);

		// Add body for POST/PUT requests.
		if ( ! empty( $data ) && in_array( $method, array( 'POST', 'PUT' ), true ) ) {
			$args['body'] = wp_json_encode( $data );
		}

		// Add query parameters for GET requests.
		if ( ! empty( $data ) && 'GET' === $method ) {
			$url = add_query_arg( $data, $url );
		}

		$response = wp_remote_request( $url, $args );

		// Handle connection errors.
		if ( is_wp_error( $response ) ) {
			self::log( 'API request failed', $endpoint, $response->get_error_message() );
			return $response;
		}

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

		// Version info for logging.
		$version_info = $api_version ? "[v{$api_version}]" : '[no-version]';

		// Handle HTTP errors.
		if ( $code >= 400 ) {
			$error_msg = $data['error']['message'] ?? __( 'Unbekannter API-Fehler', 'kurs-booking' );
			self::log( 'API error', $endpoint, $error_msg . " {$version_info}", $code );
			return new WP_Error(
				'api_error',
				$error_msg,
				array( 'status' => $code )
			);
		}

		self::log( 'API success', $endpoint, "OK {$version_info}", $code );

		return $data ?? array();
	}

	/**
	 * Test API connection.
	 *
	 * Retrieves the sevDesk user to verify the API token is valid.
	 *
	 * @return array<string, mixed>|WP_Error User info on success, error on failure.
	 */
	public static function test_connection(): array|WP_Error {
		$settings = Kurs_Booking_Settings::get_sevdesk_settings();

		if ( empty( $settings['api_token'] ) ) {
			return new WP_Error(
				'no_api_token',
				__( 'Bitte geben Sie einen API-Token ein.', 'kurs-booking' )
			);
		}

		$response = self::api_request( '/SevUser' );

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

		if ( empty( $response['objects'] ) ) {
			return new WP_Error(
				'no_user',
				__( 'Kein Benutzer gefunden.', 'kurs-booking' )
			);
		}

		$user = $response['objects'][0];

		return array(
			'success'  => true,
			'user_id'  => $user['id'] ?? 0,
			'username' => $user['username'] ?? '',
			'email'    => $user['email'] ?? '',
			'fullname' => trim( ( $user['firstName'] ?? '' ) . ' ' . ( $user['lastName'] ?? '' ) ),
		);
	}

	/**
	 * Get sevDesk user ID.
	 *
	 * Uses configured user_id if available, otherwise fetches from API.
	 *
	 * @return int|WP_Error User ID or error.
	 */
	public static function get_sev_user_id(): int|WP_Error {
		$settings = Kurs_Booking_Settings::get_sevdesk_settings();

		// Use configured user ID if available.
		if ( ! empty( $settings['user_id'] ) ) {
			return (int) $settings['user_id'];
		}

		// Fetch from API.
		$response = self::api_request( '/SevUser' );

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

		if ( empty( $response['objects'][0]['id'] ) ) {
			return new WP_Error(
				'no_user',
				__( 'sevDesk Benutzer-ID nicht gefunden.', 'kurs-booking' )
			);
		}

		return (int) $response['objects'][0]['id'];
	}

	/**
	 * Check if sevDesk integration is enabled and configured.
	 *
	 * @return bool True if enabled and API token is set.
	 */
	public static function is_enabled(): bool {
		$settings = Kurs_Booking_Settings::get_sevdesk_settings();

		return ! empty( $settings['api_token'] ) && ! empty( $settings['auto_invoice'] );
	}

	/**
	 * Check if API token is configured.
	 *
	 * @return bool True if API token is set.
	 */
	public static function is_configured(): bool {
		$settings = Kurs_Booking_Settings::get_sevdesk_settings();

		return ! empty( $settings['api_token'] );
	}

	/**
	 * Log sevDesk operations.
	 *
	 * Only logs when WP_DEBUG is enabled.
	 *
	 * @param string $action   Action performed.
	 * @param string $endpoint API endpoint.
	 * @param string $message  Log message.
	 * @param int    $code     HTTP status code (optional).
	 */
	private static function log(
		string $action,
		string $endpoint,
		string $message,
		int $code = 0
	): void {
		if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
			return;
		}

		$log_entry = sprintf(
			'[Kurs Booking sevDesk] %s | Endpoint: %s | %s%s',
			$action,
			$endpoint,
			$message,
			$code ? " | HTTP {$code}" : ''
		);

		error_log( $log_entry );
	}

	/**
	 * Make API request with retry on rate limit.
	 *
	 * Implements exponential backoff for 429 (Too Many Requests) errors.
	 *
	 * @param string               $endpoint API endpoint (without base URL).
	 * @param string               $method   HTTP method (GET, POST, PUT, DELETE).
	 * @param array<string, mixed> $data     Request data.
	 * @return array<string, mixed>|WP_Error Response data or error.
	 */
	public static function api_request_with_retry(
		string $endpoint,
		string $method = 'GET',
		array $data = array()
	): array|WP_Error {
		$max_retries = 3;
		$retry_delay = 1; // Initial delay in seconds.

		$response = null;

		for ( $i = 0; $i < $max_retries; $i++ ) {
			$response = self::api_request( $endpoint, $method, $data );

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

			$error_data = $response->get_error_data();

			// Only retry on rate limit (429).
			if ( isset( $error_data['status'] ) && 429 === $error_data['status'] ) {
				self::log( 'Rate limit hit', $endpoint, "Retry {$i} in " . ( $retry_delay * ( $i + 1 ) ) . 's' );
				sleep( $retry_delay * ( $i + 1 ) ); // Exponential backoff.
				continue;
			}

			// For other errors, return immediately.
			return $response;
		}

		return $response;
	}

	// =========================================================================
	// Contact Management (Sprint 3.2)
	// =========================================================================

	/**
	 * Find contact by email address.
	 *
	 * @param string $email Customer email.
	 * @return int|null Contact ID or null if not found.
	 */
	public static function find_contact_by_email( string $email ): ?int {
		if ( ! is_email( $email ) ) {
			return null;
		}

		// Suche über CommunicationWay API (nested Filter auf Contact funktionieren nicht!).
		// Nur nach value filtern - type-Filter funktioniert bei sevDesk nicht.
		$response = self::api_request(
			'/CommunicationWay',
			'GET',
			array(
				'value' => $email,
			)
		);

		if ( is_wp_error( $response ) ) {
			self::log( 'Contact search failed', '/CommunicationWay', $response->get_error_message() );
			return null;
		}

		// CommunicationWay gefunden? Dann Contact-ID extrahieren.
		if ( ! empty( $response['objects'][0]['contact']['id'] ) ) {
			$contact_id = (int) $response['objects'][0]['contact']['id'];
			self::log( 'Contact found by email', '/CommunicationWay', "Email: $email → Contact ID: $contact_id" );
			return $contact_id;
		}

		return null;
	}

	/**
	 * Create a new contact in sevDesk.
	 *
	 * @param array<string, string> $data Contact data with keys:
	 *                                    - name (required)
	 *                                    - email (required)
	 *                                    - phone (optional)
	 *                                    - street (optional)
	 *                                    - zip (optional)
	 *                                    - city (optional)
	 * @return int|WP_Error Contact ID on success, error on failure.
	 */
	public static function create_contact( array $data ): int|WP_Error {
		// Validate required fields.
		if ( empty( $data['name'] ) || empty( $data['email'] ) ) {
			return new WP_Error(
				'missing_fields',
				__( 'Name und E-Mail sind erforderlich.', 'kurs-booking' )
			);
		}

		$settings    = Kurs_Booking_Settings::get_sevdesk_settings();
		$category_id = ! empty( $settings['category_id'] )
			? (int) $settings['category_id']
			: self::CATEGORY_CUSTOMER;

		// Parse name into first/last name.
		$name_parts = self::parse_name( $data['name'] );

		// Create contact.
		$contact_data = array(
			'category'       => array(
				'id'         => $category_id,
				'objectName' => 'Category',
			),
			'surename'       => $name_parts['first'],  // Note: sevDesk typo "surename".
			'familyname'     => $name_parts['last'],
			'customerNumber' => null, // Auto-generate.
		);

		// Add gender/anrede if provided (sevDesk: 'm' = Herr, 'f' = Frau).
		if ( ! empty( $data['gender'] ) ) {
			$contact_data['gender'] = $data['gender'];
		}

		$response = self::api_request( '/Contact', 'POST', $contact_data );

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

		if ( empty( $response['objects']['id'] ) ) {
			return new WP_Error(
				'contact_creation_failed',
				__( 'Kontakt konnte nicht erstellt werden.', 'kurs-booking' )
			);
		}

		$contact_id = (int) $response['objects']['id'];

		// Add email as communication way.
		$email_result = self::add_communication_way( $contact_id, 'email', $data['email'] );

		if ( is_wp_error( $email_result ) ) {
			self::log( 'Email CW failed', '/CommunicationWay', $email_result->get_error_message() );
		}

		// Add phone if provided.
		if ( ! empty( $data['phone'] ) ) {
			$phone_result = self::add_communication_way( $contact_id, 'phone', $data['phone'] );

			if ( is_wp_error( $phone_result ) ) {
				self::log( 'Phone CW failed', '/CommunicationWay', $phone_result->get_error_message() );
			}
		}

		// Add address if provided.
		if ( ! empty( $data['street'] ) || ! empty( $data['city'] ) ) {
			$address_result = self::create_contact_address( $contact_id, $data );

			if ( is_wp_error( $address_result ) ) {
				self::log( 'Address failed', '/ContactAddress', $address_result->get_error_message() );
			}
		}

		self::log( 'Contact created', '/Contact', "ID: {$contact_id}" );

		return $contact_id;
	}

	/**
	 * Add communication way to contact.
	 *
	 * @param int    $contact_id Contact ID.
	 * @param string $type       Type: 'email' or 'phone'.
	 * @param string $value      The value (email address or phone number).
	 * @return int|WP_Error Communication way ID or error.
	 */
	private static function add_communication_way(
		int $contact_id,
		string $type,
		string $value
	): int|WP_Error {
		$type_ids = array(
			'email'  => 2,
			'phone'  => 1,
			'mobile' => 3,
			'fax'    => 4,
			'web'    => 7,
		);

		if ( ! isset( $type_ids[ $type ] ) ) {
			return new WP_Error(
				'invalid_type',
				__( 'Ungültiger Kommunikationstyp.', 'kurs-booking' )
			);
		}

		$data = array(
			'contact' => array(
				'id'         => $contact_id,
				'objectName' => 'Contact',
			),
			'type'    => array(
				'id'         => $type_ids[ $type ],
				'objectName' => 'CommunicationWayType',
			),
			'value'   => sanitize_text_field( $value ),
			'key'     => array(
				'id'         => 2, // 2 = Geschäftlich.
				'objectName' => 'CommunicationWayKey',
			),
			'main'    => true,
		);

		$response = self::api_request( '/CommunicationWay', 'POST', $data );

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

		if ( empty( $response['objects']['id'] ) ) {
			return new WP_Error(
				'cw_creation_failed',
				__( 'Kommunikationsweg konnte nicht erstellt werden.', 'kurs-booking' )
			);
		}

		return (int) $response['objects']['id'];
	}

	/**
	 * Create contact address (invoice address).
	 *
	 * @param int                   $contact_id Contact ID.
	 * @param array<string, string> $data       Address data with keys:
	 *                                         - street (required)
	 *                                         - zip (required)
	 *                                         - city (required)
	 *                                         - country (optional, default: 'DE')
	 * @return int|WP_Error Address ID on success, error on failure.
	 */
	public static function create_contact_address(
		int $contact_id,
		array $data
	): int|WP_Error {
		$address_data = array(
			'contact'  => array(
				'id'         => $contact_id,
				'objectName' => 'Contact',
			),
			'category' => array(
				'id'         => self::ADDRESS_INVOICE, // 47 = Rechnungsadresse.
				'objectName' => 'Category',
			),
			'street'   => sanitize_text_field( $data['street'] ?? '' ),
			'zip'      => sanitize_text_field( $data['zip'] ?? '' ),
			'city'     => sanitize_text_field( $data['city'] ?? '' ),
			'country'  => array(
				'id'         => self::get_country_id( $data['country'] ?? 'DE' ),
				'objectName' => 'StaticCountry',
			),
		);

		$response = self::api_request( '/ContactAddress', 'POST', $address_data );

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

		if ( empty( $response['objects']['id'] ) ) {
			return new WP_Error(
				'address_creation_failed',
				__( 'Adresse konnte nicht erstellt werden.', 'kurs-booking' )
			);
		}

		return (int) $response['objects']['id'];
	}

	/**
	 * Parse full name into first and last name.
	 *
	 * @param string $full_name Full name.
	 * @return array<string, string> Array with 'first' and 'last' keys.
	 */
	private static function parse_name( string $full_name ): array {
		$parts = explode( ' ', trim( $full_name ), 2 );

		return array(
			'first' => $parts[0] ?? '',
			'last'  => $parts[1] ?? '',
		);
	}

	/**
	 * Get sevDesk country ID from ISO code.
	 *
	 * @param string $iso_code ISO country code (e.g., 'DE', 'AT', 'CH').
	 * @return int Country ID.
	 */
	private static function get_country_id( string $iso_code ): int {
		$countries = array(
			'DE' => 1,   // Deutschland.
			'AT' => 2,   // Österreich.
			'CH' => 3,   // Schweiz.
			'GB' => 4,   // Großbritannien.
			'US' => 5,   // USA.
			'FR' => 6,   // Frankreich.
			'IT' => 7,   // Italien.
			'ES' => 8,   // Spanien.
			'NL' => 9,   // Niederlande.
			'BE' => 10,  // Belgien.
		);

		return $countries[ strtoupper( $iso_code ) ] ?? 1; // Default: Deutschland.
	}

	/**
	 * Map gender/anrede value to sevDesk format.
	 *
	 * sevDesk uses: 'm' = male (Herr), 'f' = female (Frau)
	 *
	 * @param string $value Input value (e.g., 'Herr', 'Frau', 'm', 'f', 'männlich', etc.).
	 * @return string sevDesk gender code ('m', 'f', or empty).
	 */
	private static function map_gender_to_sevdesk( string $value ): string {
		$value = strtolower( trim( $value ) );

		// Direct codes.
		if ( in_array( $value, array( 'm', 'male', 'männlich', 'maennlich' ), true ) ) {
			return 'm';
		}

		if ( in_array( $value, array( 'f', 'w', 'female', 'weiblich' ), true ) ) {
			return 'f';
		}

		// German salutations.
		if ( in_array( $value, array( 'herr', 'hr', 'hr.' ), true ) ) {
			return 'm';
		}

		if ( in_array( $value, array( 'frau', 'fr', 'fr.' ), true ) ) {
			return 'f';
		}

		// Divers/other - sevDesk doesn't have a third option, return empty.
		return '';
	}

	/**
	 * Extract address data from booking.
	 *
	 * Checks custom_fields for address data with various field name conventions.
	 *
	 * @param int $buchung_id Booking post ID.
	 * @return array{name: string, street: string, zip: string, city: string, country: string}
	 */
	public static function get_booking_address( int $buchung_id ): array {
		$name = Kurs_Booking_Buchung::get_meta( $buchung_id, 'name', '' );

		// Get custom fields for address data.
		$custom_fields_json = Kurs_Booking_Buchung::get_meta( $buchung_id, 'custom_fields', '{}' );
		$custom_fields      = json_decode( $custom_fields_json, true ) ?: array();

		// Extract address with multiple field name conventions.
		$street  = $custom_fields['adresse'] ?? $custom_fields['strasse'] ?? $custom_fields['street'] ?? '';
		$zip     = $custom_fields['plz'] ?? $custom_fields['zip'] ?? $custom_fields['postleitzahl'] ?? '';
		$city    = $custom_fields['ort'] ?? $custom_fields['stadt'] ?? $custom_fields['city'] ?? '';
		$country = $custom_fields['land'] ?? $custom_fields['country'] ?? 'DE';

		return array(
			'name'    => $name,
			'street'  => $street,
			'zip'     => $zip,
			'city'    => $city,
			'country' => $country,
		);
	}

	/**
	 * Find or create contact from booking data.
	 *
	 * This is the main entry point for booking → contact.
	 *
	 * @param int $buchung_id Booking post ID.
	 * @return int|WP_Error Contact ID or error.
	 */
	public static function get_or_create_contact_for_booking( int $buchung_id ): int|WP_Error {
		$email = Kurs_Booking_Buchung::get_meta( $buchung_id, 'email', '' );
		$name  = Kurs_Booking_Buchung::get_meta( $buchung_id, 'name', '' );
		$phone = Kurs_Booking_Buchung::get_meta( $buchung_id, 'phone', '' );

		// Get custom fields for additional data (anrede, address).
		$custom_fields_json = Kurs_Booking_Buchung::get_meta( $buchung_id, 'custom_fields', '{}' );
		$custom_fields      = json_decode( $custom_fields_json, true ) ?: array();

		if ( empty( $email ) ) {
			return new WP_Error(
				'no_email',
				__( 'Buchung hat keine E-Mail-Adresse.', 'kurs-booking' )
			);
		}

		// Check if we already have a sevDesk contact for this booking.
		$existing_contact_id = get_post_meta( $buchung_id, '_sevdesk_contact_id', true );

		if ( $existing_contact_id ) {
			return (int) $existing_contact_id;
		}

		// Search for existing contact by email.
		$contact_id = self::find_contact_by_email( $email );

		if ( $contact_id ) {
			// Store for future reference.
			update_post_meta( $buchung_id, '_sevdesk_contact_id', $contact_id );
			return $contact_id;
		}

		// Extract anrede/gender from custom fields.
		// Supported field names: anrede, salutation, gender.
		$gender = '';
		foreach ( array( 'anrede', 'salutation', 'gender' ) as $field_name ) {
			if ( ! empty( $custom_fields[ $field_name ] ) ) {
				$gender = self::map_gender_to_sevdesk( $custom_fields[ $field_name ] );
				break;
			}
		}

		// Extract address from custom fields.
		$street = $custom_fields['adresse'] ?? $custom_fields['strasse'] ?? $custom_fields['street'] ?? '';
		$zip    = $custom_fields['plz'] ?? $custom_fields['zip'] ?? '';
		$city   = $custom_fields['ort'] ?? $custom_fields['stadt'] ?? $custom_fields['city'] ?? '';

		// Create new contact.
		$contact_id = self::create_contact(
			array(
				'name'   => $name,
				'email'  => $email,
				'phone'  => $phone,
				'gender' => $gender,
				'street' => $street,
				'zip'    => $zip,
				'city'   => $city,
			)
		);

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

		// Store for future reference.
		update_post_meta( $buchung_id, '_sevdesk_contact_id', $contact_id );

		return $contact_id;
	}

	// =========================================================================
	// Invoice Management (Sprint 3.3)
	// =========================================================================

	/**
	 * Get next invoice number from sevDesk.
	 *
	 * @param string $type Invoice type (RE=Rechnung, AN=Angebot, AB=Auftragsbestätigung).
	 * @return string|WP_Error Invoice number or error.
	 */
	public static function get_next_invoice_number( string $type = 'RE' ): string|WP_Error {
		$response = self::api_request(
			'/Invoice/Factory/getNextInvoiceNumber',
			'GET',
			array(
				'invoiceType'   => $type,
				'useNextNumber' => 'true',
			)
		);

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

		if ( empty( $response['objects'] ) ) {
			return new WP_Error(
				'no_invoice_number',
				__( 'Rechnungsnummer konnte nicht abgerufen werden.', 'kurs-booking' )
			);
		}

		return (string) $response['objects'];
	}

	/**
	 * Create invoice for booking.
	 *
	 * @param int $buchung_id Booking post ID.
	 * @return array<string, mixed>|WP_Error Invoice data or error.
	 */
	public static function create_invoice_for_booking( int $buchung_id ): array|WP_Error {
		// Check if invoice already exists.
		$existing_invoice_id = get_post_meta( $buchung_id, '_sevdesk_invoice_id', true );

		if ( $existing_invoice_id ) {
			return new WP_Error(
				'invoice_exists',
				__( 'Für diese Buchung existiert bereits eine Rechnung.', 'kurs-booking' ),
				array( 'invoice_id' => $existing_invoice_id )
			);
		}

		// Get or create contact.
		$contact_id = self::get_or_create_contact_for_booking( $buchung_id );

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

		// Get sevDesk user ID.
		$sev_user_id = self::get_sev_user_id();

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

		// Build invoice positions from booking.
		$positions = self::build_invoice_positions( $buchung_id );

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

		// Get settings.
		$settings           = Kurs_Booking_Settings::get_sevdesk_settings();
		$price_settings     = Kurs_Booking_Settings::get_price_settings();
		$is_small_business  = ! empty( $price_settings['small_business'] );
		$prices_include_vat = ( $price_settings['price_display'] ?? 'gross' ) === 'gross';

		// Get booking data for header.
		$kurs_id    = (int) Kurs_Booking_Buchung::get_meta( $buchung_id, 'kurs_id', 0 );
		$kurs_title = get_the_title( $kurs_id );
		$kurs_date  = Kurs_Booking_Kurs::get_meta( $kurs_id, 'start_date', '' );

		// E-Rechnung (ZUGFeRD) aktivieren wenn in Settings aktiviert.
		$is_e_invoice = ! empty( $settings['e_invoice'] );

		// Get configurable values from settings.
		$time_to_pay = (int) get_option( 'kurs_booking_sevdesk_time_to_pay', 14 );
		$currency    = get_option( 'kurs_booking_sevdesk_currency', 'EUR' );

		// Get address data from booking for invoice.
		$address = self::get_booking_address( $buchung_id );

		// Check if we have explicit address data.
		$has_address = ! empty( $address['street'] ) && ! empty( $address['city'] );

		// Build customer reference (Kundenreferenz / "Ihre Referenz").
		$customer_reference = self::build_customer_reference( $buchung_id, $kurs_title );

		// Build invoice data.
		// Hinweis: sevDesk API - mapAll: true am ENDE mappt fehlende Felder vom Contact,
		// überschreibt aber KEINE explizit gesetzten Adressfelder (wenn sie VOR mapAll stehen).
		$invoice_array = array(
			'objectName'           => 'Invoice',
			'contact'              => array(
				'id'         => $contact_id,
				'objectName' => 'Contact',
			),
			'contactPerson'        => array(
				'id'         => $sev_user_id,
				'objectName' => 'SevUser',
			),
			'invoiceDate'          => gmdate( 'Y-m-d' ),
			'header'               => sprintf(
				/* translators: %s: Course title */
				__( 'Rechnung für: %s', 'kurs-booking' ),
				$kurs_title
			),
			'headText'             => self::build_head_text( $buchung_id, $kurs_title, $kurs_date ),
			'footText'             => self::build_foot_text( $is_small_business ),
			'timeToPay'            => $time_to_pay,
			'discount'             => 0,
			'deliveryDate'         => $kurs_date ?: gmdate( 'Y-m-d' ),
			'status'               => $settings['invoice_status'] ?? self::INVOICE_STATUS_DRAFT,
			'taxType'              => $is_small_business ? 'noteu' : 'default',
			'taxRate'              => $is_small_business ? 0 : ( $price_settings['vat_rate'] ?? 19 ),
			'invoiceType'          => 'RE',
			'currency'             => $currency,
			'showNet'              => ! $prices_include_vat,
			'sendType'             => 'VM', // VM = Versandart Mail.
			'propertyIsEInvoice'   => $is_e_invoice, // E-Rechnung (ZUGFeRD 2.1/XRechnung).
			'customerInternalNote' => $customer_reference, // Kundenreferenz "Ihre Referenz".
		);

		// Adressfelder VOR mapAll setzen (werden dann nicht überschrieben).
		if ( $has_address ) {
			$invoice_array['addressName']   = $address['name'];
			$invoice_array['addressStreet'] = $address['street'];
			$invoice_array['addressZip']    = $address['zip'];
			$invoice_array['addressCity']   = $address['city'];
		}

		// mapAll: true am ENDE - mappt nur fehlende Felder vom Contact.
		$invoice_array['mapAll'] = true;

		$invoice_data = array(
			'invoice'        => $invoice_array,
			'invoicePosSave' => $positions,
		);

		// Make API request.
		$response = self::api_request(
			'/Invoice/Factory/saveInvoice',
			'POST',
			$invoice_data
		);

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

		if ( empty( $response['objects']['invoice']['id'] ) ) {
			return new WP_Error(
				'invoice_creation_failed',
				__( 'Rechnung konnte nicht erstellt werden.', 'kurs-booking' )
			);
		}

		$invoice_id     = (int) $response['objects']['invoice']['id'];
		$invoice_number = $response['objects']['invoice']['invoiceNumber'] ?? '';

		// Save meta data to booking.
		update_post_meta( $buchung_id, '_sevdesk_invoice_id', $invoice_id );
		update_post_meta( $buchung_id, '_sevdesk_invoice_number', $invoice_number );
		if ( $is_e_invoice ) {
			update_post_meta( $buchung_id, '_sevdesk_is_e_invoice', '1' );
		}

		$e_invoice_info = $is_e_invoice ? ' [E-Rechnung]' : '';
		self::log(
			'Invoice created',
			'/Invoice/Factory/saveInvoice',
			"Booking {$buchung_id} → Invoice {$invoice_number} (ID: {$invoice_id}){$e_invoice_info}"
		);

		return array(
			'invoice_id'     => $invoice_id,
			'invoice_number' => $invoice_number,
			'contact_id'     => $contact_id,
			'is_e_invoice'   => $is_e_invoice,
		);
	}

	/**
	 * Build invoice positions from booking data.
	 *
	 * Sprint 4.10: Only includes variants with auto_invoice: true.
	 * Supports per_day pricing (price × course days).
	 *
	 * @param int $buchung_id Booking post ID.
	 * @return array<int, array<string, mixed>>|WP_Error Invoice positions array or error.
	 */
	private static function build_invoice_positions( int $buchung_id ): array|WP_Error {
		$kurs_id      = (int) Kurs_Booking_Buchung::get_meta( $buchung_id, 'kurs_id', 0 );
		$tickets_json = Kurs_Booking_Buchung::get_meta( $buchung_id, 'tickets', '{}' );
		$tickets      = json_decode( $tickets_json, true ) ?: array();

		// Sprint 4.10: Get user-selected days per variant.
		$ticket_days_json = Kurs_Booking_Buchung::get_meta( $buchung_id, 'ticket_days', '{}' );
		$ticket_days      = json_decode( $ticket_days_json, true ) ?: array();

		if ( empty( $tickets ) ) {
			// Fallback: Single variant booking.
			$anzahl      = (int) Kurs_Booking_Buchung::get_meta( $buchung_id, 'anzahl', 1 );
			$total_price = (float) Kurs_Booking_Buchung::get_meta( $buchung_id, 'total_price', 0 );

			if ( $anzahl > 0 && $total_price > 0 ) {
				$tickets = array( 1 => $anzahl );
			}
		}

		if ( empty( $tickets ) ) {
			return new WP_Error(
				'no_tickets',
				__( 'Buchung enthält keine Tickets.', 'kurs-booking' )
			);
		}

		$varianten      = Kurs_Booking_Kurs::get_varianten( $kurs_id );
		$kurs_days      = Kurs_Booking_Kurs::get_course_days( $kurs_id );
		$price_settings = Kurs_Booking_Settings::get_price_settings();
		$positions      = array();
		$position_index = 0;

		foreach ( $tickets as $variant_id => $quantity ) {
			$quantity = (int) $quantity;

			if ( $quantity < 1 ) {
				continue;
			}

			// Find variant.
			// Varianten können über 'id'-Feld, Index+1 (alte Logik) oder Index (0-basiert) gefunden werden.
			$variant = null;
			foreach ( $varianten as $index => $v ) {
				$v_id = $v['id'] ?? null;
				// Prüfe: explizite ID, Index+1 (1-basiert), oder Index (0-basiert).
				if ( $v_id !== null && (int) $v_id === (int) $variant_id ) {
					$variant = $v;
					break;
				}
				if ( ( $index + 1 ) === (int) $variant_id ) {
					$variant = $v;
					break;
				}
				if ( $index === (int) $variant_id ) {
					$variant = $v;
					break;
				}
			}

			if ( ! $variant ) {
				continue;
			}

			// Sprint 4.10: Skip variants that should not be auto-invoiced.
			$auto_invoice = $variant['auto_invoice'] ?? true;
			if ( ! $auto_invoice ) {
				self::log(
					'Skipping manual variant',
					'build_invoice_positions',
					sprintf( 'Variant "%s" (ID: %d) skipped - auto_invoice=false', $variant['name'] ?? '', $variant_id )
				);
				continue;
			}

			// Build position name from Settings template.
			$position_text = Kurs_Booking_Settings::build_invoice_position_text( $buchung_id );
			$variant_name  = $variant['name'] ?? __( 'Ticket', 'kurs-booking' );
			$variant_price = (float) ( $variant['preis'] ?? 0 );

			// Sprint 4.10: Apply per_day pricing with user-selected days.
			$per_day = ! empty( $variant['per_day'] );
			if ( $per_day ) {
				// Get user-selected days for this variant, default to full course days.
				$selected_days = isset( $ticket_days[ $variant_id ] ) && $ticket_days[ $variant_id ] > 0
					? min( absint( $ticket_days[ $variant_id ] ), $kurs_days )
					: $kurs_days;
				$variant_price = $variant_price * $selected_days;
				$variant_name .= sprintf(
					/* translators: %d: Number of days */
					__( ' (%d Tage)', 'kurs-booking' ),
					$selected_days
				);
			}

			// Combine: "Hundetraining (Buchung: WW-2025-0001) - Erwachsene".
			$full_position_name = $position_text . ' - ' . $variant_name;

			// Calculate prices based on settings.
			// Hinweis: Settings-Keys sind 'small_business' und 'price_display'.
			$is_small_business  = ! empty( $price_settings['small_business'] );
			$prices_include_vat = ( $price_settings['price_display'] ?? 'gross' ) === 'gross';
			$vat_rate           = (int) ( $price_settings['vat_rate'] ?? 19 );

			if ( $is_small_business ) {
				// Kleinunternehmer: Keine MwSt.
				$price_net = $variant_price;
				$tax_rate  = 0;
				$price_tax = 0.0;
			} elseif ( $prices_include_vat ) {
				// Brutto-Preise: Netto berechnen.
				$price_net = Kurs_Booking_Settings::calculate_net_from_gross( $variant_price );
				$tax_rate  = $vat_rate;
				$price_tax = Kurs_Booking_Settings::calculate_vat_amount( $variant_price );
			} else {
				// Netto-Preise: Direkt verwenden.
				$price_net = $variant_price;
				$tax_rate  = $vat_rate;
				$price_tax = $variant_price * $vat_rate / 100;
			}

			// Hinweis: sevDesk interpretiert 'price' als BRUTTO-Preis!
			// Bei Brutto-Preisen: price = variant_price (brutto)
			// Bei Netto-Preisen: price = variant_price * (1 + tax_rate/100) (berechnet brutto)
			$price_for_sevdesk = $prices_include_vat
				? $variant_price // Bereits Brutto.
				: $variant_price * ( 1 + $tax_rate / 100 ); // Netto → Brutto berechnen.

			$positions[] = array(
				'objectName'     => 'InvoicePos',
				'mapAll'         => true,
				'quantity'       => $quantity,
				'price'          => round( $price_for_sevdesk, 2 ), // sevDesk erwartet BRUTTO!
				'name'           => $full_position_name,
				'unity'          => array(
					'id'         => self::UNITY_PIECE,
					'objectName' => 'Unity',
				),
				'positionNumber' => $position_index,
				'text'           => $full_position_name,
				'discount'       => 0,
				'taxRate'        => $tax_rate,
			);

			$position_index++;
		}

		if ( empty( $positions ) ) {
			return new WP_Error(
				'no_positions',
				__( 'Keine gültigen Rechnungspositionen. Alle Varianten sind als "manuelle Verrechnung" markiert.', 'kurs-booking' )
			);
		}

		return $positions;
	}

	/**
	 * Build invoice head text.
	 *
	 * @param int    $buchung_id Booking ID.
	 * @param string $kurs_title Course title.
	 * @param string $kurs_date  Course date.
	 * @return string Head text.
	 */
	private static function build_head_text(
		int $buchung_id,
		string $kurs_title,
		string $kurs_date
	): string {
		$booking_title = get_the_title( $buchung_id );
		$kunde_name    = Kurs_Booking_Buchung::get_meta( $buchung_id, 'name', '' );
		$kunde_email   = Kurs_Booking_Buchung::get_meta( $buchung_id, 'email', '' );
		$formatted_date = $kurs_date ? wp_date( 'd.m.Y', strtotime( $kurs_date ) ) : '';

		// Template aus Einstellungen holen.
		$default_template = "Vielen Dank für Ihre Buchung!\n\nBuchungsnummer: {buchungs_nummer}\nVeranstaltung: {kurs_name}\nDatum: {datum}";
		$template = get_option( 'kurs_booking_sevdesk_head_text', $default_template );

		// Platzhalter ersetzen.
		$placeholders = array(
			'{buchungs_nummer}' => $booking_title,
			'{kurs_name}'       => $kurs_title,
			'{datum}'           => $formatted_date,
			'{kunde_name}'      => $kunde_name,
			'{kunde_email}'     => $kunde_email,
		);

		$text = str_replace( array_keys( $placeholders ), array_values( $placeholders ), $template );

		return $text;
	}

	/**
	 * Build invoice foot text.
	 *
	 * @param bool $is_small_business Whether small business regulation applies.
	 * @return string Foot text.
	 */
	private static function build_foot_text( bool $is_small_business ): string {
		$bank        = Kurs_Booking_Settings::get_bank_data();
		$firma_name  = get_option( 'kurs_booking_company_name', '' );

		// Kleinunternehmer-Hinweis voranstellen.
		$text = '';
		if ( $is_small_business ) {
			$text .= __( 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.', 'kurs-booking' ) . "\n\n";
		}

		// Template aus Einstellungen holen.
		$default_template = "Bitte überweisen Sie den Betrag innerhalb von 14 Tagen auf folgendes Konto:\n\nBank: {bank_name}\nIBAN: {bank_iban}\nBIC: {bank_bic}\nKontoinhaber: {bank_inhaber}";
		$template = get_option( 'kurs_booking_sevdesk_foot_text', $default_template );

		// Platzhalter ersetzen.
		$placeholders = array(
			'{bank_name}'    => $bank['bank_name'] ?? '',
			'{bank_iban}'    => $bank['iban'] ?? '',
			'{bank_bic}'     => $bank['bic'] ?? '',
			'{bank_inhaber}' => $bank['holder'] ?? '',
			'{firma_name}'   => $firma_name,
		);

		$text .= str_replace( array_keys( $placeholders ), array_values( $placeholders ), $template );

		return $text;
	}

	/**
	 * Build customer reference text (Kundenreferenz / "Ihre Referenz").
	 *
	 * @param int    $buchung_id Booking ID.
	 * @param string $kurs_title Course title.
	 * @return string Customer reference text.
	 */
	private static function build_customer_reference( int $buchung_id, string $kurs_title ): string {
		$booking_title = get_the_title( $buchung_id );
		$kunde_name    = Kurs_Booking_Buchung::get_meta( $buchung_id, 'name', '' );

		// Template aus Einstellungen holen (Standard: Buchungsnummer).
		$template = get_option( 'kurs_booking_sevdesk_reference_text', '{buchungs_nummer}' );

		// Platzhalter ersetzen.
		$placeholders = array(
			'{buchungs_nummer}' => $booking_title,
			'{kurs_name}'       => $kurs_title,
			'{kunde_name}'      => $kunde_name,
		);

		return str_replace( array_keys( $placeholders ), array_values( $placeholders ), $template );
	}

	/**
	 * Send invoice via email.
	 *
	 * @param int    $invoice_id sevDesk invoice ID.
	 * @param string $email      Recipient email.
	 * @param string $subject    Email subject (optional).
	 * @param string $text       Email body (optional).
	 * @return bool|WP_Error True on success, error on failure.
	 */
	public static function send_invoice_email(
		int $invoice_id,
		string $email,
		string $subject = '',
		string $text = ''
	): bool|WP_Error {
		if ( ! is_email( $email ) ) {
			return new WP_Error(
				'invalid_email',
				__( 'Ungültige E-Mail-Adresse.', 'kurs-booking' )
			);
		}

		if ( empty( $subject ) ) {
			$subject = __( 'Ihre Rechnung', 'kurs-booking' );
		}

		if ( empty( $text ) ) {
			$text = __( 'Anbei erhalten Sie Ihre Rechnung.', 'kurs-booking' );
		}

		$data = array(
			'toEmail'  => $email,
			'subject'  => $subject,
			'text'     => $text,
			'copy'     => false,
			'sendType' => 'VM',
		);

		$response = self::api_request(
			"/Invoice/{$invoice_id}/sendViaEmail",
			'POST',
			$data
		);

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

		self::log( 'Invoice email sent', "/Invoice/{$invoice_id}/sendViaEmail", $email );

		return true;
	}

	/**
	 * Get invoice PDF.
	 *
	 * @param int $invoice_id sevDesk invoice ID.
	 * @return string|WP_Error Base64 encoded PDF or error.
	 */
	public static function get_invoice_pdf( int $invoice_id ): string|WP_Error {
		// First render the PDF.
		$render_response = self::api_request(
			"/Invoice/{$invoice_id}/render",
			'POST',
			array( 'forceReload' => true )
		);

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

		// Then get the document.
		$response = self::api_request(
			"/Invoice/{$invoice_id}/getPdf",
			'GET'
		);

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

		if ( empty( $response['objects']['content'] ) ) {
			return new WP_Error(
				'pdf_not_found',
				__( 'PDF konnte nicht abgerufen werden.', 'kurs-booking' )
			);
		}

		return $response['objects']['content'];
	}

	/**
	 * Get invoice XML for E-Rechnung (XRechnung/ZUGFeRD).
	 *
	 * Returns the embedded XML data from an E-Rechnung.
	 * Only works if the invoice was created with propertyIsEInvoice: true.
	 *
	 * @param int $invoice_id sevDesk invoice ID.
	 * @return string|WP_Error XML content or error.
	 */
	public static function get_invoice_xml( int $invoice_id ): string|WP_Error {
		$response = self::api_request(
			"/Invoice/{$invoice_id}/getXml",
			'GET'
		);

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

		// Response contains the XML directly or in objects.
		if ( ! empty( $response['objects'] ) ) {
			return $response['objects'];
		}

		// Some versions return XML content directly.
		if ( is_string( $response ) ) {
			return $response;
		}

		return new WP_Error(
			'xml_not_found',
			__( 'XRechnung-XML konnte nicht abgerufen werden. Stellen Sie sicher, dass die Rechnung als E-Rechnung erstellt wurde.', 'kurs-booking' )
		);
	}

	/**
	 * Check if invoice is an E-Rechnung (ZUGFeRD).
	 *
	 * @param int $invoice_id sevDesk invoice ID.
	 * @return bool True if invoice is E-Rechnung.
	 */
	public static function is_e_invoice( int $invoice_id ): bool {
		$response = self::api_request(
			"/Invoice/{$invoice_id}",
			'GET',
			array( 'embed' => 'propertyIsEInvoice' )
		);

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

		return ! empty( $response['objects']['propertyIsEInvoice'] );
	}

	/**
	 * Mark invoice as sent/open.
	 *
	 * Changes invoice status from draft (100) to open (200).
	 * Updated for sevDesk API 2.0 - uses sendBy endpoint instead of deprecated changeStatus.
	 *
	 * @see https://tech.sevdesk.com/api_news/posts/2024_04_04-changing-status-invoices-cretid-notes/
	 *
	 * @param int    $invoice_id sevDesk invoice ID.
	 * @param string $send_type  Send type: 'VPR' (Print), 'VP' (Post), 'VM' (Email), 'VPDF' (PDF).
	 * @return array{invoice_number: string}|WP_Error Invoice number on success, error on failure.
	 */
	public static function mark_invoice_as_open( int $invoice_id, string $send_type = 'VPDF' ): array|WP_Error {
		// sevDesk 2.0: Use sendBy endpoint to change status from Draft to Open.
		// This also assigns an invoice number.
		$response = self::api_request(
			"/Invoice/{$invoice_id}/sendBy",
			'PUT',
			array(
				'sendType'  => $send_type,
				'sendDraft' => false,
			)
		);

		if ( is_wp_error( $response ) ) {
			// Fallback: Try resetToOpen for already-processed invoices.
			if ( str_contains( $response->get_error_message(), 'sendBy' ) ) {
				$reset_response = self::api_request(
					"/Invoice/{$invoice_id}/resetToOpen",
					'PUT'
				);

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

				self::log( 'Invoice reset to open', "/Invoice/{$invoice_id}/resetToOpen", 'Status: Open (200)' );

				return array( 'invoice_number' => $reset_response['objects']['invoiceNumber'] ?? '' );
			}

			return $response;
		}

		$invoice_number = $response['objects']['invoiceNumber'] ?? '';

		self::log(
			'Invoice marked as open',
			"/Invoice/{$invoice_id}/sendBy",
			"Status: Open (200), Number: {$invoice_number}, SendType: {$send_type}"
		);

		return array( 'invoice_number' => $invoice_number );
	}

	// =========================================================================
	// Hook Integration (Sprint 3.4)
	// =========================================================================

	/**
	 * Initialize sevDesk hooks.
	 */
	public static function init(): void {
		// Hook into booking confirmation.
		add_action( 'kurs_booking_confirmed', array( __CLASS__, 'on_booking_confirmed' ), 10, 1 );

		// Hook into booking cancellation (Sprint 6).
		add_action( 'kurs_booking_cancelled', array( __CLASS__, 'on_booking_cancelled' ), 10, 4 );

		// Hook into invoice creation for optional email send.
		add_action( 'kurs_booking_sevdesk_invoice_created', array( __CLASS__, 'maybe_send_invoice_email' ), 10, 2 );

		// AJAX handlers.
		add_action( 'wp_ajax_kurs_booking_create_sevdesk_invoice', array( __CLASS__, 'ajax_create_invoice' ) );
		add_action( 'wp_ajax_kurs_booking_test_sevdesk_connection', array( __CLASS__, 'ajax_test_connection' ) );
	}

	/**
	 * Handle booking cancellation.
	 *
	 * Optionally creates a credit note or cancels the invoice in sevDesk.
	 *
	 * @param int    $buchung_id    Booking post ID.
	 * @param float  $refund_amount Amount to refund.
	 * @param float  $fee_amount    Cancellation fee amount.
	 * @param string $reason        Cancellation reason.
	 */
	public static function on_booking_cancelled( int $buchung_id, float $refund_amount, float $fee_amount, string $reason ): void {
		// Check if sevDesk is enabled.
		if ( ! self::is_enabled() ) {
			return;
		}

		// Check if invoice exists.
		$invoice_id = get_post_meta( $buchung_id, '_sevdesk_invoice_id', true );

		if ( empty( $invoice_id ) ) {
			self::log(
				'No invoice to cancel',
				'on_booking_cancelled',
				"Booking {$buchung_id} has no sevDesk invoice"
			);
			return;
		}

		// TODO: Implement credit note creation (future Sprint).
		// For now, just log the cancellation.
		self::log(
			'Booking cancelled',
			'on_booking_cancelled',
			sprintf(
				'Booking %d cancelled. Invoice %s. Refund: %.2f EUR, Fee: %.2f EUR, Reason: %s',
				$buchung_id,
				$invoice_id,
				$refund_amount,
				$fee_amount,
				$reason
			)
		);

		// Store cancellation info as meta.
		update_post_meta( $buchung_id, '_sevdesk_cancellation_logged', current_time( 'mysql' ) );

		/**
		 * Action after sevDesk booking cancellation is logged.
		 *
		 * Hook for future credit note implementation.
		 *
		 * @param int    $buchung_id    Booking post ID.
		 * @param string $invoice_id    sevDesk invoice ID.
		 * @param float  $refund_amount Amount to refund.
		 * @param float  $fee_amount    Cancellation fee amount.
		 */
		do_action( 'kurs_booking_sevdesk_cancellation_logged', $buchung_id, $invoice_id, $refund_amount, $fee_amount );
	}

	/**
	 * Handle booking confirmation.
	 *
	 * Creates contact and invoice in sevDesk when a booking is confirmed.
	 *
	 * @param int $buchung_id Booking post ID.
	 */
	public static function on_booking_confirmed( int $buchung_id ): void {
		// Check if sevDesk is enabled.
		if ( ! self::is_enabled() ) {
			return;
		}

		// Check if auto-invoice is enabled.
		$settings = Kurs_Booking_Settings::get_sevdesk_settings();

		if ( empty( $settings['auto_invoice'] ) ) {
			self::log( 'Auto-invoice disabled', 'on_booking_confirmed', "Booking {$buchung_id} skipped" );
			return;
		}

		// Check if invoice already exists.
		$existing_invoice = get_post_meta( $buchung_id, '_sevdesk_invoice_id', true );

		if ( $existing_invoice ) {
			self::log(
				'Invoice exists',
				'on_booking_confirmed',
				"Booking {$buchung_id} already has invoice {$existing_invoice}"
			);
			return;
		}

		// Create invoice (this will also create/find contact).
		$result = self::create_invoice_for_booking( $buchung_id );

		if ( is_wp_error( $result ) ) {
			self::log(
				'Invoice creation failed',
				'on_booking_confirmed',
				$result->get_error_message()
			);

			// Store error for admin notice.
			update_post_meta( $buchung_id, '_sevdesk_error', $result->get_error_message() );
			update_post_meta( $buchung_id, '_sevdesk_error_time', current_time( 'mysql' ) );

			/**
			 * Fires when sevDesk invoice creation fails.
			 *
			 * @param int      $buchung_id Booking ID.
			 * @param WP_Error $error      The error.
			 */
			do_action( 'kurs_booking_sevdesk_invoice_failed', $buchung_id, $result );

			return;
		}

		// Clear any previous errors.
		delete_post_meta( $buchung_id, '_sevdesk_error' );
		delete_post_meta( $buchung_id, '_sevdesk_error_time' );

		/**
		 * Fires when sevDesk invoice is created successfully.
		 *
		 * @param int   $buchung_id Booking ID.
		 * @param array $result     Invoice data with invoice_id, invoice_number, contact_id.
		 */
		do_action( 'kurs_booking_sevdesk_invoice_created', $buchung_id, $result );

		self::log(
			'Invoice created successfully',
			'on_booking_confirmed',
			sprintf(
				'Booking %d → Invoice %s (ID: %d)',
				$buchung_id,
				$result['invoice_number'],
				$result['invoice_id']
			)
		);
	}

	/**
	 * Send invoice email after creation.
	 *
	 * Hook into: kurs_booking_sevdesk_invoice_created
	 *
	 * @param int                  $buchung_id Booking ID.
	 * @param array<string, mixed> $result     Invoice data.
	 */
	public static function maybe_send_invoice_email( int $buchung_id, array $result ): void {
		$settings = Kurs_Booking_Settings::get_sevdesk_settings();

		// Check if auto-send is enabled.
		if ( empty( $settings['auto_send_invoice'] ) ) {
			return;
		}

		$email = Kurs_Booking_Buchung::get_meta( $buchung_id, 'email', '' );

		if ( empty( $email ) ) {
			return;
		}

		$kurs_id    = (int) Kurs_Booking_Buchung::get_meta( $buchung_id, 'kurs_id', 0 );
		$kurs_title = get_the_title( $kurs_id );

		$subject = sprintf(
			/* translators: %s: Course title */
			__( 'Ihre Rechnung für %s', 'kurs-booking' ),
			$kurs_title
		);

		$text = sprintf(
			/* translators: %s: Course title */
			__( 'Vielen Dank für Ihre Buchung von "%s". Anbei erhalten Sie Ihre Rechnung.', 'kurs-booking' ),
			$kurs_title
		);

		$send_result = self::send_invoice_email(
			$result['invoice_id'],
			$email,
			$subject,
			$text
		);

		if ( is_wp_error( $send_result ) ) {
			self::log(
				'Invoice email failed',
				'maybe_send_invoice_email',
				$send_result->get_error_message()
			);
		} else {
			self::log(
				'Invoice email sent',
				'maybe_send_invoice_email',
				"Sent to {$email}"
			);
		}
	}

	/**
	 * AJAX handler for manual invoice creation.
	 */
	public static function ajax_create_invoice(): void {
		check_ajax_referer( 'kurs_booking_admin', 'nonce' );

		if ( ! current_user_can( 'edit_posts' ) ) {
			wp_send_json_error( array( 'message' => __( 'Keine Berechtigung.', 'kurs-booking' ) ) );
		}

		$buchung_id = isset( $_POST['buchung_id'] ) ? absint( $_POST['buchung_id'] ) : 0;

		if ( ! $buchung_id ) {
			wp_send_json_error( array( 'message' => __( 'Ungültige Buchungs-ID.', 'kurs-booking' ) ) );
		}

		// Check if sevDesk is configured.
		if ( ! self::is_configured() ) {
			wp_send_json_error( array( 'message' => __( 'sevDesk ist nicht konfiguriert.', 'kurs-booking' ) ) );
		}

		$result = self::create_invoice_for_booking( $buchung_id );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		// Clear any previous errors.
		delete_post_meta( $buchung_id, '_sevdesk_error' );
		delete_post_meta( $buchung_id, '_sevdesk_error_time' );

		wp_send_json_success(
			array(
				'message'        => __( 'Rechnung erfolgreich erstellt.', 'kurs-booking' ),
				'invoice_number' => $result['invoice_number'],
				'invoice_id'     => $result['invoice_id'],
			)
		);
	}

	/**
	 * AJAX handler for connection test.
	 */
	public static function ajax_test_connection(): void {
		check_ajax_referer( 'kurs_booking_admin', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Keine Berechtigung.', 'kurs-booking' ) ) );
		}

		$result = self::test_connection();

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success(
			array(
				'message'  => __( 'Verbindung erfolgreich!', 'kurs-booking' ),
				'user_id'  => $result['user_id'],
				'username' => $result['username'],
				'email'    => $result['email'],
				'fullname' => $result['fullname'],
			)
		);
	}
}
