<?php

declare (strict_types = 1);
namespace Brightfern\Transmission;

use Brightfern\Support\Http\HttpClient;
// Send-side types are premium-only and referenced by fully-qualified name
// inside the send methods below. Deliberately no `use` imports: those would
// survive into the free build as dangling references to a stripped path.
/**
 * Concrete Access Point provider for Storecove (https://www.storecove.com).
 * Chosen as the reference implementation because it has public REST docs, a
 * sandbox, and covers BOTH send and receive over Peppol.
 *
 * Configuration (never hardcode — comes from settings):
 *   - apiKey        : Bearer token from the Storecove dashboard.
 *   - legalEntityId : the sender legal entity created in Storecove.
 *   - baseUrl       : https://api.storecove.com/api/v2 (prod) — same host has
 *                     sandbox keys; use a test legal entity for sandbox.
 *
 * NOTE ON API SHAPE: the exact JSON field names below follow Storecove's v2
 * document_submissions API (raw-document submission). Where a field name could
 * drift between API versions it is marked; verify against the current docs when
 * wiring real credentials. All network failures return a failed result rather
 * than throwing, per the interface contract.
 */
final class StorecoveProvider implements AccessPointProvider {
    public function __construct(
        private HttpClient $http,
        private string $apiKey,
        private string $legalEntityId,
        private string $baseUrl = 'https://api.storecove.com/api/v2'
    ) {
    }

    public function id() : string {
        return 'storecove';
    }

    public function label() : string {
        return 'Storecove';
    }

    /**
     * The $document type lives ONLY in the docblock, never in the signature:
     * this method is private and carries no __premium_only suffix, so Freemius
     * leaves it in the free build — where Send/ has been stripped. A real type
     * hint would be a dangling reference there (and bin/verify-free-build.py
     * greps for exactly that, comments excluded, so a docblock stays legible
     * to IDEs and static analysis while being invisible to the gate).
     *
     * @param \Brightfern\Transmission\Send\OutboundDocument $document
     * @return array<string,mixed>
     */
    private function buildPeppolBody( $document ) : array {
        // Submit the already-validated UBL as a raw document; Storecove routes it
        // over Peppol to the recipient electronic address.
        return array(
            'legalEntityId' => ( is_numeric( $this->legalEntityId ) ? (int) $this->legalEntityId : $this->legalEntityId ),
            'routing'       => array(
                'eIdentifiers' => array(array(
                    'scheme' => $document->recipientScheme,
                    'id'     => $document->recipientId,
                )),
            ),
            'document'      => array(
                'documentType'    => $document->documentType,
                'rawDocumentData' => array(
                    'document'           => base64_encode( $document->xml ),
                    'parse'              => true,
                    'parsedDocumentType' => $document->documentType,
                ),
            ),
        );
    }

    /**
     * NOTE ON API SHAPE — UNVERIFIED: built from Storecove's public
     * marketing/docs description of their KSeF "V-flow" (submit through the
     * same /document_submissions endpoint; Storecove converts to FA(3) and
     * files it with KSeF itself), not their authenticated API reference —
     * we don't have Storecove sandbox access with KSeF/V-flow enabled yet.
     * Unlike the Peppol body, there is no recipient network address to route
     * to (KSeF is a clearance destination, not a delivery address), so
     * `routing.eIdentifiers` is replaced with the seller's own NIP, and
     * `legalEntityId` is assumed to carry the KSeF-registration context the
     * same way it already does for Peppol. Correct this against Storecove's
     * real docs once sandbox access exists — see docs/DECISIONS.md.
     *
     * Untyped $document for the same free-build reason as buildPeppolBody().
     *
     * @param \Brightfern\Transmission\Send\OutboundDocument $document
     * @return array<string,mixed>
     */
    private function buildKsefBody( $document ) : array {
        return array(
            'legalEntityId' => ( is_numeric( $this->legalEntityId ) ? (int) $this->legalEntityId : $this->legalEntityId ),
            'routing'       => array(
                'taxIdentifiers' => array(array(
                    'country' => $document->countryCode,
                    'id'      => (string) ($document->meta['sellerNip'] ?? ''),
                )),
            ),
            'document'      => array(
                'documentType'    => $document->documentType,
                'rawDocumentData' => array(
                    'document'           => base64_encode( $document->xml ),
                    'parse'              => false,
                    'parsedDocumentType' => $document->format,
                ),
            ),
        );
    }

    public function supportsReceiving() : bool {
        return true;
    }

    public function handleIncoming( array $payload ) : ?InboundInvoice {
        // Storecove fires several event types; only act on received purchase
        // invoices. The exact discriminator depends on the configured webhook —
        // we check the common shapes defensively.
        $event = (string) ($payload['event'] ?? $payload['type'] ?? '');
        if ( '' !== $event && !str_contains( strtolower( $event ), 'received' ) && !str_contains( strtolower( $event ), 'purchase' ) ) {
            return null;
        }
        $guid = (string) ($payload['guid'] ?? $payload['id'] ?? '');
        if ( '' === $guid ) {
            return null;
        }
        // The webhook may carry the document inline (base64) or only a reference.
        $xml = '';
        if ( isset( $payload['document'] ) && is_string( $payload['document'] ) ) {
            $decoded = base64_decode( $payload['document'], true );
            $xml = ( false !== $decoded ? $decoded : $payload['document'] );
        } elseif ( '' !== $guid ) {
            $xml = $this->fetchDocument( $guid );
        }
        if ( '' === $xml ) {
            return null;
        }
        return new InboundInvoice(
            $guid,
            (string) ($payload['sender'] ?? $payload['senderId'] ?? ''),
            (string) ($payload['recipient'] ?? $payload['receiverId'] ?? ''),
            (string) ($payload['documentType'] ?? 'invoice'),
            $xml,
            (string) ($payload['receivedAt'] ?? ''),
            'peppol-bis-billing-3.0',
            $payload
        );
    }

    public function poll() : array {
        // Polling fallback: list received documents and normalize them.
        $response = $this->http->get( $this->baseUrl . '/purchase_invoices', $this->headers() );
        if ( !$response->ok() ) {
            return array();
        }
        $json = $response->json() ?? array();
        $items = ( is_array( $json['items'] ?? null ) ? $json['items'] : (( is_array( $json ) ? $json : array() )) );
        $results = array();
        foreach ( $items as $item ) {
            if ( !is_array( $item ) ) {
                continue;
            }
            $inbound = $this->handleIncoming( $item );
            if ( null !== $inbound ) {
                $results[] = $inbound;
            }
        }
        return $results;
    }

    private function fetchDocument( string $guid ) : string {
        $response = $this->http->get( $this->baseUrl . '/purchase_invoices/' . rawurlencode( $guid ) . '/raw', $this->headers() );
        if ( !$response->ok() ) {
            return '';
        }
        // Body may be raw XML or a JSON wrapper with base64.
        $json = $response->json();
        if ( null !== $json && isset( $json['document'] ) && is_string( $json['document'] ) ) {
            $decoded = base64_decode( $json['document'], true );
            return ( false !== $decoded ? $decoded : '' );
        }
        return $response->body;
    }

    /** @return array<string,string> */
    private function headers() : array {
        return array(
            'Authorization' => 'Bearer ' . $this->apiKey,
            'Content-Type'  => 'application/json',
            'Accept'        => 'application/json',
        );
    }

    /** JSON-encode that works with or without WordPress loaded (tests). */
    private function encode( mixed $data ) : string {
        if ( function_exists( 'wp_json_encode' ) ) {
            return (string) \wp_json_encode( $data );
        }
        return (string) json_encode( $data );
    }

}
