<?php
/**
 * Brevo Transport Implementation.
 *
 * @package NimblyMail\SMTP
 *
 * @since 1.0.0
 */

namespace NimblyMail\SMTP\Mailers;

use Exception;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;

defined( 'ABSPATH' ) || exit;

/**
 * Class BrevoTransport
 *
 * SMTP-shaped subclass that PHPMailer drives like a normal transport but
 * which actually delivers the message via HTTPS POST to Brevo's
 * `/v3/smtp/email` endpoint. The `connect()`/`hello()`/`mail()`/`recipient()`
 * stubs all return true so PHPMailer's state machine reaches `data()`,
 * which builds the JSON payload and dispatches the request.
 *
 * @since 1.0.0
 */
class BrevoTransport extends SMTP {

    /**
     * HTTP request timeout, in seconds.
     *
     * @since 1.0.0
     *
     * @var int
     */
    private const HTTP_TIMEOUT = 30;

    /**
     * Maximum allowed total raw attachment bytes (~7MB).
     *
     * Brevo rejects requests whose combined attachment payload exceeds
     * roughly 10MB (after base64 expansion). 7MB raw keeps us well under
     * the limit.
     *
     * @since 1.0.0
     *
     * @var int
     */
    private const MAX_ATTACHMENT_BYTES = 7340032;

    /**
     * Brevo Transactional Email API endpoint.
     *
     * @since 1.0.0
     *
     * @var string
     */
    private const API_ENDPOINT = 'https://api.brevo.com/v3/smtp/email';

    /**
     * The PHPMailer instance whose state we read when building the payload.
     *
     * @since 1.0.0
     *
     * @var PHPMailer
     */
    private PHPMailer $phpmailer;

    /**
     * Brevo API key used to authenticate the HTTPS request.
     *
     * @since 1.0.0
     *
     * @var string
     */
    private string $api_key;

    /**
     * Constructor.
     *
     * @since 1.0.0
     *
     * @param PHPMailer $phpmailer The PHPMailer instance whose state will be read at `data()` time.
     * @param string    $api_key   The Brevo API key.
     */
    public function __construct( PHPMailer $phpmailer, string $api_key ) {
        $this->phpmailer = $phpmailer;
        $this->api_key   = $api_key;
    }

    /**
     * Stubbed SMTP connect — always succeeds.
     *
     * @since 1.0.0
     *
     * @param string   $host    Ignored (no real SMTP connection is opened).
     * @param int|null $port    Ignored.
     * @param int      $timeout Ignored.
     * @param array    $options Ignored.
     *
     * @return bool Always true.
     */
    public function connect( $host, $port = null, $timeout = 30, $options = array() ) {
        return true;
    }

    /**
     * Stubbed SMTP STARTTLS — always succeeds.
     *
     * @since 1.0.0
     *
     * @return bool Always true.
     */
    public function startTLS() {
        return true;
    }

    /**
     * Stubbed SMTP authenticate — always succeeds.
     *
     * @since 1.0.0
     *
     * @param string      $username Ignored.
     * @param string      $password Ignored.
     * @param string|null $authtype Ignored.
     * @param mixed       $OAuth    Ignored.
     *
     * @return bool Always true.
     */
    public function authenticate( $username, $password, $authtype = null, $OAuth = null ) {
        return true;
    }

    /**
     * Stubbed SMTP HELO/EHLO — always succeeds.
     *
     * @since 1.0.0
     *
     * @param string $host Ignored.
     *
     * @return bool Always true.
     */
    public function hello( $host = '' ) {
        return true;
    }

    /**
     * Stubbed SMTP MAIL FROM — always succeeds.
     *
     * @since 1.0.0
     *
     * @param string $from Ignored — sender comes from PHPMailer state.
     *
     * @return bool Always true.
     */
    public function mail( $from ) {
        return true;
    }

    /**
     * Stubbed SMTP RCPT TO — always succeeds.
     *
     * @since 1.0.0
     *
     * @param string $address Ignored — recipients come from PHPMailer state.
     * @param string $dsn     Ignored.
     *
     * @return bool Always true.
     */
    public function recipient( $address, $dsn = '' ) {
        return true;
    }

    /**
     * Build the API payload and POST it to Brevo.
     *
     * Replaces SMTP's DATA phase. Captures any failure into `$this->error`
     * so PHPMailer surfaces a useful error message via `wp_mail_failed`.
     *
     * @since 1.0.0
     *
     * @param string $msg_data Raw RFC-5322 message body assembled by PHPMailer; unused — we build our own JSON payload.
     *
     * @return bool True on a 2xx response with a `messageId`, false otherwise.
     * @throws Exception When the HTTP transport itself fails or Brevo returns an error code; caught internally and converted to a soft failure.
     */
    public function data( $msg_data ) {
        try {
            $this->guard_attachment_size();
            $payload = $this->build_payload();

            $response = wp_remote_post(
                self::API_ENDPOINT,
                array(
                    'timeout' => self::HTTP_TIMEOUT,
                    'headers' => array(
                        'api-key'      => $this->api_key,
                        'accept'       => 'application/json',
                        'content-type' => 'application/json',
                    ),
                    'body'    => wp_json_encode( $payload ),
                )
            );

            if ( is_wp_error( $response ) ) {
                throw new Exception( $response->get_error_message() );
            }

            $code = (int) wp_remote_retrieve_response_code( $response );
            $body = (string) wp_remote_retrieve_body( $response );
            $data = json_decode( $body, true );

            if ( ( 200 === $code || 201 === $code || 202 === $code ) && isset( $data['messageId'] ) ) {
                return true;
            }

            throw new Exception( $this->parse_api_error( $data, $code ) );
        } catch ( Exception $e ) {
            $this->error = array(
                'error'  => $e->getMessage(),
                'detail' => $e->getCode(),
            );
            return false;
        }
    }

    /**
     * Stubbed SMTP QUIT — always succeeds.
     *
     * @since 1.0.0
     *
     * @param bool $close_on_error Ignored.
     *
     * @return bool Always true.
     */
    public function quit( $close_on_error = true ) {
        return true;
    }

    /**
     * Stubbed SMTP close — no-op.
     *
     * @since 1.0.0
     *
     * @return void
     */
    public function close() {}

    /**
     * Reject sends whose attachments exceed Brevo's payload size limit.
     *
     * @since 1.0.0
     *
     * @return void
     * @throws Exception When the cumulative attachment payload exceeds MAX_ATTACHMENT_BYTES.
     */
    private function guard_attachment_size(): void {
        $total = 0;
        foreach ( $this->phpmailer->getAttachments() as $attachment ) {
            if ( $attachment[5] ) {
                $total += strlen( $attachment[0] );
            } elseif ( ! empty( $attachment[0] ) && file_exists( $attachment[0] ) ) {
                $total += filesize( $attachment[0] );
            }

            if ( $total > self::MAX_ATTACHMENT_BYTES ) {
                throw new Exception( esc_html__( 'Email too large for Brevo.', 'nimblymail-smtp' ) );
            }
        }
    }

    /**
     * Build the JSON payload sent to Brevo's `/v3/smtp/email` endpoint.
     *
     * @since 1.0.0
     *
     * @return array The payload ready for `wp_json_encode()`.
     */
    private function build_payload(): array {
        $payload = array(
            'sender'  => $this->build_address( $this->phpmailer->From, $this->phpmailer->FromName ),
            'to'      => $this->build_address_list( $this->phpmailer->getToAddresses() ),
            'subject' => $this->phpmailer->Subject,
        );

        if ( 'text/html' === $this->phpmailer->ContentType ) {
            $payload['htmlContent'] = $this->phpmailer->Body;
            if ( ! empty( $this->phpmailer->AltBody ) ) {
                $payload['textContent'] = $this->phpmailer->AltBody;
            }
        } else {
            $payload['textContent'] = $this->phpmailer->Body;
        }

        $cc = $this->build_address_list( $this->phpmailer->getCcAddresses() );
        if ( ! empty( $cc ) ) {
            $payload['cc'] = $cc;
        }

        $bcc = $this->build_address_list( $this->phpmailer->getBccAddresses() );
        if ( ! empty( $bcc ) ) {
            $payload['bcc'] = $bcc;
        }

        $reply_to = $this->build_reply_to();
        if ( $reply_to ) {
            $payload['replyTo'] = $reply_to;
        }

        $headers = $this->build_custom_headers();
        if ( ! empty( $headers ) ) {
            $payload['headers'] = $headers;
        }

        $attachments = $this->build_attachments( $this->phpmailer->getAttachments() );
        if ( ! empty( $attachments ) ) {
            $payload['attachment'] = $attachments;
        }

        return $payload;
    }

    /**
     * Build a Brevo `{email, name?}` address object.
     *
     * @since 1.0.0
     *
     * @param string $email The email address.
     * @param string $name  The display name (omitted when empty).
     *
     * @return array Brevo address object.
     */
    private function build_address( string $email, string $name ): array {
        $object = array( 'email' => $email );
        if ( ! empty( $name ) ) {
            $object['name'] = $name;
        }
        return $object;
    }

    /**
     * Convert PHPMailer's address tuples into Brevo address objects.
     *
     * @since 1.0.0
     *
     * @param array $addresses PHPMailer-style `[ [email, name], ... ]` array.
     *
     * @return array List of Brevo `{email, name?}` objects.
     */
    private function build_address_list( array $addresses ): array {
        $list = array();
        foreach ( $addresses as $addr ) {
            if ( empty( $addr[0] ) ) {
                continue;
            }
            $list[] = $this->build_address( $addr[0], $addr[1] ?? '' );
        }
        return $list;
    }

    /**
     * Resolve the first reply-to address into a Brevo address object.
     *
     * Brevo's API accepts a single replyTo. When PHPMailer has multiple
     * reply-to addresses configured, only the first non-empty one is sent.
     *
     * @since 1.0.0
     *
     * @return array|null The replyTo object, or null when none is configured.
     */
    private function build_reply_to(): ?array {
        foreach ( $this->phpmailer->getReplyToAddresses() as $addr ) {
            if ( ! empty( $addr[0] ) ) {
                return $this->build_address( $addr[0], $addr[1] ?? '' );
            }
        }
        return null;
    }

    /**
     * Convert PHPMailer's custom-headers list into Brevo's headers map.
     *
     * @since 1.0.0
     *
     * @return array Map of `header-name => header-value`.
     */
    private function build_custom_headers(): array {
        $headers = array();
        foreach ( $this->phpmailer->getCustomHeaders() as $custom ) {
            if ( ! empty( $custom[0] ) ) {
                $headers[ $custom[0] ] = $custom[1];
            }
        }
        return $headers;
    }

    /**
     * Convert PHPMailer attachments into Brevo's `[{name, content}, ...]`.
     *
     * Reads file-backed attachments from disk and base64-encodes them.
     * Inline string attachments are encoded directly. Empty results are
     * skipped silently.
     *
     * @since 1.0.0
     *
     * @param array $attachments PHPMailer attachments tuple list.
     *
     * @return array Brevo-shaped attachment array.
     */
    private function build_attachments( array $attachments ): array {
        $built = array();
        foreach ( $attachments as $attachment ) {
            if ( $attachment[5] ) {
                // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Brevo API requires base64-encoded attachment content.
                $content = base64_encode( $attachment[0] );
            } else {
                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading local attachment file from PHPMailer.
                $raw = file_get_contents( $attachment[0] );
                // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Brevo API requires base64-encoded attachment content.
                $content = false !== $raw ? base64_encode( $raw ) : '';
            }

            if ( empty( $content ) ) {
                continue;
            }

            $built[] = array(
                'name'    => ! empty( $attachment[2] ) ? $attachment[2] : ( ! empty( $attachment[1] ) ? $attachment[1] : 'attachment' ),
                'content' => $content,
            );
        }
        return $built;
    }

    /**
     * Format a Brevo API error response into a human-readable message.
     *
     * @since 1.0.0
     *
     * @param mixed $data      Decoded JSON body, or null when the response wasn't JSON.
     * @param int   $http_code The HTTP status code returned by the request.
     *
     * @return string Translated error message safe for surfacing in `wp_mail_failed`.
     */
    private function parse_api_error( $data, int $http_code ): string {
        $message = '';
        if ( is_array( $data ) ) {
            $message = (string) ( $data['message'] ?? ( $data['code'] ?? '' ) );
        }
        if ( '' === $message ) {
            $message = sprintf( 'HTTP %d', $http_code );
        }
        return sprintf(
            /* translators: %s: error message returned by the Brevo API. */
            __( 'Brevo API Error: %s', 'nimblymail-smtp' ),
            $message
        );
    }
}
