<?php

require_once __DIR__ . '/../../core/config.php';
require_once __DIR__ . '/../../core/database.php';
require_once __DIR__ . '/../billing/BillingRuntimeGate.php';

class FactoApiException extends Exception
{
    private $httpCode;
    private $responseBody;
    private $decodedResponse;
    private $endpoint;

    public function __construct($message, $httpCode = 0, $responseBody = null, $decodedResponse = null, $endpoint = null)
    {
        parent::__construct($message, (int) $httpCode);
        $this->httpCode = (int) $httpCode;
        $this->responseBody = $responseBody;
        $this->decodedResponse = $decodedResponse;
        $this->endpoint = $endpoint;
    }

    public function getHttpCode()
    {
        return $this->httpCode;
    }

    public function getResponseBody()
    {
        return $this->responseBody;
    }

    public function getDecodedResponse()
    {
        return $this->decodedResponse;
    }

    public function getEndpoint()
    {
        return $this->endpoint;
    }
}

class FactoClient
{
    private $environment;
    private $settings;
    private $baseUrl;
    private $pdo;
    private $tokenEnvironmentColumnChecked = false;
    private $tokenEnvironmentColumnExists = false;

    public function __construct($environment = null)
    {
        $this->environment = $this->normalizeEnvironment($environment);
        $this->settings = icylab_facto_config($this->environment);
        $this->baseUrl = rtrim($this->settings['base_url'], '/');
        $this->pdo = db();
    }

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

    public function getBaseUrl()
    {
        return $this->baseUrl;
    }

    public function getAccountId()
    {
        return isset($this->settings['account_id']) ? $this->settings['account_id'] : '';
    }

    public function getSettingsSummary()
    {
        return array(
            'environment' => $this->environment,
            'base_url' => $this->baseUrl,
            'account_id_configured' => $this->getAccountId() !== '',
            'client_id_configured' => !empty($this->settings['client_id']),
            'username_configured' => !empty($this->settings['username']),
            'timeout' => isset($this->settings['timeout']) ? (int) $this->settings['timeout'] : 45,
            'connect_timeout' => isset($this->settings['connect_timeout']) ? (int) $this->settings['connect_timeout'] : 12
        );
    }

    public function authenticate()
    {
        $this->assertAuthConfig();

        $payload = array(
            'grant_type' => 'password',
            'client_id' => $this->settings['client_id'],
            'client_secret' => $this->settings['client_secret'],
            'username' => $this->settings['username'],
            'password' => $this->settings['password']
        );

        $response = $this->requestRaw('POST', '/auth', $payload, null, array(), false);

        if (!isset($response['access_token']) || trim((string) $response['access_token']) === '') {
            throw new Exception('FACTO no devolvio access_token para ambiente ' . $this->environment . '.');
        }

        $expiresIn = isset($response['expires_in']) ? (int) $response['expires_in'] : 3600;
        $expiresAt = date('Y-m-d H:i:s', time() + max(60, $expiresIn - 120));

        $this->storeToken(array(
            'access_token' => $response['access_token'],
            'refresh_token' => isset($response['refresh_token']) ? $response['refresh_token'] : null,
            'token_type' => isset($response['token_type']) ? $response['token_type'] : null,
            'scope' => isset($response['scope']) ? $response['scope'] : null,
            'expires_at' => $expiresAt
        ));

        return $response['access_token'];
    }

    public function getAccessToken()
    {
        $token = $this->loadStoredToken();

        if ($token && strtotime($token['expires_at']) > time()) {
            return $token['access_token'];
        }

        return $this->authenticate();
    }

    public function get($endpoint, $params = array())
    {
        billing_assert_runtime_operation('read', $this->environment, 'Consulta remota FACTO');
        return $this->requestWithAuth('GET', $endpoint, null, $params);
    }

    public function post($endpoint, $body = array(), $params = array())
    {
        $isDraftPreview = strpos((string)$endpoint, '/documents') === 0
            && is_array($body)
            && !empty($body['options']['draft_preview']);
        $operation = strpos((string) $endpoint, '/payments') === 0 ? 'finance' : ($isDraftPreview ? 'preview' : 'issue');
        billing_assert_runtime_operation($operation, $this->environment, 'Solicitud POST remota FACTO');
        return $this->requestWithAuth('POST', $endpoint, $body, $params);
    }

    public function put($endpoint, $body = array(), $params = array())
    {
        billing_assert_runtime_operation('issue', $this->environment, 'Solicitud PUT remota FACTO');
        return $this->requestWithAuth('PUT', $endpoint, $body, $params);
    }

    public function patch($endpoint, $body = array(), $params = array())
    {
        $operation = strpos((string) $endpoint, '/inbox_documents') === 0 ? 'finance' : 'issue';
        billing_assert_runtime_operation($operation, $this->environment, 'Solicitud PATCH remota FACTO');
        return $this->requestWithAuth('PATCH', $endpoint, $body, $params);
    }

    public function delete($endpoint, $params = array())
    {
        billing_assert_runtime_operation('issue', $this->environment, 'Solicitud DELETE remota FACTO');
        return $this->requestWithAuth('DELETE', $endpoint, null, $params);
    }

    public function getDocuments($params = array())
    {
        return $this->get('/documents', $params);
    }

    public function getDocument($documentId, $generateStamp = false)
    {
        $params = array();
        if ($generateStamp) {
            $params['generate_stamp'] = 1;
        }

        return $this->get('/documents/' . rawurlencode((string) $documentId), $params);
    }

    public function createDocument($payload, $params = array(), $policyContext = array())
    {
        $operation = is_array($payload) && !empty($payload['options']['draft_preview']) ? 'preview' : 'issue';
        billing_assert_runtime_operation($operation, $this->environment, 'Creación remota de documento FACTO');
        billing_assert_issue_payload_policy($this->environment, is_array($payload) ? $payload : array(), is_array($policyContext) ? $policyContext : array());
        return $this->post('/documents', $payload, $params);
    }

    public function createDocumentDraftPreview($payload, $params = array(), $policyContext = array())
    {
        if (!isset($payload['options']) || !is_array($payload['options'])) {
            $payload['options'] = array();
        }

        $payload['options']['draft_preview'] = true;

        return $this->createDocument($payload, $params, $policyContext);
    }

    public function createDocumentDraftOnly($payload, $params = array(), $policyContext = array())
    {
        if (!isset($payload['options']) || !is_array($payload['options'])) {
            $payload['options'] = array();
        }

        $payload['options']['draft_only'] = true;

        return $this->createDocument($payload, $params, $policyContext);
    }

    public function updateDocument($documentId, $payload)
    {
        billing_assert_runtime_operation('issue', $this->environment, 'Actualización remota de documento FACTO');
        return $this->put('/documents/' . rawurlencode((string) $documentId), $payload);
    }

    public function deleteDocument($documentId)
    {
        billing_assert_runtime_operation('issue', $this->environment, 'Eliminación remota de documento FACTO');
        return $this->delete('/documents/' . rawurlencode((string) $documentId));
    }

    public function getInboxDocuments($params = array())
    {
        return $this->get('/inbox_documents', $params);
    }

    public function getInboxDocument($inboxDocumentId)
    {
        return $this->get('/inbox_documents/' . rawurlencode((string) $inboxDocumentId));
    }

    public function patchInboxDocument($inboxDocumentId, $payload)
    {
        billing_assert_runtime_operation('finance', $this->environment, 'Actualización remota de DTE recibido FACTO');
        return $this->patch('/inbox_documents/' . rawurlencode((string) $inboxDocumentId), $payload);
    }

    public function getCustomersSuppliers($params = array())
    {
        return $this->get('/customers_suppliers', $params);
    }

    public function getCustomerSupplier($id)
    {
        return $this->get('/customers_suppliers/' . (int) $id);
    }

    public function getDocumentTypes($params = array())
    {
        return $this->get('/document_types', $params);
    }

    public function getDocumentType($documentTypeId)
    {
        return $this->get('/document_types/' . (int) $documentTypeId);
    }

    public function getDocumentAuthorizations($params = array())
    {
        return $this->get('/document_authorizations', $params);
    }

    public function getDocumentAuthorization($documentAuthorizationId)
    {
        return $this->get('/document_authorizations/' . (int) $documentAuthorizationId);
    }

    public function getCompanyBranches($params = array())
    {
        return $this->get('/company_branches', $params);
    }

    public function getCompanyBranch($companyBranchId)
    {
        return $this->get('/company_branches/' . rawurlencode((string) $companyBranchId));
    }

    public function createPayment($payload)
    {
        billing_assert_runtime_operation('finance', $this->environment, 'Creación remota de pago FACTO');
        return $this->post('/payments', $payload);
    }

    public function getPayment($paymentId)
    {
        return $this->get('/payments/' . rawurlencode((string) $paymentId));
    }

    public function getDocumentTransfers($params = array())
    {
        return $this->get('/document_transfers', $params);
    }

    public function getDocumentTransfer($documentTransferId)
    {
        return $this->get('/document_transfers/' . rawurlencode((string) $documentTransferId));
    }

    public function sanitizeForLog($value)
    {
        $copy = $value;
        $this->sanitizeRecursive($copy);
        return $copy;
    }

    private function requestWithAuth($method, $endpoint, $body = null, $params = array())
    {
        $token = $this->getAccessToken();

        try {
            return $this->requestRaw($method, $endpoint, $body, $token, $params, true);
        } catch (FactoApiException $e) {
            if ($e->getHttpCode() === 401) {
                $token = $this->authenticate();
                return $this->requestRaw($method, $endpoint, $body, $token, $params, true);
            }

            throw $e;
        }
    }

    private function requestRaw($method, $endpoint, $body = null, $token = null, $params = array(), $authRequest = true)
    {
        $url = $this->baseUrl . '/' . ltrim($endpoint, '/');

        if (!empty($params)) {
            $url .= '?' . http_build_query($params);
        }

        $headers = array('Accept: application/json');

        if ($body !== null) {
            $headers[] = 'Content-Type: application/json';
        }

        if ($token !== null) {
            $headers[] = 'Authorization: Bearer ' . $token;
        }

        $ch = curl_init($url);

        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, isset($this->settings['connect_timeout']) ? (int) $this->settings['connect_timeout'] : 12);
        curl_setopt($ch, CURLOPT_TIMEOUT, isset($this->settings['timeout']) ? (int) $this->settings['timeout'] : 45);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method));
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

        if ($body !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        }

        $rawResponse = curl_exec($ch);
        $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        $curlErrno = curl_errno($ch);

        curl_close($ch);

        if ($rawResponse === false) {
            throw new FactoApiException(
                'Error cURL FACTO ambiente ' . $this->environment . ': ' . $curlError,
                0,
                null,
                null,
                $endpoint
            );
        }

        $decoded = null;
        if ($rawResponse !== '') {
            $decoded = json_decode($rawResponse, true);
        }

        if ($rawResponse !== '' && $decoded === null && json_last_error() !== JSON_ERROR_NONE) {
            throw new FactoApiException(
                'FACTO devolvio una respuesta no JSON en ambiente ' . $this->environment . ' para ' . $endpoint . '.',
                $httpCode,
                $rawResponse,
                null,
                $endpoint
            );
        }

        if ($httpCode < 200 || $httpCode >= 300) {
            $message = $this->extractErrorMessage($decoded, $rawResponse);
            throw new FactoApiException(
                'FACTO HTTP ' . $httpCode . ' ambiente ' . $this->environment . ': ' . $message,
                $httpCode,
                $rawResponse,
                $decoded,
                $endpoint
            );
        }

        if ($decoded === null) {
            return array('success' => true, 'http_code' => $httpCode);
        }

        return $decoded;
    }

    private function assertAuthConfig()
    {
        $missing = array();

        foreach (array('base_url', 'client_id', 'client_secret', 'username', 'password') as $key) {
            if (!isset($this->settings[$key]) || trim((string) $this->settings[$key]) === '') {
                $missing[] = $key;
            }
        }

        if (!empty($missing)) {
            throw new Exception('Configuracion FACTO incompleta para ambiente ' . $this->environment . ': ' . implode(', ', $missing) . '.');
        }
    }

    private function storeToken($tokenData)
    {
        if ($this->hasTokenEnvironmentColumn()) {
            $stmt = $this->pdo->prepare("\n                INSERT INTO facto_tokens\n                (environment, access_token, refresh_token, token_type, scope, expires_at)\n                VALUES\n                (?, ?, ?, ?, ?, ?)\n                ON DUPLICATE KEY UPDATE\n                    access_token = VALUES(access_token),\n                    refresh_token = VALUES(refresh_token),\n                    token_type = VALUES(token_type),\n                    scope = VALUES(scope),\n                    expires_at = VALUES(expires_at),\n                    updated_at = CURRENT_TIMESTAMP\n            ");

            $stmt->execute(array(
                $this->environment,
                $tokenData['access_token'],
                $tokenData['refresh_token'],
                $tokenData['token_type'],
                $tokenData['scope'],
                $tokenData['expires_at']
            ));
            return;
        }

        $stmt = $this->pdo->prepare("\n            INSERT INTO facto_tokens\n            (id, access_token, refresh_token, token_type, scope, expires_at)\n            VALUES\n            (1, ?, ?, ?, ?, ?)\n            ON DUPLICATE KEY UPDATE\n                access_token = VALUES(access_token),\n                refresh_token = VALUES(refresh_token),\n                token_type = VALUES(token_type),\n                scope = VALUES(scope),\n                expires_at = VALUES(expires_at),\n                updated_at = CURRENT_TIMESTAMP\n        ");

        $stmt->execute(array(
            $tokenData['access_token'],
            $tokenData['refresh_token'],
            $tokenData['token_type'],
            $tokenData['scope'],
            $tokenData['expires_at']
        ));
    }

    private function loadStoredToken()
    {
        if ($this->hasTokenEnvironmentColumn()) {
            $stmt = $this->pdo->prepare("\n                SELECT access_token, expires_at\n                FROM facto_tokens\n                WHERE environment = ?\n                LIMIT 1\n            ");
            $stmt->execute(array($this->environment));
            return $stmt->fetch();
        }

        $stmt = $this->pdo->query("\n            SELECT access_token, expires_at\n            FROM facto_tokens\n            WHERE id = 1\n            LIMIT 1\n        ");

        return $stmt->fetch();
    }

    private function hasTokenEnvironmentColumn()
    {
        if ($this->tokenEnvironmentColumnChecked) {
            return $this->tokenEnvironmentColumnExists;
        }

        $this->tokenEnvironmentColumnChecked = true;

        try {
            $stmt = $this->pdo->query("SHOW COLUMNS FROM facto_tokens LIKE 'environment'");
            $this->tokenEnvironmentColumnExists = (bool) $stmt->fetch();
        } catch (Exception $e) {
            $this->tokenEnvironmentColumnExists = false;
        }

        return $this->tokenEnvironmentColumnExists;
    }

    private function normalizeEnvironment($environment = null)
    {
        if (function_exists('icylab_facto_normalize_environment')) {
            return icylab_facto_normalize_environment($environment);
        }

        $env = strtolower(trim((string) ($environment ?: 'production')));
        return in_array($env, array('sandbox', 'production'), true) ? $env : 'production';
    }

    private function extractErrorMessage($decoded, $rawResponse)
    {
        if (is_array($decoded)) {
            foreach (array('message', 'error_message', 'error', 'detail', 'title') as $key) {
                if (isset($decoded[$key]) && is_scalar($decoded[$key])) {
                    return (string) $decoded[$key];
                }
            }

            if (isset($decoded['result']['error_message']) && is_scalar($decoded['result']['error_message'])) {
                return (string) $decoded['result']['error_message'];
            }

            return json_encode($this->sanitizeForLog($decoded), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }

        return is_string($rawResponse) && $rawResponse !== '' ? $rawResponse : 'sin detalle';
    }

    private function sanitizeRecursive(&$value)
    {
        if (!is_array($value)) {
            return;
        }

        foreach ($value as $key => &$item) {
            $lowerKey = strtolower((string) $key);
            if (strpos($lowerKey, 'token') !== false || strpos($lowerKey, 'secret') !== false || strpos($lowerKey, 'password') !== false || strpos($lowerKey, 'authorization') !== false) {
                $item = '[REDACTED]';
                continue;
            }

            if (is_array($item)) {
                $this->sanitizeRecursive($item);
            }
        }
    }
}
