<?php
declare(strict_types=1);

namespace Itdatex\Ausgangsrechnung;

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

use Itdatex\EInvoiceCore\CiiBuilder;
use Itdatex\EInvoiceCore\InvoiceDraft;
use Itdatex\EInvoiceCore\InvoiceRepository;
use Itdatex\EInvoiceCore\Parser;
use Itdatex\EInvoiceCore\Storage;
use Itdatex\EInvoiceCore\UblBuilder;
use Itdatex\EInvoiceCore\Validator;
use Itdatex\EInvoiceCore\ZugferdPdfBuilder;

/**
 * Pflegt die zentrale Ausstell-Pipeline: Build → Validate → Persist → Repo → Audit.
 *
 * Spiegelt IngestService des Rechnungseingangs — gleiche Lifecycle-Hooks,
 * gleiche Storage-Konventionen, gleiche DB-Tabellen (mit direction='outgoing').
 */
final class IssueService
{
    public const RESULT_ISSUED     = 'issued';
    public const RESULT_DUPLICATE  = 'duplicate';
    public const RESULT_INVALID    = 'invalid';
    public const RESULT_FAILED     = 'failed';

    public const FORMAT_UBL         = 'ubl';
    public const FORMAT_CII         = 'cii';
    public const FORMAT_ZUGFERD_PDF = 'zugferd-pdf';

    private UblBuilder $ublBuilder;
    private CiiBuilder $ciiBuilder;
    private ZugferdPdfBuilder $pdfBuilder;
    private Validator $validator;
    private Parser $parser;
    private Storage $storage;
    private InvoiceRepository $repo;

    public function __construct(
        ?UblBuilder $ublBuilder = null,
        ?CiiBuilder $ciiBuilder = null,
        ?ZugferdPdfBuilder $pdfBuilder = null,
        ?Validator $validator = null,
        ?Parser $parser = null,
        ?Storage $storage = null,
        ?InvoiceRepository $repo = null
    ) {
        $this->ublBuilder = $ublBuilder ?? new UblBuilder();
        $this->ciiBuilder = $ciiBuilder ?? new CiiBuilder();
        $this->pdfBuilder = $pdfBuilder ?? new ZugferdPdfBuilder( $this->ciiBuilder );
        $this->validator  = $validator  ?? new Validator();
        $this->parser     = $parser     ?? new Parser();
        $this->storage    = $storage    ?? new Storage( self::uploadDir() );
        $this->repo       = $repo       ?? new InvoiceRepository( 'outgoing' );
    }

    public static function supportedFormats(): array
    {
        return [
            self::FORMAT_UBL         => __( 'XRechnung UBL', 'itdatex-e-invoice-output' ),
            self::FORMAT_CII         => __( 'ZUGFeRD / XRechnung CII (EN 16931)', 'itdatex-e-invoice-output' ),
            self::FORMAT_ZUGFERD_PDF => __( 'ZUGFeRD-PDF (hybride PDF/A mit CII)', 'itdatex-e-invoice-output' ),
        ];
    }

    /**
     * Erzeugt eine XRechnung-UBL-Datei aus dem Draft und legt einen
     * DB-Eintrag an.
     *
     * @return array{result:string, invoice_id:int|null, message?:string, file_path?:string}
     */
    public function issue( InvoiceDraft $draft, string $actor = '', string $format = self::FORMAT_UBL ): array
    {
        if ( $actor === '' ) {
            $actor = function_exists( 'wp_get_current_user' ) && wp_get_current_user()->ID
                ? ( wp_get_current_user()->user_login ?: 'system' )
                : 'system';
        }
        if ( ! array_key_exists( $format, self::supportedFormats() ) ) {
            return [ 'result' => self::RESULT_FAILED, 'invoice_id' => null,
                'message' => sprintf( 'unsupported_format:%s', $format ) ];
        }

        try {
            // 1) Output bauen (dispatched nach Format)
            $bytes = match ( $format ) {
                self::FORMAT_CII         => $this->ciiBuilder->build( $draft ),
                self::FORMAT_ZUGFERD_PDF => $this->pdfBuilder->build( $draft ),
                default                  => $this->ublBuilder->build( $draft ),
            };
            $ext = $format === self::FORMAT_ZUGFERD_PDF ? '.pdf' : '.xml';

            // 2) Temporaer ablegen, damit Storage + Validator gemeinsam darauf zugreifen
            $tmp = tempnam( sys_get_temp_dir(), 'ausrg_' ) . $ext;
            file_put_contents( $tmp, $bytes );

            try {
                // 3) Duplikat-Check via SHA-256
                $sha256   = (string) hash_file( 'sha256', $tmp );
                $existing = $this->repo->findFileBySha( $sha256 );
                if ( $existing ) {
                    $existingInvoice = $this->repo->findInvoiceByFileId( (int) $existing->id );
                    return [
                        'result'     => self::RESULT_DUPLICATE,
                        'invoice_id' => $existingInvoice ? (int) $existingInvoice->id : null,
                        'message'    => 'sha256_match',
                    ];
                }

                // 4) Parse + validate (gleicher Pfad wie Eingang — symmetrisch)
                $invoiceData = $this->parser->parse( $tmp );
                $report      = $this->validator->validate( $tmp );

                /**
                 * Symmetrische Hooks zum Eingangs-Plugin — Add-ons koennen sich
                 * an Ausgangs-Rechnungen genauso aufhaengen wie an Eingangs-.
                 *
                 * @param string                                  $tmpPath     Pfad zur frisch gebauten XML.
                 * @param \Itdatex\EInvoiceCore\ValidationReport $report
                 * @param \Itdatex\EInvoiceCore\InvoiceData      $invoiceData
                 */
                do_action( 'ausrg_after_validate', $tmp, $report, $invoiceData );

                // 5) Persistieren (Hash-Kette ueber beide Richtungen — gemeinsame Tabelle)
                $prevChain = $this->repo->lastChainHash();
                $fileData  = $this->storage->persist( $tmp, self::filename( $draft, $format ), $prevChain );
                $fileId    = $this->repo->insertFile( $fileData, 'issued' );
                if ( $fileId === 0 ) {
                    return [ 'result' => self::RESULT_FAILED, 'invoice_id' => null, 'message' => 'file_insert_failed' ];
                }

                // Vollstaendige Draft-Daten in raw_meta persistieren:
                //  - Buyer fuer Prefill-Uebernahme in Folgerechnungen
                //  - Lines + Zahlungsdaten, damit ein Storno-Draft rekonstruiert
                //    werden kann, ohne die XML-Datei parsen zu muessen
                //  - typeCode + preceding fuer Anzeige der Rechnungs-Beziehungen
                $rawMeta = (string) wp_json_encode( [
                    'buyer'   => self::buyerToFormShape( $draft->buyer ),
                    'lines'   => self::linesToArray( $draft->lines ),
                    'invoice' => [
                        'currency'         => $draft->currency,
                        'buyer_reference'  => $draft->buyerReference,
                        'note'             => $draft->note,
                        'due_date'         => $draft->dueDate?->format( 'Y-m-d' ),
                        'delivery_date'    => $draft->deliveryDate?->format( 'Y-m-d' ),
                        'payment_terms'    => $draft->paymentTerms,
                        'payment_iban'     => $draft->paymentIban,
                        'payment_bic'      => $draft->paymentBic,
                        'type_code'        => $draft->typeCode,
                        'preceding_number' => $draft->precedingInvoiceNumber,
                        'preceding_date'   => $draft->precedingIssueDate?->format( 'Y-m-d' ),
                    ],
                ] );
                $invoiceId = $this->repo->insertInvoice( $fileId, $invoiceData, $report, $rawMeta );
                if ( $invoiceId === 0 ) {
                    return [ 'result' => self::RESULT_FAILED, 'invoice_id' => null, 'message' => 'invoice_insert_failed' ];
                }

                $this->repo->appendAudit(
                    $invoiceId,
                    $actor,
                    'issued',
                    sprintf( 'format=%s, errors=%d, warnings=%d',
                        $invoiceData->format->value,
                        $report->errorCount(),
                        $report->warningCount()
                    )
                );

                /** @see ausrg_after_validate fuer Signatur */
                do_action( 'ausrg_after_invoice_insert', $invoiceId, $invoiceData, $report );

                return [
                    'result'     => self::RESULT_ISSUED,
                    'invoice_id' => $invoiceId,
                    'file_path'  => $fileData['full_path'],
                ];
            } finally {
                wp_delete_file( $tmp );
            }

        } catch ( \Throwable $e ) {
            return [ 'result' => self::RESULT_FAILED, 'invoice_id' => null, 'message' => $e->getMessage() ];
        }
    }

    private static function uploadDir(): string
    {
        // Gemeinsamer Speicher mit dem Rechnungseingang (User-Entscheidung
        // bei Migrations-Planung). Existiert die Option nicht, faellt der
        // Storage selbst auf die Pflichtpruefung zurueck.
        return (string) get_option( 'reing_upload_dir', '' );
    }

    private static function filename( InvoiceDraft $draft, string $format ): string
    {
        $safe = preg_replace( '/[^A-Za-z0-9._-]/', '_', $draft->invoiceNumber ) ?? 'invoice';
        $ext  = $format === self::FORMAT_ZUGFERD_PDF ? '.pdf' : '.xml';
        return $safe . $ext;
    }

    /**
     * @param \Itdatex\EInvoiceCore\InvoiceLineDraft[] $lines
     * @return array<int, array<string, mixed>>
     */
    private static function linesToArray( array $lines ): array
    {
        $out = [];
        foreach ( $lines as $l ) {
            $out[] = [
                'name'                 => $l->name,
                'quantity'             => $l->quantity,
                'unit_price'           => $l->unitPrice,
                'unit_code'            => $l->unitCode,
                'vat_rate'             => $l->vatRate,
                'vat_category'         => $l->vatCategory,
                'description'          => $l->description,
                'tax_exemption_reason' => $l->taxExemptionReason,
            ];
        }
        return $out;
    }

    /**
     * Wandelt eine InvoiceParty in die Snake-Case-Struktur, die das
     * Create-Formular erwartet (`buyer[postal_code]` etc.). Adresse
     * wird zu einer newline-joined String, weil das textarea eine
     * Zeile pro Adress-Bestandteil bevorzugt.
     *
     * @return array<string, string>
     */
    private static function buyerToFormShape( \Itdatex\EInvoiceCore\InvoiceParty $p ): array
    {
        return [
            'name'                      => $p->name,
            'vat_number'                => (string) ( $p->vatNumber ?? '' ),
            'address'                   => implode( "\n", $p->addressLines ),
            'city'                      => (string) ( $p->city ?? '' ),
            'postal_code'               => (string) ( $p->postalCode ?? '' ),
            'country_code'              => (string) ( $p->countryCode ?? '' ),
            'contact_name'              => (string) ( $p->contactName ?? '' ),
            'contact_email'             => (string) ( $p->contactEmail ?? '' ),
            'contact_phone'             => (string) ( $p->contactPhone ?? '' ),
            'electronic_address_scheme' => (string) ( $p->electronicAddressScheme ?? '' ),
            'electronic_address_value'  => (string) ( $p->electronicAddressValue ?? '' ),
        ];
    }
}
