<?php

namespace MintyPHP\Module\Security\Service;

use MintyPHP\Http\SessionStoreInterface;

/**
 * Thin Business Central OData V4 gateway for the Security module.
 *
 * Standalone and intentionally minimal: only the two queries the security-check
 * wizard needs (customer search + domains-for-customer) plus a connection test.
 * Supports Basic and OAuth2 auth modes. No remote assets, TLS verification on
 * (GR-SEC-004).
 *
 * @api Consumed by the customer-search / customer-domains / settings-test pages.
 */
class SecurityBcGateway
{
    public const ENTITY_CUSTOMER = 'Integration_Customer_Card';
    public const ENTITY_CONTRACT_DOMAINS = 'FS_Contract_Domains';

    private const CONNECT_TIMEOUT = 10;
    private const REQUEST_TIMEOUT = 30;

    public function __construct(
        private readonly SecurityBcSettingsGateway $settingsGateway,
        private readonly SecurityOAuthTokenService $oauthTokenService,
        private readonly SessionStoreInterface $sessionStore
    ) {
    }

    /**
     * Search customers by name or number.
     *
     * Tries contains() first, falls back to startswith() / range filter for BC
     * versions that reject contains().
     *
     * @return array<int, array<string, mixed>>
     */
    public function searchCustomers(string $query): array
    {
        $query = trim($query);
        if ($query === '') {
            return [];
        }

        $escaped = $this->escapeODataStrictUserInput($query);
        $upperQuery = mb_strtoupper($escaped);
        $select = '&$select=No,Name,Search_Name,Address,City,Post_Code,Phone_No,E_Mail';
        $baseUrl = $this->settingsGateway->buildEntityUrl(self::ENTITY_CUSTOMER);

        $strategies = [
            "contains(Search_Name,'" . $upperQuery . "') or contains(Name,'" . $escaped . "') or contains(No,'" . $escaped . "')",
            "startswith(Search_Name,'" . $upperQuery . "') or startswith(Name,'" . $escaped . "') or startswith(No,'" . $escaped . "')",
            "Search_Name ge '" . $upperQuery . "' and Search_Name lt '" . $upperQuery . "z'",
        ];

        $lastException = null;
        foreach ($strategies as $filter) {
            $url = $baseUrl . '?$filter=' . rawurlencode($filter) . '&$top=50' . $select;
            try {
                $response = $this->request('GET', $url);
                if ($response !== null) {
                    return $this->extractODataValues($response);
                }

                throw new \RuntimeException('BC OData request failed for URL: ' . $url);
            } catch (\RuntimeException $e) {
                $lastException = $e;
                continue;
            }
        }

        throw $lastException ?? new \RuntimeException('All OData search strategies failed');
    }

    /**
     * Get active domains for a customer.
     *
     * @return array<int, array<string, mixed>>
     */
    public function listDomainsForCustomer(string $customerNo): array
    {
        $customerNo = trim($customerNo);
        if ($customerNo === '') {
            return [];
        }

        $escaped = $this->escapeODataStrictUserInput($customerNo);
        $filter = "Customer_No eq '" . $escaped . "' and State eq 'Aktiv'";
        $select = 'No,Customer_No,URL,State';
        $url = $this->settingsGateway->buildEntityUrl(self::ENTITY_CONTRACT_DOMAINS)
            . '?$filter=' . rawurlencode($filter)
            . '&$top=200'
            . '&$select=' . rawurlencode($select)
            . '&$orderby=' . rawurlencode('URL asc');

        $response = $this->request('GET', $url);
        if ($response === null) {
            return [];
        }

        return $this->extractODataValues($response);
    }

    /**
     * Test the configured BC connection.
     *
     * @return array{ok: bool, error?: string, url?: string, http_code?: int}
     */
    public function testConnection(): array
    {
        if (!$this->settingsGateway->isConfigured()) {
            return ['ok' => false, 'error' => 'Configuration incomplete'];
        }

        $url = $this->settingsGateway->buildEntityUrl(self::ENTITY_CUSTOMER) . '?$top=1&$select=No';

        $ch = curl_init();
        if ($ch === false) {
            return ['ok' => false, 'error' => 'curl_init failed'];
        }

        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => self::CONNECT_TIMEOUT,
            CURLOPT_TIMEOUT => self::REQUEST_TIMEOUT,
            CURLOPT_HTTPHEADER => ['Accept: application/json', 'OData-Version: 4.0'],
            CURLOPT_CUSTOMREQUEST => 'GET',
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
        ]);

        $this->applyAuth($ch);

        $responseBody = curl_exec($ch);
        $curlErrno = curl_errno($ch);
        $curlError = curl_error($ch);
        $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if ($curlErrno !== 0) {
            return ['ok' => false, 'error' => 'cURL error ' . $curlErrno . ': ' . $curlError, 'url' => $url];
        }

        if ($httpCode < 200 || $httpCode >= 300) {
            $hint = match ($httpCode) {
                401 => 'Unauthorized — check credentials',
                403 => 'Forbidden — user has no access to this endpoint',
                404 => 'Not found — check OData URL and company name',
                default => 'HTTP ' . $httpCode,
            };

            return ['ok' => false, 'error' => $hint, 'url' => $url, 'http_code' => $httpCode];
        }

        if (!is_string($responseBody)) {
            return ['ok' => false, 'error' => 'Empty response body', 'url' => $url, 'http_code' => $httpCode];
        }

        $decoded = json_decode($responseBody, true);
        if (!is_array($decoded)) {
            return ['ok' => false, 'error' => 'Invalid JSON response', 'url' => $url, 'http_code' => $httpCode];
        }

        return ['ok' => true, 'url' => $url, 'http_code' => $httpCode];
    }

    /**
     * @return array<string, mixed>|null
     */
    private function request(string $method, string $url): ?array
    {
        if (!$this->settingsGateway->isConfigured()) {
            return null;
        }

        $ch = curl_init();
        if ($ch === false) {
            return null;
        }

        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => self::CONNECT_TIMEOUT,
            CURLOPT_TIMEOUT => self::REQUEST_TIMEOUT,
            CURLOPT_HTTPHEADER => ['Accept: application/json', 'OData-Version: 4.0'],
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
        ]);

        $this->applyAuth($ch);

        $responseBody = curl_exec($ch);
        $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        unset($ch);

        if (!is_string($responseBody) || $httpCode < 200 || $httpCode >= 300) {
            if ($httpCode >= 400 && $httpCode < 500) {
                throw new \RuntimeException('BC OData client error: HTTP ' . $httpCode);
            }

            return null;
        }

        $decoded = json_decode($responseBody, true);
        if (!is_array($decoded)) {
            return null;
        }

        return $decoded;
    }

    /**
     * @param \CurlHandle $ch
     */
    private function applyAuth($ch): void
    {
        $authMode = $this->settingsGateway->getAuthMode();

        if ($authMode === SecurityBcSettingsGateway::AUTH_MODE_BASIC) {
            $user = $this->settingsGateway->getBasicUser() ?? '';
            $password = $this->settingsGateway->getBasicPassword() ?? '';
            curl_setopt($ch, CURLOPT_USERPWD, $user . ':' . $password);
            curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
            return;
        }

        if ($authMode !== SecurityBcSettingsGateway::AUTH_MODE_OAUTH2) {
            return;
        }

        $tenantId = $this->resolveCurrentTenantId();
        if ($tenantId <= 0) {
            return;
        }

        $token = $this->oauthTokenService->getAccessToken($tenantId, $this->buildOAuthAudience());
        if ($token === null || trim($token) === '') {
            return;
        }

        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Accept: application/json',
            'OData-Version: 4.0',
            'Authorization: Bearer ' . $token,
        ]);
    }

    /**
     * @param array<string, mixed> $response
     * @return array<int, array<string, mixed>>
     */
    private function extractODataValues(array $response): array
    {
        $values = $response['value'] ?? [];
        if (!is_array($values)) {
            return [];
        }

        return array_values($values);
    }

    /**
     * @throws \InvalidArgumentException If the value contains disallowed characters.
     */
    private function escapeODataStrictUserInput(string $value): string
    {
        if (preg_match('/^[\p{L}\p{N}\s.\-_@\/&,;:#+*]+$/u', $value) !== 1) {
            throw new \InvalidArgumentException('Search query contains invalid characters.');
        }

        return str_replace("'", "''", $value);
    }

    private function resolveCurrentTenantId(): int
    {
        try {
            $session = $this->sessionStore->all();
        } catch (\Throwable) {
            return 0;
        }

        $tenantId = (int) ($session['current_tenant']['id'] ?? 0);

        return $tenantId > 0 ? $tenantId : 0;
    }

    private function buildOAuthAudience(): ?string
    {
        $baseUrl = trim($this->settingsGateway->getODataBaseUrl());
        if ($baseUrl === '') {
            return null;
        }

        $parsed = parse_url($baseUrl);
        if (!is_array($parsed) || empty($parsed['host'])) {
            return null;
        }

        $scheme = strtolower((string) ($parsed['scheme'] ?? 'https'));
        $host = (string) $parsed['host'];
        $port = isset($parsed['port']) ? ':' . (int) $parsed['port'] : '';

        return $scheme . '://' . $host . $port;
    }
}
