<?php
/** Builds VeriFactu data exclusively from immutable WooCommerce fiscal snapshots. */
if (!defined('ABSPATH')) {
    exit;
}

class VerifactuInvoiceData
{
    const EPSILON = 0.0051;
    const MAX_RECONCILIATION_RESIDUAL = 0.02;

    /**
     * Validates the fiscal snapshot before a WooCommerce order consumes a
     * fiscal number. This keeps numbering fail-closed: incomplete recipient
     * data or ambiguous tax classification cannot create an unusable invoice.
     */
    public static function validateInvoiceSnapshotForIssuance(array $snapshot)
    {
        self::assertSnapshotCurrency($snapshot);
        $recipient = self::buildRecipient((array) ($snapshot['recipient'] ?? array()));
        $totalExcl = self::roundAmount($snapshot['total_excl'] ?? 0);
        $totalIncl = self::roundAmount($snapshot['total_incl'] ?? 0);
        $totalTax = self::roundAmount($snapshot['total_tax'] ?? 0);
        if ($totalIncl < -self::EPSILON) {
            throw new Exception('Una factura ordinaria no puede registrarse con importe total negativo.');
        }
        $invoiceType = self::resolveOrdinaryInvoiceType($recipient, $totalIncl);
        $rows = self::reconcileAndClassify(
            isset($snapshot['tax_rows']) ? (array) $snapshot['tax_rows'] : array(),
            $totalExcl,
            $totalTax,
            (array) ($snapshot['recipient'] ?? array()),
            array('is_rebu' => self::isRebu(isset($snapshot['fiscal_note']) ? $snapshot['fiscal_note'] : ''))
        );
        return array('invoice_type' => $invoiceType, 'tax_rows' => $rows);
    }

    /** Validate a refund snapshot before assigning its rectificative number. */
    public static function validateCreditSnapshotForIssuance(array $snapshot)
    {
        self::assertSnapshotCurrency($snapshot);
        self::buildRecipient((array) ($snapshot['recipient'] ?? array()));
        $totalExcl = abs(self::roundAmount($snapshot['total_excl'] ?? 0));
        $totalIncl = abs(self::roundAmount($snapshot['total_incl'] ?? 0));
        $totalTax = abs(self::roundAmount($snapshot['total_tax'] ?? 0));
        if ($totalIncl <= self::EPSILON) {
            throw new Exception('El abono no contiene un importe fiscal positivo que pueda rectificarse.');
        }
        self::reconcileAndClassify(
            isset($snapshot['tax_rows']) ? (array) $snapshot['tax_rows'] : array(),
            $totalExcl,
            $totalTax,
            (array) ($snapshot['recipient'] ?? array()),
            array('is_rebu' => self::isRebu(isset($snapshot['fiscal_note']) ? $snapshot['fiscal_note'] : ''))
        );
        return true;
    }

    public static function fromInvoice($order, $invoice)
    {
        $order = self::requireOrder($order);
        $invoice = VerifactuDocumentIdentity::getInvoice($order, $invoice);
        $snapshot = $invoice->snapshot;
        self::assertSnapshotCurrency($snapshot);
        $identity = VerifactuDocumentIdentity::getInvoiceIdentity($order, $invoice);
        $recipient = self::buildRecipient((array) $snapshot['recipient']);
        $totalExcl = self::roundAmount($snapshot['total_excl']);
        $totalIncl = self::roundAmount($snapshot['total_incl']);
        $totalTax = self::roundAmount($snapshot['total_tax']);
        if ($totalIncl < -self::EPSILON) {
            throw new Exception('Una factura ordinaria no puede registrarse con importe total negativo.');
        }
        $invoiceType = self::resolveOrdinaryInvoiceType($recipient, $totalIncl);
        $rows = self::reconcileAndClassify(
            isset($snapshot['tax_rows']) ? (array) $snapshot['tax_rows'] : array(),
            $totalExcl,
            $totalTax,
            (array) $snapshot['recipient'],
            array('is_rebu' => self::isRebu(isset($snapshot['fiscal_note']) ? $snapshot['fiscal_note'] : ''))
        );

        return array(
            'id_shop' => function_exists('get_current_blog_id') ? (int) get_current_blog_id() : 1,
            'id_order' => (int) $order->get_id(),
            'id_order_invoice' => (int) $invoice->id,
            'id_order_slip' => 0,
            'document_kind' => 'INVOICE',
            'identity' => $identity,
            'invoice_type' => $invoiceType,
            'rectification_type' => null,
            'original_identities' => array(),
            'description' => self::truncate('Factura WooCommerce ' . $identity['number'], 500),
            'recipient' => $invoiceType === 'F1' ? $recipient : null,
            'tax_rows' => $rows,
            'tax_total' => self::formatAmount($totalTax),
            'total' => self::formatAmount($totalIncl),
            'total_excl' => self::formatAmount($totalExcl),
            'commercial_reference' => self::truncate('WC-O' . (int) $order->get_id() . '-I' . (int) $invoice->id, 60),
        );
    }

    public static function fromCreditSlip($order, $creditSlip, array $options = array())
    {
        $order = self::requireOrder($order);
        $refund = $creditSlip instanceof WC_Order_Refund
            ? $creditSlip
            : VerifactuDocumentIdentity::getCreditSlip($order, (int) $creditSlip);
        $creditDocument = VerifactuDocumentIdentity::getCreditDocument((int) $refund->get_id());
        $snapshot = $creditDocument->snapshot;
        self::assertSnapshotCurrency($snapshot);

        $sourceInvoices = VerifactuDocumentIdentity::getCreditSlipSourceInvoices($order, $refund);
        $originalIdentities = array();
        foreach ($sourceInvoices as $sourceInvoice) {
            $originalIdentities[] = VerifactuDocumentIdentity::getInvoiceIdentity($order, $sourceInvoice);
        }

        $sourceTypes = array();
        if (!empty($options['source_invoice_types']) && is_array($options['source_invoice_types'])) {
            foreach ($sourceInvoices as $sourceInvoice) {
                $id = (int) $sourceInvoice->id;
                if (empty($options['source_invoice_types'][$id])) {
                    throw new Exception('No consta el tipo VeriFactu aceptado de la factura original ' . $id . '.');
                }
                $sourceTypes[] = strtoupper((string) $options['source_invoice_types'][$id]);
            }
        } else {
            $sourceRecipient = self::buildRecipient((array) $sourceInvoices[0]->snapshot['recipient']);
            $sourceTypes[] = self::resolveOrdinaryInvoiceType($sourceRecipient, (float) $sourceInvoices[0]->total_paid_tax_incl);
        }
        $sourceTypes = array_values(array_unique($sourceTypes));
        if (count($sourceTypes) !== 1 || !in_array($sourceTypes[0], array('F1', 'F2'), true)) {
            throw new Exception('La rectificativa no puede determinar de forma inequivoca el tipo de la factura original.');
        }
        $ordinaryType = $sourceTypes[0];
        $rectificativeType = isset($options['invoice_type']) && trim((string) $options['invoice_type']) !== ''
            ? strtoupper(trim((string) $options['invoice_type']))
            : ($ordinaryType === 'F2' ? 'R5' : VerifactuConfig::getDefaultRectificativeType());
        if ($ordinaryType === 'F2') {
            $rectificativeType = 'R5';
        }
        if (!in_array($rectificativeType, array('R1','R2','R3','R4','R5'), true)) {
            throw new Exception('El tipo de factura rectificativa debe ser R1, R2, R3, R4 o R5.');
        }
        if ($ordinaryType !== 'F2' && $rectificativeType === 'R5') {
            throw new Exception('R5 solo puede utilizarse para rectificar facturas simplificadas.');
        }

        $totalExcl = abs(self::roundAmount($snapshot['total_excl']));
        $totalIncl = abs(self::roundAmount($snapshot['total_incl']));
        $totalTax = abs(self::roundAmount($snapshot['total_tax']));
        if ($totalIncl <= self::EPSILON) {
            throw new Exception('El abono no contiene un importe fiscal positivo que pueda rectificarse.');
        }
        $recipient = self::buildRecipient((array) $snapshot['recipient']);
        $rows = self::reconcileAndClassify(
            isset($snapshot['tax_rows']) ? (array) $snapshot['tax_rows'] : array(),
            $totalExcl,
            $totalTax,
            (array) $snapshot['recipient'],
            array('is_rebu' => self::isRebu(isset($snapshot['fiscal_note']) ? $snapshot['fiscal_note'] : ''))
        );
        foreach ($rows as &$row) {
            $row['base'] = self::formatAmount(-abs((float) $row['base']));
            $row['tax'] = self::formatAmount(-abs((float) $row['tax']));
            if (isset($row['recargo_tax'])) {
                $row['recargo_tax'] = self::formatAmount(-abs((float) $row['recargo_tax']));
            }
        }
        unset($row);
        $identity = VerifactuDocumentIdentity::getCreditSlipIdentity($order, (int) $refund->get_id());

        return array(
            'id_shop' => function_exists('get_current_blog_id') ? (int) get_current_blog_id() : 1,
            'id_order' => (int) $order->get_id(),
            'id_order_invoice' => count($sourceInvoices) === 1 ? (int) $sourceInvoices[0]->id : 0,
            'id_order_slip' => (int) $refund->get_id(),
            'document_kind' => 'CREDIT_SLIP',
            'identity' => $identity,
            'invoice_type' => $rectificativeType,
            'rectification_type' => 'I',
            'original_identities' => $originalIdentities,
            'description' => self::truncate(
                !empty($options['description']) ? (string) $options['description'] : 'Rectificacion por diferencias mediante abono WooCommerce ' . $identity['number'],
                500
            ),
            'recipient' => $ordinaryType === 'F1' ? $recipient : null,
            'tax_rows' => $rows,
            'tax_total' => self::formatAmount(-abs($totalTax)),
            'total' => self::formatAmount(-abs($totalIncl)),
            'total_excl' => self::formatAmount(-abs($totalExcl)),
            'commercial_reference' => self::truncate('WC-O' . (int) $order->get_id() . '-R' . (int) $refund->get_id(), 60),
            'original_invoice_type' => $ordinaryType,
        );
    }

    private static function reconcileAndClassify(array $rawRows, $expectedBase, $expectedTax, array $address, array $fiscalContext)
    {
        $groups = array();
        foreach ($rawRows as $row) {
            $rate = round((float) ($row['rate'] ?? 0), 2);
            $idTax = (int) ($row['id_tax'] ?? 0);
            $key = $idTax . '|' . number_format($rate, 2, '.', '');
            if (!isset($groups[$key])) {
                $groups[$key] = array('id_tax'=>$idTax,'rate'=>$rate,'base'=>0.0,'tax'=>0.0,'sources'=>array());
            }
            $groups[$key]['base'] += (float) ($row['base'] ?? 0);
            $groups[$key]['tax'] += (float) ($row['tax'] ?? 0);
            $source = isset($row['source']) ? (string) $row['source'] : '';
            if ($source !== '' && !in_array($source, $groups[$key]['sources'], true)) {
                $groups[$key]['sources'][] = $source;
            }
        }
        if (empty($groups)) {
            throw new Exception('WooCommerce no conserva un desglose fiscal suficiente para este documento. No se generara un registro ambiguo.');
        }
        self::reconcileGroups($groups, (float) $expectedBase, (float) $expectedTax);
        $result = array();
        foreach ($groups as $group) {
            $base = self::roundAmount($group['base']);
            $tax = self::roundAmount($group['tax']);
            if (abs($base) < self::EPSILON && abs($tax) < self::EPSILON) {
                continue;
            }
            $treatment = self::resolveTaxTreatment($group, $address, $fiscalContext);
            $reportedRate = array_key_exists('reported_rate', $treatment) ? (float) $treatment['reported_rate'] : (float) $group['rate'];
            $reportedBase = array_key_exists('reported_base', $treatment) ? (float) $treatment['reported_base'] : $base;
            $reportedTax = array_key_exists('reported_tax', $treatment) ? (float) $treatment['reported_tax'] : $tax;
            if (!empty($treatment['recargo_rate'])) {
                throw new Exception('El recargo de equivalencia requiere un desglose separado de IVA y recargo. El envio queda bloqueado si WooCommerce no lo conserva de forma inequivoca.');
            }
            $result[] = array(
                'impuesto' => $treatment['impuesto'],
                'clave_regimen' => $treatment['clave_regimen'],
                'calificacion' => $treatment['calificacion'] ?? null,
                'exenta' => $treatment['exenta'] ?? null,
                'rate' => self::formatRate($reportedRate),
                'base' => self::formatAmount($reportedBase),
                'tax' => self::formatAmount($reportedTax),
                'source_base' => self::formatAmount($base),
                'source_tax' => self::formatAmount($tax),
                'source' => implode(',', $group['sources']),
                'id_tax' => $group['id_tax'],
            );
        }
        if (empty($result) || count($result) > 12) {
            throw new Exception('El desglose fiscal debe contener entre 1 y 12 lineas.');
        }
        $baseCheck = self::roundAmount(self::sum($result, 'source_base'));
        $taxCheck = self::roundAmount(self::sum($result, 'source_tax'));
        if (abs($baseCheck - self::roundAmount($expectedBase)) > 0.02 || abs($taxCheck - self::roundAmount($expectedTax)) > 0.02) {
            throw new Exception('El desglose fiscal de WooCommerce no coincide con los totales inmutables del documento.');
        }
        return $result;
    }

    private static function reconcileGroups(array &$groups, $targetBase, $targetTax)
    {
        $currentBase = 0.0; $currentTax = 0.0;
        foreach ($groups as $group) { $currentBase += $group['base']; $currentTax += $group['tax']; }
        $baseDelta = $targetBase - $currentBase;
        $taxDelta = $targetTax - $currentTax;
        if (abs($baseDelta) < self::EPSILON && abs($taxDelta) < self::EPSILON) { return; }
        $keys = array_keys($groups);
        $weight = 0.0;
        foreach ($groups as $group) { $weight += abs($group['base']); }
        if ($weight < self::EPSILON) {
            throw new Exception('No se pueden conciliar descuentos sobre bases imponibles nulas.');
        }
        $allocations = array(); $predictedTaxDelta = 0.0;
        foreach ($keys as $key) {
            $allocations[$key] = $baseDelta * (abs($groups[$key]['base']) / $weight);
            $predictedTaxDelta += $allocations[$key] * ($groups[$key]['rate'] / 100);
        }
        $remainingTax = $taxDelta - $predictedTaxDelta;
        if (abs($remainingTax) > self::MAX_RECONCILIATION_RESIDUAL) {
            $sorted = $keys;
            usort($sorted, static function ($a,$b) use ($groups) { return $groups[$a]['rate'] <=> $groups[$b]['rate']; });
            $low = reset($sorted); $high = end($sorted);
            $rateGap = ($groups[$high]['rate'] - $groups[$low]['rate']) / 100;
            if (abs($rateGap) < 0.000001) {
                throw new Exception('Los totales y el desglose tributario de WooCommerce no son conciliables con el tipo impositivo almacenado.');
            }
            $transfer = $remainingTax / $rateGap;
            $allocations[$high] += $transfer; $allocations[$low] -= $transfer;
        }
        foreach ($keys as $key) {
            $newBase = $groups[$key]['base'] + $allocations[$key];
            if (($groups[$key]['base'] >= 0 && $newBase < -self::MAX_RECONCILIATION_RESIDUAL) || ($groups[$key]['base'] <= 0 && $newBase > self::MAX_RECONCILIATION_RESIDUAL)) {
                throw new Exception('La distribucion de descuentos produciria una base imponible de signo incoherente.');
            }
            $groups[$key]['base'] = $newBase;
            $groups[$key]['tax'] += $allocations[$key] * ($groups[$key]['rate'] / 100);
        }
        $newBase=0.0; $newTax=0.0; $largest=$keys[0];
        foreach ($keys as $key) {
            $newBase += $groups[$key]['base']; $newTax += $groups[$key]['tax'];
            if (abs($groups[$key]['base']) > abs($groups[$largest]['base'])) { $largest=$key; }
        }
        $baseResidual=$targetBase-$newBase; $taxResidual=$targetTax-$newTax;
        if (abs($baseResidual)>self::MAX_RECONCILIATION_RESIDUAL || abs($taxResidual)>self::MAX_RECONCILIATION_RESIDUAL) {
            throw new Exception('La conciliacion de descuentos excede la tolerancia de dos centimos.');
        }
        $groups[$largest]['base'] += $baseResidual; $groups[$largest]['tax'] += $taxResidual;
    }

    private static function resolveTaxTreatment(array $row, array $address, array $fiscalContext)
    {
        if (!empty($fiscalContext['is_rebu']) && abs((float)$row['rate']) < 0.001 && abs((float)$row['tax']) < self::EPSILON) {
            return array('impuesto'=>self::detectSpanishTaxCode($address),'clave_regimen'=>'03','calificacion'=>'S1','reported_rate'=>0.0,'reported_base'=>0.0,'reported_tax'=>0.0);
        }
        $mappings = VerifactuConfig::getTaxMappings();
        $countryIso = strtoupper((string) ($address['country_code'] ?? ''));
        $scope = $countryIso === 'ES' ? 'domestic' : 'foreign';
        $keys = array();
        if (!empty($row['id_tax'])) { $keys[]='rate_id:' . (int)$row['id_tax']; $keys[]='id_tax:' . (int)$row['id_tax']; }
        $keys[]='rate:' . number_format((float)$row['rate'],2,'.','');
        $keys[]='scope:' . $scope; $keys[]='default';
        $mapping=null;
        foreach ($keys as $key) { if (isset($mappings[$key]) && is_array($mappings[$key])) { $mapping=$mappings[$key]; break; } }
        if ($mapping===null) {
            if ($scope==='foreign') {
                throw new Exception('La fiscalidad de un destinatario extranjero no puede deducirse solo de su pais. Configure DD_VF_TAX_MAPPING_JSON.');
            }
            if (abs((float)$row['rate']) < 0.001) {
                throw new Exception('Existe una linea al 0 %. Configure si es exenta, no sujeta, inversion del sujeto pasivo o sujeta al 0 % mediante DD_VF_TAX_MAPPING_JSON.');
            }
            return array('impuesto'=>self::detectSpanishTaxCode($address),'clave_regimen'=>'01','calificacion'=>'S1');
        }
        $taxCode = isset($mapping['impuesto']) ? (string)$mapping['impuesto'] : self::detectSpanishTaxCode($address);
        $regime = isset($mapping['clave_regimen']) ? (string)$mapping['clave_regimen'] : '01';
        $qualification = isset($mapping['calificacion']) ? strtoupper((string)$mapping['calificacion']) : '';
        $exemption = isset($mapping['exenta']) ? strtoupper((string)$mapping['exenta']) : '';
        if (!preg_match('/^(01|02|03|05)$/',$taxCode) || !preg_match('/^[0-9]{2}$/',$regime)) { throw new Exception('El mapa fiscal contiene un codigo de impuesto o regimen no valido.'); }
        if ($exemption!=='') {
            if (!preg_match('/^E[1-6]$/',$exemption)) { throw new Exception('La causa de exencion debe ser E1, E2, E3, E4, E5 o E6.'); }
            if (abs((float)$row['tax']) >= self::EPSILON) { throw new Exception('Una linea exenta no puede contener cuota repercutida.'); }
            return array('impuesto'=>$taxCode,'clave_regimen'=>$regime,'exenta'=>$exemption);
        }
        if (!in_array($qualification,array('S1','S2','N1','N2'),true)) { throw new Exception('La calificacion fiscal debe ser S1, S2, N1 o N2.'); }
        if (in_array($qualification,array('N1','N2'),true) && abs((float)$row['tax']) >= self::EPSILON) { throw new Exception('Una operacion no sujeta no puede contener cuota repercutida.'); }
        $result=array('impuesto'=>$taxCode,'clave_regimen'=>$regime,'calificacion'=>$qualification);
        if (isset($mapping['recargo_rate']) && (float)$mapping['recargo_rate']>0) { $result['recargo_rate']=(float)$mapping['recargo_rate']; }
        return $result;
    }

    private static function buildRecipient(array $address)
    {
        $country = strtoupper(trim((string)($address['country_code'] ?? '')));
        $name = self::truncate((string)($address['name'] ?? ''),120);
        if ($name==='') { throw new Exception('El destinatario no tiene nombre o razon social.'); }
        $taxId=preg_replace('/[^A-Z0-9]/','',strtoupper((string)($address['tax_id'] ?? '')));
        if ($country==='ES') {
            $taxId=VerifactuConfig::normalizeNif($taxId);
            if ($taxId!=='' && !VerifactuConfig::isValidNifLength($taxId)) { throw new Exception('El NIF/DNI del destinatario espanol debe contener 9 caracteres alfanumericos.'); }
            return array('name'=>$name,'identifier_kind'=>$taxId!==''?'NIF':'NONE','identifier'=>$taxId,'country_code'=>'ES','id_type'=>null);
        }
        if (substr($taxId,0,2)===$country) { $taxId=substr($taxId,2); }
        if ($taxId==='' || strlen($taxId)>20) { throw new Exception('El destinatario extranjero necesita un identificador fiscal de hasta 20 caracteres.'); }
        $idType=VerifactuConfig::getForeignIdType();
        return array('name'=>$name,'identifier_kind'=>'IDOtro','identifier'=>$taxId,'country_code'=>$country,'id_type'=>$idType);
    }

    private static function resolveOrdinaryInvoiceType(array $recipient, $total)
    {
        if ($recipient['identifier_kind']!=='NONE') { return 'F1'; }
        if (!VerifactuConfig::allowSimplifiedInvoices()) { throw new Exception('La factura no tiene NIF y la emision F2 esta desactivada.'); }
        if ((float)$total <= VerifactuConfig::getSimplifiedInvoiceLimit()+self::EPSILON) { return 'F2'; }
        throw new Exception('La factura no tiene identificador fiscal y supera el limite F2 configurado (' . self::formatAmount(VerifactuConfig::getSimplifiedInvoiceLimit()) . ' EUR).');
    }

    private static function detectSpanishTaxCode(array $address)
    {
        $postcode=preg_replace('/\s+/','',(string)($address['postcode'] ?? ''));
        $state=strtoupper((string)($address['state'] ?? ''));
        if (preg_match('/^(35|38)/',$postcode) || in_array($state,array('GC','TF','CN'),true)) { return '03'; }
        if (preg_match('/^(51|52)/',$postcode) || in_array($state,array('CE','ML'),true)) { return '02'; }
        return '01';
    }

    private static function isRebu($note)
    {
        $value=strtoupper(remove_accents((string)$note));
        if ($value==='' || strpos($value,'NO REBU')!==false) { return false; }
        foreach (array('REBU','REGIMEN ESPECIAL DE BIENES USADOS','REGIMEN ESPECIAL BIENES USADOS','REGIMEN ESPECIAL DE OBJETOS DE ARTE','REGIMEN ESPECIAL DE ANTIGUEDADES','REGIMEN ESPECIAL DE OBJETOS DE COLECCION') as $marker) {
            if (strpos($value,$marker)!==false) { return true; }
        }
        return false;
    }

    private static function assertSnapshotCurrency(array $snapshot)
    {
        if (strtoupper((string)($snapshot['currency'] ?? ''))!=='EUR') { throw new Exception('El documento fiscal no esta expresado en EUR.'); }
    }

    private static function requireOrder($order)
    {
        if (is_numeric($order)) { $order=wc_get_order((int)$order); }
        if (!($order instanceof WC_Order) || $order instanceof WC_Order_Refund) { throw new Exception('No se ha podido cargar el pedido WooCommerce.'); }
        return $order;
    }

    private static function sum(array $rows,$field) { $sum=0.0; foreach($rows as $row){$sum+=(float)($row[$field]??0);} return $sum; }
    public static function formatAmount($value) { return number_format(self::roundAmount($value),2,'.',''); }
    public static function formatRate($value) { $formatted=number_format(round((float)$value,2),2,'.',''); return rtrim(rtrim($formatted,'0'),'.'); }
    private static function roundAmount($value) { return round((float)$value,2); }
    private static function truncate($value,$maxLength) { $value=trim((string)$value); return function_exists('mb_substr')?mb_substr($value,0,$maxLength,'UTF-8'):substr($value,0,$maxLength); }
}
