<?php
declare(strict_types=1);

namespace Itdatex\Ausgangsrechnung;

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

use DateTimeImmutable;
use Itdatex\EInvoiceCore\InvoiceDraft;
use Itdatex\EInvoiceCore\InvoiceLineDraft;
use Itdatex\EInvoiceCore\InvoiceParty;
use Itdatex\EInvoiceCore\InvoiceRepository;

/**
 * Erzeugt Storno- bzw. Korrektur-Rechnungen (BT-3 = 384) fuer eine
 * vorhandene Ausgangs-Rechnung.
 *
 * Konvention: alle Zeilen-Mengen werden negiert; das erzeugt automatisch
 * negative Netto/Brutto-Betraege. Der Bezug zur Original-Rechnung wird
 * ueber BT-25 (precedingInvoiceNumber) hergestellt.
 *
 * Setzt voraus, dass die Original-Rechnung mit dem aktuellen Plugin
 * ausgestellt wurde — nur dann liegen Buyer und Line-Drafts vollstaendig
 * in `raw_meta` vor. Aeltere Rechnungen (vor dem raw_meta-Feature)
 * lassen sich noch nicht stornieren; das erkennen wir durch fehlende
 * `lines` und melden es dem Aufrufer.
 */
final class StornoService
{
    /** UN/CEFACT 1001 — Korrektur-Rechnung (Storno). */
    public const TYPE_CODE_CORRECTED = 384;

    public function __construct(
        private readonly InvoiceRepository $repo = new InvoiceRepository( 'outgoing' ),
    ) {}

    /**
     * Ergebnis-Codes fuer die Handler-Schicht.
     */
    public const RESULT_OK              = 'ok';
    public const RESULT_ALREADY_STORNO  = 'already_storno';
    public const RESULT_ORIGINAL_MISSING = 'original_missing';
    public const RESULT_NOT_STORNABLE   = 'not_stornable';

    /**
     * Baut den Storno-Draft. Kein Persist — das macht der Aufrufer via IssueService.
     *
     * @return array{result:string, draft?:InvoiceDraft, message?:string}
     */
    public function buildStornoDraft(
        int $originalInvoiceId,
        string $newInvoiceNumber,
        InvoiceParty $seller,
        ?DateTimeImmutable $issueDate = null
    ): array {
        $original = $this->repo->findInvoice( $originalInvoiceId );
        if ( ! $original || (string) $original->direction !== 'outgoing' ) {
            return [ 'result' => self::RESULT_ORIGINAL_MISSING ];
        }

        // Bereits eine Storno-Rechnung? Nicht doppelt stornieren.
        if ( (int) ( $original->raw_meta ? ( json_decode( (string) $original->raw_meta, true )['invoice']['type_code'] ?? 380 ) : 380 ) === self::TYPE_CODE_CORRECTED ) {
            return [ 'result' => self::RESULT_ALREADY_STORNO,
                'message' => __( 'Diese Rechnung ist selbst eine Storno-Rechnung.', 'itdatex-e-invoice-output' ) ];
        }
        if ( $this->findStornoFor( $originalInvoiceId ) !== null ) {
            return [ 'result' => self::RESULT_ALREADY_STORNO,
                'message' => __( 'Fuer diese Rechnung existiert bereits ein Storno.', 'itdatex-e-invoice-output' ) ];
        }

        $meta = json_decode( (string) $original->raw_meta, true );
        if ( ! is_array( $meta ) || ! isset( $meta['buyer'], $meta['lines'], $meta['invoice'] ) ) {
            return [ 'result' => self::RESULT_NOT_STORNABLE,
                'message' => __(
                    'Original-Rechnung hat keine strukturierten Positions-Daten (vermutlich vor Einfuehrung dieses Features ausgestellt). Storno bitte manuell erstellen.',
                    'itdatex-e-invoice-output'
                ) ];
        }

        $buyer = self::buyerFromArray( (array) $meta['buyer'] );
        $lines = self::negatedLinesFromArray( (array) $meta['lines'] );
        if ( $lines === [] ) {
            return [ 'result' => self::RESULT_NOT_STORNABLE,
                'message' => __( 'Original-Rechnung hat keine Positionen.', 'itdatex-e-invoice-output' ) ];
        }
        $inv = (array) $meta['invoice'];

        $issueDate ??= new DateTimeImmutable( 'today' );
        $originalIssueDate = (string) $original->issue_date;
        $precedingIssue    = ( $originalIssueDate !== '' && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $originalIssueDate ) )
            ? new DateTimeImmutable( $originalIssueDate )
            : null;

        $draft = new InvoiceDraft(
            invoiceNumber:          $newInvoiceNumber,
            issueDate:              $issueDate,
            seller:                 $seller,
            buyer:                  $buyer,
            lines:                  $lines,
            dueDate:                null,   // Kein Faelligkeitsdatum bei Storno
            currency:               (string) ( $inv['currency'] ?? 'EUR' ),
            buyerReference:         self::nullIfEmpty( $inv['buyer_reference'] ?? null ),
            note:                   sprintf(
                /* translators: %s = Original-Rechnungsnr. */
                (string) __( 'Storno zur Rechnung %s', 'itdatex-e-invoice-output' ),
                (string) $original->invoice_number
            ),
            paymentIban:            self::nullIfEmpty( $inv['payment_iban'] ?? null ),
            paymentBic:             self::nullIfEmpty( $inv['payment_bic']  ?? null ),
            paymentTerms:           self::nullIfEmpty( $inv['payment_terms'] ?? null ),
            deliveryDate:           null,
            typeCode:               self::TYPE_CODE_CORRECTED,
            precedingInvoiceNumber: (string) $original->invoice_number,
            precedingIssueDate:     $precedingIssue,
        );

        return [ 'result' => self::RESULT_OK, 'draft' => $draft ];
    }

    /**
     * Sucht ein bereits vorhandenes Storno fuer die uebergebene Original-Rechnung
     * (matcht ueber preceding_number im raw_meta).
     */
    public function findStornoFor( int $originalInvoiceId ): ?object
    {
        $original = $this->repo->findInvoice( $originalInvoiceId );
        if ( ! $original ) {
            return null;
        }
        $originalNumber = (string) $original->invoice_number;
        if ( $originalNumber === '' ) {
            return null;
        }
        // listByDirection selektiert kein raw_meta — wir muessen pro Kandidat
        // nachschlagen. Bei realistischen Bestaenden (~500 offen zu pruefen)
        // ist das ein akzeptabler N+1.
        foreach ( $this->repo->listByDirection( 'outgoing', 500 ) as $lite ) {
            if ( (int) $lite->id === $originalInvoiceId ) continue;
            $full = $this->repo->findInvoice( (int) $lite->id );
            if ( ! $full ) continue;
            $rowMeta = json_decode( (string) ( $full->raw_meta ?? '' ), true );
            if ( ! is_array( $rowMeta ) ) continue;
            $preceding = (string) ( $rowMeta['invoice']['preceding_number'] ?? '' );
            $typeCode  = (int)    ( $rowMeta['invoice']['type_code']        ?? 380 );
            if ( $typeCode === self::TYPE_CODE_CORRECTED && $preceding === $originalNumber ) {
                return $full;
            }
        }
        return null;
    }

    /**
     * @param array<string, mixed> $b
     */
    private static function buyerFromArray( array $b ): InvoiceParty
    {
        $addressLines = preg_split( '/\r\n|\r|\n/', trim( (string) ( $b['address'] ?? '' ) ) ) ?: [];
        $addressLines = array_values( array_filter( array_map( 'trim', $addressLines ), static fn( $l ) => $l !== '' ) );

        return new InvoiceParty(
            name:                    (string) ( $b['name'] ?? '' ),
            vatNumber:               self::nullIfEmpty( $b['vat_number']    ?? null ),
            addressLines:            $addressLines,
            city:                    self::nullIfEmpty( $b['city']         ?? null ),
            postalCode:              self::nullIfEmpty( $b['postal_code']  ?? null ),
            countryCode:             self::nullIfEmpty( $b['country_code'] ?? null ),
            contactName:             self::nullIfEmpty( $b['contact_name']  ?? null ),
            contactEmail:            self::nullIfEmpty( $b['contact_email'] ?? null ),
            contactPhone:            self::nullIfEmpty( $b['contact_phone'] ?? null ),
            electronicAddressScheme: self::nullIfEmpty( $b['electronic_address_scheme'] ?? null ),
            electronicAddressValue:  self::nullIfEmpty( $b['electronic_address_value']  ?? null ),
        );
    }

    /**
     * @param array<int, array<string, mixed>> $rows
     * @return InvoiceLineDraft[]
     */
    private static function negatedLinesFromArray( array $rows ): array
    {
        $out = [];
        foreach ( $rows as $r ) {
            if ( ! is_array( $r ) ) continue;
            $name = trim( (string) ( $r['name'] ?? '' ) );
            $qty  = (float) ( $r['quantity'] ?? 0 );
            if ( $name === '' || $qty === 0.0 ) continue;
            $out[] = new InvoiceLineDraft(
                name:               $name,
                quantity:           -$qty,   // Storno = negierte Menge
                unitPrice:          (float) ( $r['unit_price'] ?? 0 ),
                vatRate:            (float) ( $r['vat_rate']  ?? 19.0 ),
                unitCode:           (string) ( $r['unit_code'] ?? 'C62' ),
                vatCategory:        (string) ( $r['vat_category'] ?? 'S' ),
                description:        isset( $r['description'] ) && $r['description'] !== '' ? (string) $r['description'] : null,
                taxExemptionReason: isset( $r['tax_exemption_reason'] ) && $r['tax_exemption_reason'] !== '' ? (string) $r['tax_exemption_reason'] : null,
            );
        }
        return $out;
    }

    private static function nullIfEmpty( mixed $value ): ?string
    {
        if ( $value === null ) return null;
        $s = trim( (string) $value );
        return $s === '' ? null : $s;
    }
}
