<?php

/**
 * Enforcement central de gates productivos para Billing.
 *
 * Reglas:
 * - sandbox: siempre permitido por estos gates; siguen aplicando permisos y validaciones normales.
 * - production/read: requiere ICYLAB_BILLING_PRODUCTION_READ_ENABLED.
 * - production/finance: requiere READ + FINANCE.
 * - production/issue: requiere READ + FINANCE + ISSUE.
 *
 * Los gates no reemplazan permisos de usuario ni validaciones tributarias.
 */

if (!defined('ICYLAB_BILLING_RUNTIME_GATE_VERSION')) {
    define('ICYLAB_BILLING_RUNTIME_GATE_VERSION', '7.1');
}

if (!class_exists('BillingRuntimeGateException')) {
    class BillingRuntimeGateException extends RuntimeException
    {
        private $gateCode;
        private $operation;
        private $environment;

        public function __construct($message, $gateCode, $operation, $environment)
        {
            parent::__construct((string) $message, 423);
            $this->gateCode = (string) $gateCode;
            $this->operation = (string) $operation;
            $this->environment = (string) $environment;
        }

        public function getGateCode()
        {
            return $this->gateCode;
        }

        public function getOperation()
        {
            return $this->operation;
        }

        public function getEnvironment()
        {
            return $this->environment;
        }
    }
}

if (!function_exists('billing_runtime_parse_bool')) {
    function billing_runtime_parse_bool($value, $default = false)
    {
        if (is_bool($value)) {
            return $value;
        }
        if ($value === null || $value === '') {
            return (bool) $default;
        }
        if (is_int($value) || is_float($value)) {
            return (int) $value === 1;
        }
        $normalized = strtolower(trim((string) $value));
        if (in_array($normalized, array('1', 'true', 'yes', 'on', 'enabled'), true)) {
            return true;
        }
        if (in_array($normalized, array('0', 'false', 'no', 'off', 'disabled'), true)) {
            return false;
        }
        return (bool) $default;
    }
}

if (!function_exists('billing_runtime_flag')) {
    function billing_runtime_flag($constantName, $envName, $default = false)
    {
        if (defined($constantName)) {
            return billing_runtime_parse_bool(constant($constantName), $default);
        }
        $value = getenv($envName);
        return billing_runtime_parse_bool($value !== false ? $value : null, $default);
    }
}

if (!function_exists('billing_runtime_environment')) {
    function billing_runtime_environment($environment = null)
    {
        if ($environment === null || trim((string) $environment) === '') {
            if (defined('FACTO_DEFAULT_ENVIRONMENT')) {
                $environment = constant('FACTO_DEFAULT_ENVIRONMENT');
            } elseif (defined('ICYLAB_FACTO_ENVIRONMENT')) {
                $environment = constant('ICYLAB_FACTO_ENVIRONMENT');
            } else {
                $envValue = getenv('ICYLAB_FACTO_ENVIRONMENT');
                $environment = $envValue !== false && trim((string) $envValue) !== '' ? $envValue : 'sandbox';
            }
        }
        if (function_exists('billing_normalize_environment')) {
            return billing_normalize_environment($environment);
        }
        if (function_exists('icylab_facto_normalize_environment')) {
            return icylab_facto_normalize_environment($environment);
        }
        $normalized = strtolower(trim((string) $environment));
        return $normalized === 'production' ? 'production' : 'sandbox';
    }
}

if (!function_exists('billing_runtime_is_placeholder')) {
    function billing_runtime_is_placeholder($value)
    {
        $normalized = strtolower(trim((string)$value));
        if ($normalized === '') {
            return true;
        }
        foreach (array('changeme', 'change_me', 'replace_me', 'reemplazar', 'example', 'tu_', '<', '>') as $needle) {
            if (strpos($normalized, $needle) !== false) {
                return true;
            }
        }
        return false;
    }
}

if (!function_exists('billing_production_facto_config_state')) {
    /**
     * Verifica únicamente presencia y forma de la configuración explícita.
     * Nunca devuelve ni registra credenciales.
     */
    function billing_production_facto_config_state()
    {
        $names = array(
            'api_base_url' => 'ICYLAB_FACTO_PRODUCTION_API_BASE_URL',
            'account_id' => 'ICYLAB_FACTO_PRODUCTION_ACCOUNT_ID',
            'client_id' => 'ICYLAB_FACTO_PRODUCTION_CLIENT_ID',
            'client_secret' => 'ICYLAB_FACTO_PRODUCTION_CLIENT_SECRET',
            'username' => 'ICYLAB_FACTO_PRODUCTION_USERNAME',
            'password' => 'ICYLAB_FACTO_PRODUCTION_PASSWORD',
        );
        $missing = array();
        $placeholders = array();
        $values = array();

        foreach ($names as $field => $name) {
            $value = trim((string)billing_runtime_config_value($name, ''));
            $values[$field] = $value;
            if ($value === '') {
                $missing[] = $field;
            } elseif (billing_runtime_is_placeholder($value)) {
                $placeholders[] = $field;
            }
        }

        $baseUrl = $values['api_base_url'] ?? '';
        $host = $baseUrl !== '' ? parse_url($baseUrl, PHP_URL_HOST) : '';
        $httpsValid = $baseUrl !== ''
            && stripos($baseUrl, 'https://') === 0
            && is_string($host)
            && trim($host) !== '';
        $accountValid = isset($values['account_id']) && preg_match('/^[0-9]+$/', $values['account_id']);

        return array(
            'complete' => empty($missing) && empty($placeholders) && $httpsValid && $accountValid,
            'missing_fields' => $missing,
            'placeholder_fields' => $placeholders,
            'https_valid' => $httpsValid,
            'account_id_valid' => (bool)$accountValid,
            'host' => $httpsValid ? strtolower((string)$host) : '',
        );
    }
}

if (!function_exists('billing_runtime_gate_state')) {
    function billing_runtime_gate_state($environment = null)
    {
        $environment = billing_runtime_environment($environment);
        $read = billing_runtime_flag(
            'ICYLAB_BILLING_PRODUCTION_READ_ENABLED',
            'ICYLAB_BILLING_PRODUCTION_READ_ENABLED',
            false
        );
        $finance = billing_runtime_flag(
            'ICYLAB_BILLING_PRODUCTION_FINANCE_ENABLED',
            'ICYLAB_BILLING_PRODUCTION_FINANCE_ENABLED',
            false
        );
        $issue = billing_runtime_flag(
            'ICYLAB_BILLING_PRODUCTION_ISSUE_ENABLED',
            'ICYLAB_BILLING_PRODUCTION_ISSUE_ENABLED',
            false
        );

        if ($environment !== 'production') {
            return array(
                'environment' => $environment,
                'read_enabled' => true,
                'finance_enabled' => true,
                'issue_enabled' => true,
                'production_read_flag' => $read,
                'production_finance_flag' => $finance,
                'production_issue_flag' => $issue,
            );
        }

        return array(
            'environment' => 'production',
            'read_enabled' => $read,
            'finance_enabled' => $read && $finance,
            'issue_enabled' => $read && $finance && $issue,
            'production_read_flag' => $read,
            'production_finance_flag' => $finance,
            'production_issue_flag' => $issue,
        );
    }
}

if (!function_exists('billing_runtime_gate_allows')) {
    function billing_runtime_gate_allows($operation, $environment = null)
    {
        $operation = strtolower(trim((string) $operation));
        $state = billing_runtime_gate_state($environment);

        if ($state['environment'] !== 'production') {
            return true;
        }

        if (in_array($operation, array('read', 'sync', 'download', 'email'), true)) {
            return (bool) $state['read_enabled'];
        }
        if (in_array($operation, array('finance', 'payment', 'collection', 'reception'), true)) {
            return (bool) $state['finance_enabled'];
        }
        if ($operation === 'preview') {
            return (bool)$state['finance_enabled'] && billing_runtime_parse_bool(
                billing_runtime_config_value('ICYLAB_FACTO_PRODUCTION_DRAFT_PREVIEW_ENABLED', false),
                false
            );
        }
        if (in_array($operation, array('issue', 'emit'), true)) {
            return (bool) $state['issue_enabled'];
        }

        return false;
    }
}

if (!function_exists('billing_issue_confirmation_text')) {
    function billing_issue_confirmation_text($environment = null)
    {
        $environment = billing_runtime_environment($environment);
        if ($environment !== 'production') {
            return 'EMITIR';
        }
        $value = defined('ICYLAB_BILLING_PRODUCTION_CONFIRMATION_TEXT')
            ? trim((string) ICYLAB_BILLING_PRODUCTION_CONFIRMATION_TEXT)
            : trim((string) getenv('ICYLAB_BILLING_PRODUCTION_CONFIRMATION_TEXT'));
        return strtoupper($value !== '' ? $value : 'EMITIR PRODUCCION');
    }
}

if (!function_exists('billing_assert_issue_confirmation')) {
    function billing_assert_issue_confirmation($confirmation, $environment = null)
    {
        $expected = billing_issue_confirmation_text($environment);
        $actual = strtoupper(trim((string) $confirmation));
        if (!hash_equals($expected, $actual)) {
            throw new InvalidArgumentException('Confirmación inválida. Escribe exactamente: ' . $expected . '.');
        }
        return true;
    }
}

if (!function_exists('billing_runtime_csv_values')) {
    function billing_runtime_csv_values($value)
    {
        $items = preg_split('/[\s,;]+/', trim((string)$value), -1, PREG_SPLIT_NO_EMPTY);
        return array_values(array_unique(array_map('trim', is_array($items) ? $items : array())));
    }
}

if (!function_exists('billing_runtime_config_value')) {
    function billing_runtime_config_value($name, $default = '')
    {
        if (defined($name)) {
            return constant($name);
        }
        $value = getenv($name);
        return $value !== false ? $value : $default;
    }
}

if (!function_exists('billing_runtime_document_key_from_type_id')) {
    function billing_runtime_document_key_from_type_id($typeId)
    {
        $map = array(
            2 => 'invoice_electronic',
            16 => 'credit_note_electronic',
            17 => 'debit_note_electronic',
            32 => 'invoice_exempt_electronic',
            37 => 'receipt_electronic',
            41 => 'receipt_exempt_electronic',
            46 => 'invoice_export_electronic',
            47 => 'debit_note_export_electronic',
            48 => 'credit_note_export_electronic',
            54 => 'dispatch_guide_electronic'
        );
        return $map[(int)$typeId] ?? '';
    }
}

if (!function_exists('billing_runtime_issue_policy_state')) {
    /**
     * Resuelve una sola vez la política de emisión productiva. El preflight y
     * la emisión real consumen este mismo estado para no divergir.
     */
    function billing_runtime_issue_policy_state($environment, array $payload = array(), array $context = array())
    {
        $environment = billing_runtime_environment($environment);
        $canaryMode = billing_runtime_parse_bool(
            billing_runtime_config_value('ICYLAB_BILLING_PRODUCTION_CANARY_MODE', true),
            true
        );

        $documentKey = trim((string)($context['document_key'] ?? ''));
        if ($documentKey === '') {
            $documentKey = billing_runtime_document_key_from_type_id($payload['header']['document_type_id'] ?? 0);
        }
        $allowedDocumentKeys = billing_runtime_csv_values(
            billing_runtime_config_value('ICYLAB_BILLING_PRODUCTION_ALLOWED_DOCUMENT_KEYS', '')
        );

        $allowedUsers = array_map('intval', billing_runtime_csv_values(
            billing_runtime_config_value('ICYLAB_BILLING_PRODUCTION_ALLOWED_USER_IDS', '')
        ));
        $allowedUsers = array_values(array_filter($allowedUsers, static function ($id) { return $id > 0; }));
        $userId = (int)($context['user_id'] ?? 0);
        if ($userId <= 0 && function_exists('current_user')) {
            $current = current_user();
            $userId = is_array($current) ? (int)($current['id'] ?? 0) : 0;
        }

        $allowedCustomers = array_map('intval', billing_runtime_csv_values(
            billing_runtime_config_value('ICYLAB_BILLING_PRODUCTION_ALLOWED_CUSTOMER_IDS', '')
        ));
        $allowedCustomers = array_values(array_filter($allowedCustomers, static function ($id) { return $id > 0; }));
        $customerId = (int)($context['customer_id'] ?? 0);

        $pilotEndsAt = trim((string)billing_runtime_config_value('ICYLAB_BILLING_PRODUCTION_PILOT_ENDS_AT', ''));
        $pilotTimestamp = $pilotEndsAt !== '' ? strtotime($pilotEndsAt) : false;
        $pilotWindowOk = $pilotEndsAt === ''
            ? !$canaryMode
            : ($pilotTimestamp !== false && time() <= $pilotTimestamp);

        $total = isset($context['total_amount'])
            ? (float)$context['total_amount']
            : (float)($payload['totals']['total_amount'] ?? 0);
        $maxTotal = max(0, (float)billing_runtime_config_value('ICYLAB_BILLING_PRODUCTION_MAX_TOTAL_AMOUNT', 0));

        return array(
            'environment' => $environment,
            'canary_mode' => $canaryMode,
            'document_key' => $documentKey,
            'allowed_document_keys' => $allowedDocumentKeys,
            'document_allowed' => $documentKey !== '' && !empty($allowedDocumentKeys) && in_array($documentKey, $allowedDocumentKeys, true),
            'user_id' => $userId,
            'allowed_user_ids' => $allowedUsers,
            'user_allowlist_configured' => !empty($allowedUsers),
            'user_allowlist_required' => $canaryMode,
            'user_allowed' => empty($allowedUsers) ? !$canaryMode : in_array($userId, $allowedUsers, true),
            'customer_id' => $customerId,
            'allowed_customer_ids' => $allowedCustomers,
            'customer_allowlist_configured' => !empty($allowedCustomers),
            'customer_allowlist_required' => $canaryMode,
            'customer_allowed' => empty($allowedCustomers) ? !$canaryMode : in_array($customerId, $allowedCustomers, true),
            'pilot_ends_at' => $pilotEndsAt,
            'pilot_timestamp' => $pilotTimestamp,
            'pilot_window_required' => $canaryMode,
            'pilot_window_ok' => $pilotWindowOk,
            'total_amount' => $total,
            'max_total_amount' => $maxTotal,
            'amount_limit_required' => $canaryMode,
            'amount_allowed' => $maxTotal <= 0 ? !$canaryMode : $total <= $maxTotal + 0.005,
        );
    }
}

if (!function_exists('billing_assert_issue_payload_policy')) {
    function billing_assert_issue_payload_policy($environment, array $payload, array $context = array())
    {
        $environment = billing_runtime_environment($environment);
        if ($environment !== 'production') {
            return true;
        }

        if (defined('APP_ENV') && APP_ENV !== 'production') {
            throw new BillingRuntimeGateException(
                'La emisión productiva requiere APP_ENV=production.',
                'BILLING_PRODUCTION_APP_ENV_INVALID',
                'issue',
                $environment
            );
        }

        $factoConfig = billing_production_facto_config_state();
        if (empty($factoConfig['complete'])) {
            throw new BillingRuntimeGateException(
                'La configuración explícita de FACTO producción está incompleta o no es válida.',
                'FACTO_PRODUCTION_CONFIG_INVALID',
                'issue',
                $environment
            );
        }

        $policy = billing_runtime_issue_policy_state($environment, $payload, $context);
        if (!empty($policy['pilot_window_required']) && $policy['pilot_ends_at'] === '') {
            throw new BillingRuntimeGateException('El piloto productivo requiere una fecha de término.', 'BILLING_PRODUCTION_PILOT_END_REQUIRED', 'issue', $environment);
        }
        if ($policy['pilot_ends_at'] !== '' && empty($policy['pilot_window_ok'])) {
            throw new BillingRuntimeGateException('La ventana productiva configurada no está vigente.', 'BILLING_PRODUCTION_PILOT_EXPIRED', 'issue', $environment);
        }

        $isPreview = !empty($payload['options']['draft_preview']);
        if ($isPreview && !billing_runtime_parse_bool(
            billing_runtime_config_value('ICYLAB_FACTO_PRODUCTION_DRAFT_PREVIEW_ENABLED', false),
            false
        )) {
            throw new BillingRuntimeGateException(
                'La vista previa remota productiva permanece deshabilitada hasta confirmar su soporte con FACTO.',
                'FACTO_PRODUCTION_DRAFT_PREVIEW_DISABLED',
                'issue',
                $environment
            );
        }

        if (empty($policy['document_allowed'])) {
            throw new BillingRuntimeGateException('El tipo documental no está autorizado para emisión productiva.', 'BILLING_PRODUCTION_DOCUMENT_NOT_ALLOWED', 'issue', $environment);
        }

        if (!empty($policy['user_allowlist_required']) && empty($policy['allowed_user_ids'])) {
            throw new BillingRuntimeGateException('El piloto productivo requiere una lista de usuarios autorizados.', 'BILLING_PRODUCTION_USER_ALLOWLIST_REQUIRED', 'issue', $environment);
        }
        if (!empty($policy['allowed_user_ids']) && empty($policy['user_allowed'])) {
            throw new BillingRuntimeGateException('El usuario no está autorizado para emisión productiva.', 'BILLING_PRODUCTION_USER_NOT_ALLOWED', 'issue', $environment);
        }

        if (!empty($policy['customer_allowlist_required']) && empty($policy['allowed_customer_ids'])) {
            throw new BillingRuntimeGateException('El piloto productivo requiere una lista de clientes autorizados.', 'BILLING_PRODUCTION_CUSTOMER_ALLOWLIST_REQUIRED', 'issue', $environment);
        }
        if (!empty($policy['allowed_customer_ids']) && empty($policy['customer_allowed'])) {
            throw new BillingRuntimeGateException('El cliente no está autorizado para emisión productiva.', 'BILLING_PRODUCTION_CUSTOMER_NOT_ALLOWED', 'issue', $environment);
        }

        if (!empty($policy['amount_limit_required']) && (float)$policy['max_total_amount'] <= 0) {
            throw new BillingRuntimeGateException('El piloto productivo requiere un monto máximo por documento.', 'BILLING_PRODUCTION_AMOUNT_LIMIT_REQUIRED', 'issue', $environment);
        }
        if ((float)$policy['max_total_amount'] > 0 && empty($policy['amount_allowed'])) {
            throw new BillingRuntimeGateException('El monto supera el máximo productivo configurado.', 'BILLING_PRODUCTION_AMOUNT_EXCEEDED', 'issue', $environment);
        }

        return true;
    }
}

if (!function_exists('billing_assert_runtime_operation')) {
    function billing_assert_runtime_operation($operation, $environment = null, $context = '')
    {
        $operation = strtolower(trim((string) $operation));
        $environment = billing_runtime_environment($environment);

        if (billing_runtime_gate_allows($operation, $environment)) {
            return true;
        }

        $contextSuffix = trim((string) $context) !== '' ? ' Contexto: ' . trim((string) $context) . '.' : '';

        if (in_array($operation, array('read', 'sync', 'download', 'email'), true)) {
            throw new BillingRuntimeGateException(
                'La lectura productiva de Facturación está deshabilitada. No se consultó ni sincronizó información FACTO.' . $contextSuffix,
                'BILLING_PRODUCTION_READ_DISABLED',
                $operation,
                $environment
            );
        }

        if (in_array($operation, array('finance', 'payment', 'collection', 'reception'), true)) {
            throw new BillingRuntimeGateException(
                'Las operaciones financieras productivas están deshabilitadas. No se creó ni modificó ninguna CxC, CxP, pago o asignación.' . $contextSuffix,
                'BILLING_PRODUCTION_FINANCE_DISABLED',
                $operation,
                $environment
            );
        }

        if ($operation === 'preview') {
            throw new BillingRuntimeGateException(
                'La vista previa remota productiva está deshabilitada. Confirma el contrato draft_preview con FACTO antes de habilitarla.' . $contextSuffix,
                'FACTO_PRODUCTION_DRAFT_PREVIEW_DISABLED',
                $operation,
                $environment
            );
        }

        if (in_array($operation, array('issue', 'emit'), true)) {
            throw new BillingRuntimeGateException(
                'La emisión tributaria productiva está deshabilitada. No se envió ningún documento a FACTO.' . $contextSuffix,
                'BILLING_PRODUCTION_ISSUE_DISABLED',
                $operation,
                $environment
            );
        }

        throw new BillingRuntimeGateException(
            'Operación productiva no autorizada por el runtime gate.' . $contextSuffix,
            'BILLING_PRODUCTION_OPERATION_DISABLED',
            $operation,
            $environment
        );
    }
}
