<?php
/**
 * Element class.
 *
 * @package EDD\Invoices\XML\Elements
 */

namespace EDD\Pro\Invoices\XML\Elements;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore

/**
 * Class Element
 *
 * @since 1.4.0
 */
abstract class Element {

	/**
	 * The XML document.
	 *
	 * @since 1.4.0
	 * @var \DOMDocument
	 */
	protected $document;

	/**
	 * The order.
	 *
	 * @since 1.4.0
	 * @var \EDD\Orders\Order
	 */
	protected $order;

	/**
	 * The individual item to add to the XML document.
	 *
	 * @since 1.4.0
	 * @var object
	 */
	protected $item;

	/**
	 * Element constructor.
	 *
	 * @since 1.4.0
	 *
	 * @param \SimpleXMLElement $xml  The XML document.
	 * @param object            $item The item.
	 */
	public function __construct( $xml, $item = null ) {
		$this->document = $xml->dom;
		$this->order    = $xml->order;
		$this->item     = $item;
	}

	/**
	 * Add an element to the XML document.
	 *
	 * @since 1.4.0
	 *
	 * @param \DOMElement|null $parent_element The parent element.
	 * @param array            $data           The data for the element.
	 * @return \DOMElement
	 */
	public function add( $parent_element = null, $data = array() ) {
		if ( empty( $data ) ) {
			$data = $this->get_filtered_data();
		}

		if ( empty( $data ) || empty( $data['name'] ) ) {
			return;
		}

		$value   = ! empty( $data['value'] ) ? $data['value'] : '';
		$element = $this->document->createElement( $data['name'], $value );

		if ( ! empty( $data['attributes'] ) ) {
			foreach ( $data['attributes'] as $name => $value ) {
				$element->setAttribute( $name, $value );
			}
		}

		if ( ! empty( $data['children'] ) ) {
			foreach ( $data['children'] as $child ) {
				$this->add( $element, $child );
			}
		}

		if ( ! $parent_element ) {
			$parent_element = $this->document;
		}
		$parent_element->appendChild( $element );

		return $element;
	}

	/**
	 * Get the data.
	 *
	 * @since 1.4.0
	 * @return array
	 */
	abstract protected function get_data(): array;

	/**
	 * Formats a number.
	 *
	 * @since 1.4.0
	 * @param float $number The number to format.
	 * @return string
	 */
	protected function format_number( $number ) {
		return number_format( $number, 2, '.', '' );
	}

	/**
	 * Get the tax category code (UNCL5305) for the order.
	 *
	 * Uses EDD's existing VAT system to detect reverse charge, including
	 * backward compatibility with the old VAT plugin's legacy meta key.
	 *
	 * @since 3.6.7
	 * @return string Tax category code (S, Z, AE, E, O, etc.).
	 */
	protected function get_tax_category(): string {
		$vat_order = \EDD\Pro\Taxes\VAT\Order::get( $this->order->id );
		if ( $vat_order->is_reverse_charged ) {
			$category = 'AE';
		} elseif ( $this->order->get_tax_rate() > 0 ) {
			$category = 'S';
		} elseif ( edd_use_taxes() ) {
			$category = 'Z';
		} else {
			$category = 'O';
		}

		/**
		 * Filter the tax category code used in XML invoices.
		 *
		 * @since 3.6.7
		 * @param string            $category The UNCL5305 tax category code.
		 * @param \EDD\Orders\Order $order    The order object.
		 */
		return (string) apply_filters( 'edd_invoices_xml_element_tax_category', $category, $this->order );
	}

	/**
	 * Get the effective tax rate for XML output.
	 *
	 * For exempt categories (AE, O, E, Z) the rate must be reported as 0
	 * per Peppol BIS 3.0 business rules (BR-AE-05, BR-E-05, BR-O-05, BR-Z-05).
	 *
	 * @since 3.6.7
	 * @return float
	 */
	protected function get_effective_tax_rate(): float {
		$category = $this->get_tax_category();

		if ( 'S' === $category ) {
			return (float) $this->order->get_tax_rate();
		}

		return 0.0;
	}

	/**
	 * Build the TaxCategory children array for AllowanceCharge elements.
	 *
	 * Omits cbc:Percent when category is "O" per BR-O-06 / BR-O-07.
	 *
	 * @since 3.6.7
	 * @return array
	 */
	protected function get_tax_category_children(): array {
		$category_id = $this->get_tax_category();

		$children = array(
			array(
				'name'  => 'cbc:ID',
				'value' => $category_id,
			),
		);

		// BR-O-06 / BR-O-07: "Not subject to VAT" must not contain a VAT rate.
		if ( 'O' !== $category_id ) {
			$children[] = array(
				'name'  => 'cbc:Percent',
				'value' => $this->format_number( $this->get_effective_tax_rate() ),
			);
		}

		$children[] = array(
			'name'     => 'cac:TaxScheme',
			'children' => array(
				array(
					'name'  => 'cbc:ID',
					'value' => 'VAT',
				),
			),
		);

		return $children;
	}

	/**
	 * Whether to include the PartyTaxScheme element.
	 *
	 * BR-O-02: PartyTaxScheme must not be present when tax category is "O"
	 * (Not subject to VAT).
	 *
	 * @since 3.6.7
	 * @return bool
	 */
	protected function should_include_party_tax_scheme(): bool {
		return 'O' !== $this->get_tax_category();
	}

	/**
	 * Get the filtered data.
	 *
	 * @since 1.4.0
	 * @return array
	 */
	private function get_filtered_data(): array {
		/**
		 * Filter the data for the element.
		 *
		 * @since 1.4.0
		 * @param array            $data  The data for the element.
		 * @param \EDD\Orders\Order $order The order object.
		 * @param string           $id    The element ID.
		 * @param object           $item  The item.
		 */
		return apply_filters(
			"edd_invoices_xml_element_{$this->get_id()}_data",
			$this->get_data(),
			$this->order,
			$this->get_id(),
			$this->item
		);
	}

	/**
	 * Get the element ID.
	 *
	 * @since 1.4.0
	 * @return string
	 */
	private function get_id() {
		$reflection = new \ReflectionClass( $this );

		return strtolower( $reflection->getShortName() );
	}
}
