<?php

namespace MintyPHP\Module\Helpdesk\Service;

use MintyPHP\Http\SessionStoreInterface;

/**
 * Gateway for Business Central SOAP codeunit calls used by helpdesk communication.
 */
class BcSoapGateway
{
    private const CONNECT_TIMEOUT = 10;
    private const REQUEST_TIMEOUT = 30;
    private const CODEUNIT_NAME = 'ICISupportWebserviceAPI';
    private const SOAP_ACTION_GET_TICKET_COMMUNICATION_TEXT = 'urn:microsoft-dynamics-schemas/codeunit/ICISupportWebserviceAPI:GetTicketCommunicationText';
    private const SOAP_ACTION_GET_TICKET_FILE = 'urn:microsoft-dynamics-schemas/codeunit/ICISupportWebserviceAPI:GetTicketFile';

    public function __construct(
        private readonly EffectiveHelpdeskSettingsService $settingsGateway,
        private readonly HelpdeskOAuthTokenService $oauthTokenService,
        private readonly SessionStoreInterface $sessionStore
    ) {
    }

    /**
     * @return array{
     *   ok: bool,
     *   soap_used: bool,
     *   return_value: int,
     *   communication_text: string,
     *   message_entries: string,
     *   error?: string
     * }
     */
    public function getTicketCommunicationText(string $ticketNo, string $messageEntries = ''): array
    {
        $ticketNo = trim($ticketNo);
        if ($ticketNo === '') {
            return [
                'ok' => false,
                'soap_used' => false,
                'return_value' => 0,
                'communication_text' => '',
                'message_entries' => '',
                'error' => 'Missing ticket number',
            ];
        }

        if (!$this->settingsGateway->isConfigured()) {
            return [
                'ok' => false,
                'soap_used' => false,
                'return_value' => 0,
                'communication_text' => '',
                'message_entries' => '',
                'error' => 'BC connection not configured',
            ];
        }

        $endpoint = $this->buildSoapEndpoint();
        if ($endpoint === null) {
            return [
                'ok' => false,
                'soap_used' => false,
                'return_value' => 0,
                'communication_text' => '',
                'message_entries' => '',
                'error' => 'Could not derive SOAP endpoint from BC URL',
            ];
        }

        $requestBody = $this->buildGetTicketCommunicationTextEnvelope($ticketNo, $messageEntries);
        $request = $this->buildRequestContext($endpoint, $requestBody);
        if (isset($request['error'])) {
            return [
                'ok' => false,
                'soap_used' => false,
                'return_value' => 0,
                'communication_text' => '',
                'message_entries' => '',
                'error' => (string) $request['error'],
            ];
        }

        $response = $this->sendSoapRequest(
            $endpoint,
            self::SOAP_ACTION_GET_TICKET_COMMUNICATION_TEXT,
            $requestBody,
            (array) ($request['headers'] ?? []),
            (string) ($request['basic_user'] ?? ''),
            (string) ($request['basic_password'] ?? '')
        );

        $httpCode = (int) ($response['http_code'] ?? 0);
        $rawBody = (string) ($response['body'] ?? '');
        if ($httpCode < 200 || $httpCode >= 300) {
            $errorSnippet = trim((string) ($response['error'] ?? ''));
            if ($errorSnippet === '' && $rawBody !== '') {
                $errorSnippet = mb_substr($rawBody, 0, 250);
            }

            return [
                'ok' => false,
                'soap_used' => false,
                'return_value' => 0,
                'communication_text' => '',
                'message_entries' => '',
                'error' => 'BC SOAP request failed: HTTP ' . $httpCode . ($errorSnippet !== '' ? ' - ' . $errorSnippet : ''),
            ];
        }

        $parsed = $this->parseGetTicketCommunicationTextResponse($rawBody);
        if (!$parsed['ok']) {
            return [
                'ok' => false,
                'soap_used' => false,
                'return_value' => 0,
                'communication_text' => '',
                'message_entries' => '',
                'error' => isset($parsed['error']) ? (string) $parsed['error'] : 'Could not parse SOAP response',
            ];
        }

        $returnValue = (int) ($parsed['return_value'] ?? 0);
        if ($returnValue >= 400) {
            return [
                'ok' => false,
                'soap_used' => false,
                'return_value' => $returnValue,
                'communication_text' => (string) ($parsed['communication_text'] ?? ''),
                'message_entries' => (string) ($parsed['message_entries'] ?? ''),
                'error' => 'BC SOAP returned error code ' . $returnValue,
            ];
        }

        return [
            'ok' => true,
            'soap_used' => true,
            'return_value' => $returnValue,
            'communication_text' => (string) ($parsed['communication_text'] ?? ''),
            'message_entries' => (string) ($parsed['message_entries'] ?? ''),
        ];
    }

    private function buildGetTicketCommunicationTextEnvelope(string $ticketNo, string $messageEntries): string
    {
        $escapedTicketNo = htmlspecialchars($ticketNo, ENT_XML1 | ENT_QUOTES, 'UTF-8');
        $normalizedMessageEntries = trim($messageEntries);
        if ($normalizedMessageEntries === '') {
            $normalizedMessageEntries = '0';
        }
        $escapedMessageEntries = htmlspecialchars($normalizedMessageEntries, ENT_XML1 | ENT_QUOTES, 'UTF-8');

        return <<<XML
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:microsoft-dynamics-schemas/codeunit/ICISupportWebserviceAPI">
  <soap:Body>
    <urn:GetTicketCommunicationText>
      <urn:ticketNo>{$escapedTicketNo}</urn:ticketNo>
      <urn:communicationText></urn:communicationText>
      <urn:messageEntries>{$escapedMessageEntries}</urn:messageEntries>
    </urn:GetTicketCommunicationText>
  </soap:Body>
</soap:Envelope>
XML;
    }

    /**
     * @return array{headers?: array<int, string>, basic_user?: string, basic_password?: string, error?: string}
     */
    private function buildRequestContext(string $endpoint, string $requestBody, string $soapAction = ''): array
    {
        if ($soapAction === '') {
            $soapAction = self::SOAP_ACTION_GET_TICKET_COMMUNICATION_TEXT;
        }

        $headers = [
            'Content-Type: text/xml; charset=utf-8',
            'Accept: text/xml,application/xml',
            'SOAPAction: "' . $soapAction . '"',
            'Content-Length: ' . strlen($requestBody),
        ];

        $authMode = $this->settingsGateway->getAuthMode();
        if ($authMode === HelpdeskSettingsGateway::AUTH_MODE_BASIC) {
            return [
                'headers' => $headers,
                'basic_user' => $this->settingsGateway->getBasicUser() ?? '',
                'basic_password' => $this->settingsGateway->getBasicPassword() ?? '',
            ];
        }

        if ($authMode !== HelpdeskSettingsGateway::AUTH_MODE_OAUTH2) {
            return ['headers' => $headers];
        }

        $tenantId = $this->resolveCurrentTenantId();
        if ($tenantId <= 0) {
            return ['error' => 'OAuth2 mode requires active tenant context'];
        }

        $audience = $this->buildOAuthAudienceFromEndpoint($endpoint);
        if ($audience === null) {
            return ['error' => 'Could not build OAuth audience for SOAP endpoint'];
        }

        $token = $this->oauthTokenService->getAccessToken($tenantId, $audience);
        if ($token === null || trim($token) === '') {
            return ['error' => 'Could not obtain OAuth2 access token for SOAP request'];
        }

        $headers[] = 'Authorization: Bearer ' . $token;

        return ['headers' => $headers];
    }

    /**
     * @return array{ok: bool, return_value?: int, communication_text?: string, message_entries?: string, error?: string}
     */
    private function parseGetTicketCommunicationTextResponse(string $body): array
    {
        $body = trim($body);
        if ($body === '') {
            return ['ok' => false, 'error' => 'Empty SOAP response'];
        }

        $dom = new \DOMDocument();
        $previousLibxmlState = libxml_use_internal_errors(true);
        $loaded = $dom->loadXML($body);
        libxml_clear_errors();
        libxml_use_internal_errors($previousLibxmlState);
        if (!$loaded) {
            return ['ok' => false, 'error' => 'Invalid SOAP XML response'];
        }

        $xpath = new \DOMXPath($dom);
        $faultNode = $xpath->query("//*[local-name()='Fault']/*[local-name()='faultstring']")->item(0);
        if ($faultNode instanceof \DOMNode) {
            return ['ok' => false, 'error' => trim((string) $faultNode->textContent) ?: 'SOAP fault'];
        }

        $returnNode = $xpath->query("//*[local-name()='return_value']")->item(0);
        $communicationNode = $xpath->query("//*[local-name()='communicationText']")->item(0);
        $messageEntriesNode = $xpath->query("//*[local-name()='messageEntries']")->item(0);

        return [
            'ok' => true,
            'return_value' => $returnNode instanceof \DOMNode && is_numeric(trim((string) $returnNode->textContent))
                ? (int) trim((string) $returnNode->textContent)
                : 0,
            'communication_text' => $communicationNode instanceof \DOMNode ? (string) $communicationNode->textContent : '',
            'message_entries' => $messageEntriesNode instanceof \DOMNode ? (string) $messageEntriesNode->textContent : '',
        ];
    }

    protected function sendSoapRequest(
        string $endpoint,
        string $soapAction,
        string $requestBody,
        array $headers,
        string $basicUser,
        string $basicPassword
    ): array {
        $ch = curl_init();
        if ($ch === false) {
            return ['http_code' => 0, 'body' => '', 'error' => 'curl_init failed'];
        }

        curl_setopt_array($ch, [
            CURLOPT_URL => $endpoint,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => self::CONNECT_TIMEOUT,
            CURLOPT_TIMEOUT => self::REQUEST_TIMEOUT,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $requestBody,
            CURLOPT_HTTPHEADER => $headers,
        ]);

        if ($basicUser !== '' || $basicPassword !== '') {
            curl_setopt($ch, CURLOPT_USERPWD, $basicUser . ':' . $basicPassword);
            curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
        }

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

        return [
            'http_code' => $httpCode,
            'body' => is_string($responseBody) ? $responseBody : '',
            'error' => $error,
        ];
    }

    /**
     * Download a ticket file attachment via SOAP GetTicketFile.
     *
     * Returns the decoded binary data or an error. Does not write to disk.
     *
     * @return array{ok: bool, data?: string, filename?: string, error?: string}
     */
    public function getTicketFile(int $entryNo, string $contactNo = ''): array
    {
        if ($entryNo <= 0) {
            return ['ok' => false, 'error' => 'Invalid entry number'];
        }

        if (!$this->settingsGateway->isConfigured()) {
            return ['ok' => false, 'error' => 'BC connection not configured'];
        }

        $endpoint = $this->buildSoapEndpoint();
        if ($endpoint === null) {
            return ['ok' => false, 'error' => 'Could not derive SOAP endpoint'];
        }

        $requestBody = $this->buildGetTicketFileEnvelope($entryNo, $contactNo);
        $request = $this->buildRequestContext($endpoint, $requestBody, self::SOAP_ACTION_GET_TICKET_FILE);
        if (isset($request['error'])) {
            return ['ok' => false, 'error' => (string) $request['error']];
        }

        $response = $this->sendSoapRequest(
            $endpoint,
            self::SOAP_ACTION_GET_TICKET_FILE,
            $requestBody,
            (array) ($request['headers'] ?? []),
            (string) ($request['basic_user'] ?? ''),
            (string) ($request['basic_password'] ?? '')
        );

        $httpCode = (int) ($response['http_code'] ?? 0);
        $rawBody = (string) ($response['body'] ?? '');
        if ($httpCode < 200 || $httpCode >= 300) {
            // Try to extract SOAP fault from error response body
            $faultDetail = $this->extractSoapFault($rawBody);

            return ['ok' => false, 'error' => 'SOAP request failed: HTTP ' . $httpCode . ($faultDetail !== '' ? ' — ' . $faultDetail : '')];
        }

        return $this->parseGetTicketFileResponse($rawBody);
    }

    private function buildGetTicketFileEnvelope(int $entryNo, string $contactNo): string
    {
        $escapedContactNo = htmlspecialchars($contactNo, ENT_XML1 | ENT_QUOTES, 'UTF-8');

        // BC signature: GetTicketFile(TicketNo, EntryNo, ContactNo, ICISupportFilePort)
        // The old monolith omitted ticketNo — BC resolves it from EntryNo.
        return <<<XML
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:microsoft-dynamics-schemas/codeunit/ICISupportWebserviceAPI">
  <soap:Body>
    <urn:GetTicketFile>
      <urn:ticketNo></urn:ticketNo>
      <urn:entryNo>{$entryNo}</urn:entryNo>
      <urn:contactNo>{$escapedContactNo}</urn:contactNo>
      <urn:iCISupportFilePort></urn:iCISupportFilePort>
    </urn:GetTicketFile>
  </soap:Body>
</soap:Envelope>
XML;
    }

    /**
     * @return array{ok: bool, data?: string, filename?: string, error?: string}
     */
    private function parseGetTicketFileResponse(string $body): array
    {
        $body = trim($body);
        if ($body === '') {
            return ['ok' => false, 'error' => 'Empty SOAP response'];
        }

        $dom = new \DOMDocument();
        $previousLibxmlState = libxml_use_internal_errors(true);
        $loaded = $dom->loadXML($body);
        libxml_clear_errors();
        libxml_use_internal_errors($previousLibxmlState);
        if (!$loaded) {
            return ['ok' => false, 'error' => 'Invalid SOAP XML response'];
        }

        $xpath = new \DOMXPath($dom);

        $faultNode = $xpath->query("//*[local-name()='Fault']/*[local-name()='faultstring']")->item(0);
        if ($faultNode instanceof \DOMNode) {
            return ['ok' => false, 'error' => 'SOAP fault: ' . trim((string) $faultNode->textContent)];
        }

        $returnNode = $xpath->query("//*[local-name()='return_value']")->item(0);
        $returnValue = ($returnNode instanceof \DOMNode && is_numeric(trim((string) $returnNode->textContent)))
            ? (int) trim((string) $returnNode->textContent)
            : 0;

        if ($returnValue !== 100) {
            return ['ok' => false, 'error' => 'BC returned error code ' . $returnValue];
        }

        // Collect Base64 parts
        $partNodes = $xpath->query("//*[local-name()='PartData']");
        $base64 = '';
        if ($partNodes !== false) {
            foreach ($partNodes as $node) {
                $base64 .= trim((string) $node->textContent);
            }
        }

        if ($base64 === '') {
            return ['ok' => false, 'error' => 'No file data in response'];
        }

        // Handle data URI prefix (data:application/octet-stream;base64,...)
        if (str_contains($base64, ',')) {
            $base64 = substr($base64, strpos($base64, ',') + 1);
        }

        $decoded = base64_decode($base64, true);
        if ($decoded === false) {
            return ['ok' => false, 'error' => 'Base64 decode failed'];
        }

        return ['ok' => true, 'data' => $decoded];
    }

    private function extractSoapFault(string $body): string
    {
        if ($body === '') {
            return '';
        }

        $dom = new \DOMDocument();
        $prev = libxml_use_internal_errors(true);
        $loaded = $dom->loadXML($body);
        libxml_clear_errors();
        libxml_use_internal_errors($prev);
        if (!$loaded) {
            return mb_substr(trim($body), 0, 200);
        }

        $xpath = new \DOMXPath($dom);
        $faultNode = $xpath->query("//*[local-name()='Fault']/*[local-name()='faultstring']")->item(0);
        if ($faultNode instanceof \DOMNode) {
            return trim((string) $faultNode->textContent);
        }

        return mb_substr(trim($body), 0, 200);
    }

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

        $parsed = parse_url($baseUrl);
        if (!is_array($parsed)) {
            return null;
        }

        $scheme = strtolower((string) ($parsed['scheme'] ?? 'https'));
        $host = trim((string) ($parsed['host'] ?? ''));
        if ($host === '') {
            return null;
        }

        $odataPath = '/' . ltrim((string) ($parsed['path'] ?? ''), '/');
        $instancePath = (string) preg_replace('#/ODataV4(?:/.*)?$#i', '', $odataPath);
        $instancePath = rtrim($instancePath !== '' ? $instancePath : '/BusinessCentral', '/');
        if ($instancePath === '') {
            $instancePath = '/BusinessCentral';
        }

        $odataPort = (int) ($parsed['port'] ?? 0);
        $soapPort = $odataPort === 7048 ? 7047 : $odataPort;

        $authority = $host;
        if ($soapPort > 0 && !(($scheme === 'https' && $soapPort === 443) || ($scheme === 'http' && $soapPort === 80))) {
            $authority .= ':' . $soapPort;
        }

        $company = rawurlencode($this->settingsGateway->getCompanyName());

        return $scheme . '://' . $authority . $instancePath . '/WS/' . $company . '/Codeunit/' . self::CODEUNIT_NAME;
    }

    private function buildOAuthAudienceFromEndpoint(string $endpoint): ?string
    {
        $parsed = parse_url($endpoint);
        if (!is_array($parsed)) {
            return null;
        }

        $scheme = strtolower((string) ($parsed['scheme'] ?? 'https'));
        $host = trim((string) ($parsed['host'] ?? ''));
        if ($host === '') {
            return null;
        }

        $port = (int) ($parsed['port'] ?? 0);
        $authority = $host;
        if ($port > 0 && !(($scheme === 'https' && $port === 443) || ($scheme === 'http' && $port === 80))) {
            $authority .= ':' . $port;
        }

        return $scheme . '://' . $authority;
    }

    private function resolveCurrentTenantId(): int
    {
        $session = $this->sessionStore->all();
        $tenantId = (int) (($session['current_tenant']['id'] ?? 0));

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