<?php

declare( strict_types=1 );

namespace Brightfern\Invoice;

use Brightfern\Invoice\Input\OrderCharge;
use Brightfern\Invoice\Input\OrderData;
use Brightfern\Invoice\Input\OrderLine;
use Brightfern\Invoice\Input\SellerProfile;
use Brightfern\Invoice\Model\Address;
use Brightfern\Invoice\Model\AllowanceCharge;
use Brightfern\Invoice\Model\Invoice;
use Brightfern\Invoice\Model\InvoiceLine;
use Brightfern\Invoice\Model\Party;
use Brightfern\Invoice\Model\TaxSubtotal;
use Brightfern\Support\Num;

/**
 * Maps a normalized {@see OrderData} + {@see SellerProfile} into the internal
 * EN 16931 {@see Invoice} model. This is the single place where the hard cases
 * live: mixed VAT rates, per-rate subtotals, line discounts, reverse charge,
 * intra-community supply, export, exempt sellers and cent-accurate rounding.
 *
 * It performs NO transmission and has NO WordPress dependency — that is what
 * makes the edge cases unit-testable (see tests/Invoice/OrderInvoiceMapperTest).
 *
 * Credit notes (typeCode 381) use the same shape as an invoice — the caller
 * sets OrderData::$isCreditNote and $precedingInvoiceNumber (BT-25); the
 * mapper otherwise treats it exactly like an invoice.
 */
final class OrderInvoiceMapper {

	public function map( OrderData $order, SellerProfile $seller ): Invoice {
		$invoice                 = new Invoice();
		$invoice->number         = $order->number;
		$invoice->issueDate      = $order->issueDate;
		$invoice->dueDate        = $order->dueDate;
		$invoice->currency       = $order->currency;
		$invoice->buyerReference = $order->buyerReference;
		$invoice->orderReference = $order->orderReference;
		$invoice->note           = $order->note;
		$invoice->sellerCountry  = $seller->countryCode;
		$invoice->typeCode       = $order->isCreditNote ? Invoice::TYPE_CREDIT_NOTE : Invoice::TYPE_INVOICE;
		$invoice->precedingInvoiceNumber    = $order->precedingInvoiceNumber;
		$invoice->precedingInvoiceIssueDate = $order->precedingInvoiceIssueDate;

		$invoice->seller = $this->buildSeller( $seller );
		$invoice->buyer  = $this->buildBuyer( $order );

		// Document-level VAT context, decided once and applied to 0-rated lines.
		$ctx = $this->vatContext( $order, $seller );

		$invoice->lines            = $this->mapLines( $order->lines, $ctx );
		$invoice->allowanceCharges = $this->mapCharges( $order->charges, $ctx );

		// VAT breakdown must be built from the mapped lines + charges so the
		// taxable bases match exactly what the serializer emits.
		$invoice->taxSubtotals = $this->buildVatBreakdown( $invoice->lines, $invoice->allowanceCharges );

		$invoice->totals = $this->buildTotals( $invoice, $order->prepaidAmount );

		// Payment defaults from the seller profile.
		$invoice->payment->meansCode      = $seller->paymentMeansCode;
		$invoice->payment->payeeIban      = $seller->payeeIban;
		$invoice->payment->payeeBic       = $seller->payeeBic;
		$invoice->payment->terms          = $seller->paymentTerms;
		$invoice->payment->remittanceInfo = $order->number; // Reconciliation key.

		return $invoice;
	}

	/**
	 * @return array{sellerHasVat:bool,sellerExempt:bool,reverseCharge:bool,intraCommunity:bool,export:bool}
	 */
	private function vatContext( OrderData $order, SellerProfile $seller ): array {
		$shipTo       = strtoupper( $order->shipToCountry() );
		$sellerCty    = strtoupper( $seller->countryCode );
		$buyerHasVat  = '' !== trim( $order->buyerVat );
		$inEu         = in_array( $shipTo, Address::EU_COUNTRIES, true );

		$export         = '' !== $shipTo && ! $inEu;
		$intraCommunity = $buyerHasVat && $inEu && '' !== $shipTo && $shipTo !== $sellerCty;

		return array(
			'sellerHasVat'   => $seller->hasVat(),
			'sellerExempt'   => $seller->exemptSmallBusiness,
			'reverseCharge'  => $order->reverseCharge,
			'intraCommunity' => $intraCommunity,
			'export'         => $export,
		);
	}

	private function buildSeller( SellerProfile $p ): Party {
		$party                   = new Party();
		$party->registrationName = $p->registrationName;
		$party->tradingName      = $p->tradingName;
		$party->vatNumber        = $p->vatNumber;
		$party->companyId        = $p->companyId;
		$party->companyIdScheme  = $p->companyIdScheme;
		$party->endpointId       = $p->endpointId;
		$party->endpointScheme   = $p->endpointScheme;
		$party->contactName      = $p->contactName;
		$party->contactPhone     = $p->contactPhone;
		$party->contactEmail     = $p->contactEmail;

		$addr                   = new Address();
		$addr->line1            = $p->line1;
		$addr->line2            = $p->line2;
		$addr->city             = $p->city;
		$addr->postalZone       = $p->postalZone;
		$addr->countrySubentity = $p->countrySubentity;
		$addr->countryCode      = strtoupper( $p->countryCode );
		$party->address         = $addr;

		return $party;
	}

	private function buildBuyer( OrderData $o ): Party {
		$party                   = new Party();
		$party->registrationName = $o->buyerName;
		$party->vatNumber        = $o->buyerVat;
		$party->companyId        = $o->buyerCompanyId;
		$party->companyIdScheme  = $o->buyerCompanyIdScheme;
		$party->endpointId       = $o->buyerEndpointId;
		$party->endpointScheme   = $o->buyerEndpointScheme;
		$party->contactName      = $o->buyerName;
		$party->contactPhone     = $o->buyerPhone;
		$party->contactEmail     = $o->buyerEmail;

		$addr                   = new Address();
		$addr->line1            = $o->buyerLine1;
		$addr->line2            = $o->buyerLine2;
		$addr->city             = $o->buyerCity;
		$addr->postalZone       = $o->buyerPostalZone;
		$addr->countrySubentity = $o->buyerCountrySubentity;
		$addr->countryCode      = strtoupper( $o->buyerCountryCode );
		$party->address         = $addr;

		return $party;
	}

	/**
	 * @param OrderLine[] $lines
	 * @param array{sellerHasVat:bool,sellerExempt:bool,reverseCharge:bool,intraCommunity:bool,export:bool} $ctx
	 * @return InvoiceLine[]
	 */
	private function mapLines( array $lines, array $ctx ): array {
		$mapped = array();
		$n      = 0;

		foreach ( $lines as $line ) {
			$il               = new InvoiceLine();
			$il->id           = '' !== $line->id ? $line->id : (string) ( ++$n );
			$il->name         = $line->name;
			$il->description   = $line->description;
			$il->quantity      = $line->quantity;
			$il->unitCode      = $line->unitCode;
			$il->sellerItemId  = $line->sku;
			$il->standardItemId = $line->gtin;
			$il->standardItemScheme = '' !== $line->gtin ? '0160' : ''; // 0160 = GTIN.

			// BT-146 unit price is the PRE-discount unit price; the discount
			// becomes a line allowance so BR-24 holds:
			//   price × qty − allowance = line net.
			$il->netPriceAmount      = Num::money( $line->unitPrice() );
			$il->lineExtensionAmount = Num::money( $line->netAfterDiscount );

			$discount = Num::money( $line->discount() );
			if ( $discount > 0.0 ) {
				$il->allowanceCharges[] = AllowanceCharge::allowance( $discount, 'Discount', '95' );
			}

			$il->vatCategoryCode = VatCategory::classify(
				$line->taxRatePercent,
				$ctx['sellerHasVat'],
				$ctx['reverseCharge'],
				$ctx['intraCommunity'],
				$ctx['export'],
				$ctx['sellerExempt']
			);
			$il->vatPercent = VatCategory::isZeroRateByRule( $il->vatCategoryCode ) ? 0.0 : Num::money( $line->taxRatePercent );

			$mapped[] = $il;
		}

		return $mapped;
	}

	/**
	 * @param OrderCharge[] $charges
	 * @param array{sellerHasVat:bool,sellerExempt:bool,reverseCharge:bool,intraCommunity:bool,export:bool} $ctx
	 * @return AllowanceCharge[]
	 */
	private function mapCharges( array $charges, array $ctx ): array {
		$mapped = array();

		foreach ( $charges as $c ) {
			$ac             = new AllowanceCharge();
			$ac->isCharge   = $c->isCharge;
			$ac->amount     = Num::money( abs( $c->amount ) );
			$ac->reasonText = $c->reasonText;
			$ac->reasonCode = $c->reasonCode;

			$category            = VatCategory::classify(
				$c->taxRatePercent,
				$ctx['sellerHasVat'],
				$ctx['reverseCharge'],
				$ctx['intraCommunity'],
				$ctx['export'],
				$ctx['sellerExempt']
			);
			$ac->vatCategoryCode = $category;
			$ac->vatPercent      = VatCategory::isZeroRateByRule( $category ) ? 0.0 : Num::money( $c->taxRatePercent );

			$mapped[] = $ac;
		}

		return $mapped;
	}

	/**
	 * Build BG-23 from the mapped lines and document charges. One row per
	 * (category, rate). Tax per row is rounded ONCE here (BR-CO-17), which is
	 * what makes mixed-rate invoices reconcile to the cent.
	 *
	 * @param InvoiceLine[]     $lines
	 * @param AllowanceCharge[] $charges
	 * @return TaxSubtotal[]
	 */
	private function buildVatBreakdown( array $lines, array $charges ): array {
		/** @var array<string,TaxSubtotal> $rows */
		$rows = array();

		$ensure = static function ( string $category, float $percent ) use ( &$rows ): TaxSubtotal {
			$key = TaxSubtotal::key( $category, $percent );
			if ( ! isset( $rows[ $key ] ) ) {
				$row                  = new TaxSubtotal( $category, $percent );
				[ $reason, $code ]    = VatCategory::exemptionReason( $category );
				$row->exemptionReasonText = $reason;
				$row->exemptionReasonCode = $code;
				$rows[ $key ]         = $row;
			}
			return $rows[ $key ];
		};

		foreach ( $lines as $line ) {
			$ensure( $line->vatCategoryCode, $line->vatPercent )->taxableAmount += $line->lineExtensionAmount;
		}

		foreach ( $charges as $c ) {
			$row                  = $ensure( (string) $c->vatCategoryCode, (float) $c->vatPercent );
			$row->taxableAmount  += $c->signedAmount();
		}

		// Finalise each row: round the taxable base, then compute the VAT once.
		foreach ( $rows as $row ) {
			$row->taxableAmount = Num::money( $row->taxableAmount );
			$row->taxAmount     = Num::money( $row->taxableAmount * $row->percent / 100 );
		}

		// Deterministic ordering (helps stable XML + test assertions).
		ksort( $rows );

		return array_values( $rows );
	}

	private function buildTotals( Invoice $invoice, float $prepaid ): \Brightfern\Invoice\Model\MonetaryTotals {
		$totals = $invoice->totals;

		$lineSum = 0.0;
		foreach ( $invoice->lines as $line ) {
			$lineSum += $line->lineExtensionAmount;
		}

		$allowanceSum = 0.0;
		$chargeSum    = 0.0;
		foreach ( $invoice->allowanceCharges as $ac ) {
			if ( $ac->isCharge ) {
				$chargeSum += $ac->amount;
			} else {
				$allowanceSum += $ac->amount;
			}
		}

		$taxSum = 0.0;
		foreach ( $invoice->taxSubtotals as $row ) {
			$taxSum += $row->taxAmount;
		}

		$totals->lineExtensionAmount  = Num::money( $lineSum );                       // BT-106.
		$totals->allowanceTotalAmount = Num::money( $allowanceSum );                  // BT-107.
		$totals->chargeTotalAmount    = Num::money( $chargeSum );                     // BT-108.
		$totals->taxExclusiveAmount   = Num::money( $lineSum - $allowanceSum + $chargeSum ); // BT-109.
		$totals->taxTotalAmount       = Num::money( $taxSum );                        // BT-110.
		$totals->taxInclusiveAmount   = Num::money( $totals->taxExclusiveAmount + $taxSum ); // BT-112.
		$totals->prepaidAmount        = Num::money( $prepaid );                       // BT-113.
		$totals->payableRoundingAmount = 0.0;                                          // BT-114.
		$totals->payableAmount        = Num::money( $totals->taxInclusiveAmount - $totals->prepaidAmount ); // BT-115.

		return $totals;
	}
}
