<?php

declare(strict_types=1);

namespace CloudXM\NFe\Services;

use CloudXM\NFe\Config\Configuration;
use CloudXM\NFe\DTOs\IssueInvoiceRequest;
use CloudXM\NFe\DTOs\ClientData;
use CloudXM\NFe\DTOs\AddressData;
use CloudXM\NFe\DTOs\FederalTaxation;
use CloudXM\SDK\Support\HasLogger;
use WC_Order;
use WC_Order_Item_Product;

/**
 * Order Mapper V2
 * 
 * Maps WooCommerce orders to v2 API request format
 * CRITICAL: Uses municipal_service_code and national_service_code (service_code removed in v2)
 */
class OrderMapperV2
{
    use HasLogger;

    public function __construct(
        private Configuration $config,
        private TenantConfigCache $tenantConfigCache
    ) {}

    /**
     * Map WooCommerce order to IssueInvoiceRequest DTO
     *
     * @param WC_Order $order
     * @param int $emissionAttempt Emission attempt number for idempotency
     * @param string|null $issueMode 'immediate', 'scheduled', or null for server default
     * @param string|null $scheduledAt ISO 8601 datetime when issue_mode is 'scheduled'
     * @param string|null $timezone Timezone for scheduled_at
     * @return IssueInvoiceRequest
     * @throws \RuntimeException if tenant config unavailable
     */
    public function mapOrderToInvoiceRequest(
        WC_Order $order,
        int $emissionAttempt = 1,
        ?string $issueMode = null,
        ?string $scheduledAt = null,
        ?string $timezone = null
    ): IssueInvoiceRequest
    {
        // Get tenant config for defaults
        try {
            $tenantConfig = $this->tenantConfigCache->get();
        } catch (\Exception $e) {
            $this->log()->error('nfe.mapper.tenant_config_failed', [
                'order_id' => $order->get_id(),
                'error' => $e->getMessage()
            ]);
            throw new \RuntimeException('Failed to get tenant configuration: ' . $e->getMessage());
        }

        $this->log()->info('nfe.mapper.mapping_order', [
            'order_id' => $order->get_id(),
            'order_number' => $order->get_order_number()
        ]);

        $municipalityCode = $this->config->get('municipality_code') ?: $tenantConfig->nfse->municipality_code;
        if (empty($municipalityCode)) {
            $this->log()->error('nfe.mapper.missing_municipality_code', [
                'order_id' => $order->get_id(),
                'order_number' => $order->get_order_number()
            ]);
            throw new \RuntimeException('Municipality code is required for NFSe issuance. Please configure it in plugin settings.');
        }

        return new IssueInvoiceRequest(
            order_number: (string) $order->get_order_number(),
            client: $this->mapClient($order),
            description: $this->buildDescription($order),
            quantity: 1.0,
            unit_price: (float) $order->get_total(),
            municipality_code: $municipalityCode,
            debug: $this->shouldEnableDebug(),
            municipal_service_code: $this->resolveMunicipalServiceCode($order, $tenantConfig->nfse),
            national_service_code: $this->resolveNationalServiceCode($order, $tenantConfig->nfse),
            nbs_code: $this->resolveNbsCode($tenantConfig->nfse),
            tax_classification: $this->resolveTaxClassification($tenantConfig->nfse),
            iss_rate: $this->resolveIssRate($order, $tenantConfig->nfse),
            internal_contributor_code: $this->config->get('internal_contributor_code'),
            complementary_information: $this->parseComplementaryInfo($order, $tenantConfig->nfse),
            federal_taxation: $this->mapFederalTaxation($order),
            idempotency_key: $this->generateIdempotencyKey($order, $emissionAttempt),
            cnpj_issuer: $this->resolveCnpjIssuer(),
            issue_mode: $issueMode,
            scheduled_at: $scheduledAt,
            timezone: $timezone,
        );
    }

    /**
     * Generate idempotency key for invoice emission
     *
     * Format: woo_order_{orderId}_attempt_{attempt}_{hash}
     *
     * The attempt number allows re-issuing after cancellation:
     * - First emission: attempt_1
     * - After cancel + reissue: attempt_2
     *
     * The hash ensures uniqueness across tenants and prevents
     * key collision if same order number exists in different systems.
     *
     * @param WC_Order $order
     * @param int $emissionAttempt Emission attempt number (starts at 1)
     * @return string
     */
    private function generateIdempotencyKey(WC_Order $order, int $emissionAttempt): string
    {
        $orderId = $order->get_id();
        $orderNumber = $order->get_order_number();

        // Get tenant API key as salt for hash
        // This ensures uniqueness across tenants and prevents key collision
        $tenantKey = $this->config->get('api_key', '');

        // CRITICAL: Include timestamp to ensure uniqueness even when emission_attempt is reused
        // This prevents conflicts with old cancelled invoices when cache is stale
        $timestamp = time();

        // Generate hash of order ID + order number + tenant key + timestamp
        // This creates a UNIQUE identifier for each emission attempt
        $hash = substr(md5("{$orderId}_{$orderNumber}_{$tenantKey}_{$timestamp}"), 0, 8);

        $key = "woo_order_{$orderNumber}_attempt_{$emissionAttempt}_{$timestamp}_{$hash}";

        $this->log()->info('nfe.mapper.idempotency_key_generated', [
            'order_id' => $orderId,
            'order_number' => $orderNumber,
            'emission_attempt' => $emissionAttempt,
            'timestamp' => $timestamp,
            'idempotency_key' => $key,
        ]);

        return $key;
    }

    /**
     * Map client data from order
     */
    private function mapClient(WC_Order $order): ClientData
    {
        $billing = $order->get_address('billing');

        // Get email - mandatory field for NFSe
        $email = $order->get_billing_email();
        if (empty($email)) {
            $this->log()->warning('nfe.missing_billing_email', [
                'order_id' => $order->get_id(),
            ]);
            throw new \RuntimeException('Customer email is required for NFSe issuance. Please ensure the billing email is set for this order.');
        }

        // Get document (CPF/CNPJ) - mandatory field
        $document = $this->getDocument($order);
        if (empty($document)) {
            throw new \RuntimeException('CPF/CNPJ is required for NFSe issuance');
        }

        // Get client name - mandatory field for NFSe
        $name = $this->getClientName($billing);
        if (empty($name)) {
            throw new \RuntimeException('Customer name is required for NFSe issuance. Please ensure the billing first name, last name, or company is set for this order.');
        }

        // Get phone (fall back to cellphone meta field)
        $phone = $order->get_billing_phone();
        if (empty($phone)) {
            $phone = $order->get_meta('_billing_cellphone');
        }

        // Map address if available
        $address = null;
        if (!empty($billing['address_1'])) {
            $address = $this->mapAddress($billing);
        }

        return new ClientData(
            name: $name,
            document: $document,
            email: $email,
            phone: $phone ?: null,
            address: $address
        );
    }

    /**
     * Map address data from billing
     */
    private function mapAddress(array $billing): AddressData
    {
        return new AddressData(
            street: $billing['address_1'] ?? null,
            number: $billing['number'] ?? null,
            district: $billing['neighborhood'] ?? null,
            city: $billing['city'] ?? null,
            state: $billing['state'] ?? null,
            zipcode: $billing['postcode'] ?? null
        );
    }

    /**
     * Map federal taxation data
     */
    private function mapFederalTaxation(WC_Order $order): ?FederalTaxation
    {
        $piscofins_cst = $this->config->get('piscofins_cst');
        $piscofins_vbc = $this->config->get('piscofins_vbc');
        $piscofins_rate = $this->config->get('piscofins_rate');
        $cofins_rate = $this->config->get('cofins_rate');
        $piscofins_value = $this->config->get('piscofins_value');
        $cofins_value = $this->config->get('cofins_value');
        $piscofins_retention_type = $this->config->get('piscofins_retention_type');
        $social_security_retention = $this->config->get('social_security_retention');
        $income_tax_retention = $this->config->get('income_tax_retention');
        $social_contribution_retention = $this->config->get('social_contribution_retention');

        // IRRF (income_tax_retention) only applies to CNPJ customers
        // CPF customers never have IRRF withheld because they cannot act as withholding agents
        $document = $this->getDocument($order);
        $isCNPJ = strlen($document) === 14;

        // Only send IRRF for CNPJ customers
        if (! $isCNPJ) {
            $income_tax_retention = null;
        }

        // Only create FederalTaxation if at least one field is set
        if (empty($piscofins_cst) && empty($piscofins_vbc) && empty($piscofins_rate) &&
            empty($cofins_rate) && empty($piscofins_value) && empty($cofins_value) &&
            empty($piscofins_retention_type) && empty($social_security_retention) &&
            empty($income_tax_retention) && empty($social_contribution_retention)) {
            return null;
        }

        return new FederalTaxation(
            pis_cofins_cst: $piscofins_cst ?: null,
            pis_cofins_vbc: $piscofins_vbc ? (float) $piscofins_vbc : null,
            pis_cofins_rate: $piscofins_rate ? (float) $piscofins_rate : null,
            cofins_rate: $cofins_rate ? (float) $cofins_rate : null,
            pis_cofins_value: $piscofins_value ? (float) $piscofins_value : null,
            cofins_value: $cofins_value ? (float) $cofins_value : null,
            pis_cofins_retention_type: $piscofins_retention_type ?: null,
            social_security_retention: $social_security_retention ? (float) $social_security_retention : null,
            income_tax_retention: $income_tax_retention ? (float) $income_tax_retention : null,
            social_contribution_retention: $social_contribution_retention ? (float) $social_contribution_retention : null
        );
    }

    /**
     * Resolve municipal service code
     * Priority: Order meta > Config > Tenant config > Legacy service_code migration
     */
    private function resolveMunicipalServiceCode(WC_Order $order, $nfseConfig): ?string
    {
        // Check order meta first
        $orderMeta = $order->get_meta('_cloudxm_nfe_municipal_service_code');
        if (!empty($orderMeta)) {
            return $orderMeta;
        }

        // Check plugin config
        $configValue = $this->config->get('municipal_service_code');
        if (!empty($configValue)) {
            return $configValue;
        }

        // Check tenant config
        if (!empty($nfseConfig->municipal_service_code)) {
            return $nfseConfig->municipal_service_code;
        }

        // Legacy migration: Check old service_code field
        $legacyServiceCode = $order->get_meta('_cloudxm_nfe_service_code');
        if (!empty($legacyServiceCode)) {
            $this->log()->info('nfe.mapper.legacy_service_code_migration', [
                'order_id' => $order->get_id(),
                'legacy_service_code' => $legacyServiceCode
            ]);
            return $legacyServiceCode;
        }

        return null;
    }

    /**
     * Resolve national service code
     * Priority: Order meta > Config > Tenant config > Legacy service_code migration
     */
    private function resolveNationalServiceCode(WC_Order $order, $nfseConfig): ?string
    {
        // Check order meta first
        $orderMeta = $order->get_meta('_cloudxm_nfe_national_service_code');
        if (!empty($orderMeta)) {
            return $orderMeta;
        }

        // Check plugin config
        $configValue = $this->config->get('national_service_code');
        if (!empty($configValue)) {
            return $configValue;
        }

        // Check tenant config
        if (!empty($nfseConfig->national_service_code)) {
            return $nfseConfig->national_service_code;
        }

        // Legacy migration: Check old service_code field
        $legacyServiceCode = $order->get_meta('_cloudxm_nfe_service_code');
        if (!empty($legacyServiceCode)) {
            $this->log()->info('nfe.mapper.legacy_service_code_migration', [
                'order_id' => $order->get_id(),
                'legacy_service_code' => $legacyServiceCode
            ]);
            return $legacyServiceCode;
        }

        return null;
    }

    /**
     * Resolve NBS code from tenant config
     */
    private function resolveNbsCode($nfseConfig): ?string
    {
        $configValue = $this->config->get('nbs_code');
        if (!empty($configValue)) {
            return $configValue;
        }

        return $nfseConfig->nbs_code ?? null;
    }

    /**
     * Resolve tax classification from tenant config
     */
    private function resolveTaxClassification($nfseConfig): ?string
    {
        $configValue = $this->config->get('tax_classification');
        if (!empty($configValue)) {
            return $configValue;
        }

        return $nfseConfig->tax_classification ?? null;
    }

    /**
     * Resolve ISS rate
     * Priority: Order meta > Config > Tenant config
     */
    private function resolveIssRate(WC_Order $order, $nfseConfig): ?float
    {
        // Check order meta first
        $orderMeta = $order->get_meta('_cloudxm_nfe_iss_rate');
        if (!empty($orderMeta)) {
            return (float) $orderMeta;
        }

        // Check plugin config
        $configValue = $this->config->get('iss_rate');
        if (!empty($configValue)) {
            return (float) $configValue;
        }

        // Tenant config doesn't have ISS rate default
        return null;
    }

    /**
     * Build description from order items
     */
    private function buildDescription(WC_Order $order): string
    {
        if ((bool) $this->config->get('enable_custom_description', false)) {
            $customDescription = $order->get_meta('_cloudxm_nfe_description');
            if (!empty($customDescription)) {
                return $customDescription;
            }
        }

        $descriptions = [];
        $items = $order->get_items();

        foreach ($items as $item) {
            if (!$item instanceof WC_Order_Item_Product) {
                continue;
            }

            $descriptions[] = $item->get_name() . ' x' . $item->get_quantity();
        }

        if ($order->get_shipping_total() > 0) {
            $descriptions[] = __('Shipping', 'cloudxm-nfe');
        }

        return implode(', ', $descriptions);
    }

    /**
     * Get client name from billing data
     */
    private function getClientName(array $billing): string
    {
        $company = $billing['company'] ?? '';
        if (!empty($company)) {
            return $company;
        }

        $firstName = $billing['first_name'] ?? '';
        $lastName = $billing['last_name'] ?? '';

        return trim($firstName . ' ' . $lastName);
    }

    /**
     * Get document (CPF/CNPJ) from order
     */
    private function getDocument(WC_Order $order): string
    {
        $personType = $order->get_meta('_billing_persontype');

        if ($personType === '2') {
            $document = $order->get_meta('_billing_cnpj');
        } elseif ($personType === '1') {
            $document = $order->get_meta('_billing_cpf');
        } else {
            $document = $order->get_meta('_billing_cpf') ?: $order->get_meta('_billing_cnpj');
        }

        // Clean document (remove non-numeric characters)
        if (!empty($document)) {
            $document = preg_replace('/[^0-9]/', '', $document);
        }

        return $document ?: '';
    }

    /**
     * Resolve the CNPJ of the issuer company.
     *
     * Returns null when not configured — the server will fall back to the
     * tenant's primary / first company automatically.
     */
    private function resolveCnpjIssuer(): ?string
    {
        $cnpj = $this->config->getSelectedCompanyCnpj();
        if ($cnpj !== '' && $cnpj !== null) {
            $cnpj = preg_replace('/[^0-9]/', '', (string) $cnpj);
            return $cnpj !== '' ? $cnpj : null;
        }

        // When no company is explicitly selected, omit cnpj_issuer entirely.
        // The API defaults to the tenant's primary company.
        return null;
    }

    /**
     * Check if debug mode should be enabled
     */
    private function shouldEnableDebug(): bool
    {
        $environment = $this->config->get('environment');
        $debugMode = $this->config->get('debug_mode');

        return $environment === 'homologation' && $debugMode === true;
    }

    /**
     * Parse complementary info with order data placeholders
     * Available placeholders: {order_id}, {order_number}, {order_date}, {customer_name},
     * {customer_email}, {customer_phone}, {billing_city}, {billing_state}, {order_total}
     */
    private function parseComplementaryInfo(WC_Order $order, \CloudXM\NFe\DTOs\NfseConfig $nfseConfig): string
    {
        // Priority: local config > tenant config > hardcoded default
        $info = $this->config->get('complementary_information', '');

        if (empty($info)) {
            $info = $nfseConfig->complementary_information ?? '';
        }

        if (empty($info)) {
            $info = 'Pedido {order_number} - {customer_name}';
        }

        $billing = $order->get_address('billing');
        $firstName = $billing['first_name'] ?? '';
        $lastName = $billing['last_name'] ?? '';
        $customerName = trim($firstName . ' ' . $lastName);
        if (empty($customerName) && !empty($billing['company'])) {
            $customerName = $billing['company'];
        }

        $placeholders = [
            '{order_id}' => $order->get_id(),
            '{order_number}' => $order->get_order_number(),
            '{order_date}' => $order->get_date_created()->date_i18n('d/m/Y H:i'),
            '{customer_name}' => $customerName,
            '{customer_email}' => $order->get_billing_email(),
            '{customer_phone}' => $order->get_billing_phone(),
            '{billing_city}' => $billing['city'] ?? '',
            '{billing_state}' => $billing['state'] ?? '',
            '{order_total}' => wc_price($order->get_total()),
            '{billing_address}' => ($billing['address_1'] ?? '') . ' ' . ($billing['number'] ?? ''),
        ];

        return str_replace(array_keys($placeholders), array_values($placeholders), $info);
    }
}
