<?php
/**
 * ZeptoMail 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 ZeptoMailTransport
 *
 * SMTP-shaped subclass that PHPMailer drives like a normal transport but
 * which actually delivers the message via HTTPS POST to ZeptoMail's
 * `/v1.1/email` endpoint at the configured region. 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 ZeptoMailTransport extends SMTP {

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

    /**
     * Maximum allowed total raw attachment bytes (~11MB).
     *
     * ZeptoMail accepts up to ~15MB of base64-encoded attachment payload.
     * Capping the raw input at 11MB keeps us comfortably under the limit
     * after base64 expansion.
     *
     * @since 1.0.0
     *
     * @var int
     */
    private const MAX_ATTACHMENT_BYTES = 11534336;

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

    /**
     * ZeptoMail Send Mail Token used to authenticate the HTTPS request.
     *
     * @since 1.0.0
     *
     * @var string
     */
    private string $api_key;

    /**
     * Regional TLD suffix for the ZeptoMail API host (e.g. `com`, `eu`).
     *
     * @since 1.0.0
     *
     * @var string
     */
    private string $region;

    /**
     * Constructor.
     *
     * @since 1.0.0
     *
     * @param PHPMailer $phpmailer The PHPMailer instance whose state will be read at `data()` time.
     * @param string    $api_key   The ZeptoMail Send Mail Token.
     * @param string    $region    The regional TLD suffix (`com`, `eu`, `in`, `com.cn`, `com.au`).
     */
    public function __construct( PHPMailer $phpmailer, string $api_key, string $region ) {
        $this->phpmailer = $phpmailer;
        $this->api_key   = $api_key;
        $this->region    = $region;
    }

    /**
     * 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 ZeptoMail.
     *
     * 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 when ZeptoMail returns 2xx with `EM_104` codes for every recipient, false otherwise.
     * @throws Exception When the HTTP transport itself fails or ZeptoMail 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(
                'https://api.zeptomail.' . $this->region . '/v1.1/email',
                array(
                    'timeout' => self::HTTP_TIMEOUT,
                    'headers' => array(
                        'Authorization' => 'Zoho-enczapikey ' . $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 ) && $this->is_success_response( $data ) ) {
                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 ZeptoMail'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 ZeptoMail.', 'nimblymail-smtp' ) );
            }
        }
    }

    /**
     * Build the JSON payload sent to ZeptoMail's `/v1.1/email` endpoint.
     *
     * @since 1.0.0
     *
     * @return array The payload ready for `wp_json_encode()`.
     */
    private function build_payload(): array {
        $payload = array(
            'from'    => $this->build_flat_address( $this->phpmailer->From, $this->phpmailer->FromName ),
            'to'      => $this->build_envelope_list( $this->phpmailer->getToAddresses() ),
            'subject' => $this->phpmailer->Subject,
        );

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

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

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

        $reply_to = $this->phpmailer->getReplyToAddresses();
        if ( ! empty( $reply_to ) ) {
            $reply_to_list = array();
            foreach ( $reply_to as $addr ) {
                if ( ! empty( $addr[0] ) ) {
                    $reply_to_list[] = $this->build_flat_address( $addr[0], $addr[1] ?? '' );
                }
            }
            if ( ! empty( $reply_to_list ) ) {
                $payload['reply_to'] = $reply_to_list;
            }
        }

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

        return $payload;
    }

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

    /**
     * Convert PHPMailer's address tuples into ZeptoMail envelope objects.
     *
     * ZeptoMail wraps each recipient address in a `{ "email_address": ... }`
     * envelope rather than passing them as flat objects.
     *
     * @since 1.0.0
     *
     * @param array $addresses PHPMailer-style `[ [email, name], ... ]` array.
     *
     * @return array List of ZeptoMail envelope objects.
     */
    private function build_envelope_list( array $addresses ): array {
        $list = array();
        foreach ( $addresses as $addr ) {
            if ( empty( $addr[0] ) ) {
                continue;
            }
            $list[] = array(
                'email_address' => $this->build_flat_address( $addr[0], $addr[1] ?? '' ),
            );
        }
        return $list;
    }

    /**
     * Convert PHPMailer attachments into ZeptoMail's two-bucket structure.
     *
     * ZeptoMail separates regular attachments from inline images, and
     * inline entries require a `cid`. File-backed attachments are read
     * from disk and base64-encoded; string attachments are encoded
     * directly. Empty content is skipped silently.
     *
     * @since 1.0.0
     *
     * @param array $attachments PHPMailer attachments tuple list.
     *
     * @return array{attachments: array, inline_images: array}
     */
    private function build_attachments( array $attachments ): array {
        $regular = array();
        $inline  = array();
        foreach ( $attachments as $attachment ) {
            if ( $attachment[5] ) {
                // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- ZeptoMail 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 -- ZeptoMail API requires base64-encoded attachment content.
                $content = false !== $raw ? base64_encode( $raw ) : '';
            }

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

            $item = array(
                'mime_type' => ! empty( $attachment[4] ) ? $attachment[4] : 'application/octet-stream',
                'content'   => $content,
            );

            if ( 'inline' === $attachment[6] && ! empty( $attachment[7] ) ) {
                $item['cid'] = $attachment[7];
                $inline[]    = $item;
            } else {
                $item['name'] = ! empty( $attachment[2] ) ? $attachment[2] : $attachment[1];
                $regular[]    = $item;
            }
        }
        return array(
            'attachments'   => $regular,
            'inline_images' => $inline,
        );
    }

    /**
     * Determine whether ZeptoMail's response indicates success for every recipient.
     *
     * ZeptoMail uses HTTP 201 with a `data` array of per-recipient entries.
     * Each entry must carry the `EM_104` success code; any other code on
     * any recipient is treated as a partial or full failure.
     *
     * @since 1.0.0
     *
     * @param array|null $data Decoded JSON body of the response.
     *
     * @return bool True when every entry reports `EM_104`, false otherwise.
     */
    private function is_success_response( ?array $data ): bool {
        if ( ! isset( $data['data'] ) || ! is_array( $data['data'] ) ) {
            return false;
        }
        foreach ( $data['data'] as $entry ) {
            if ( ( $entry['code'] ?? '' ) !== 'EM_104' ) {
                return false;
            }
        }
        return true;
    }

    /**
     * Format a ZeptoMail 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 ) ) {
            if ( isset( $data['error']['message'] ) ) {
                $message = (string) $data['error']['message'];
            } elseif ( isset( $data['message'] ) ) {
                $message = (string) $data['message'];
            } elseif ( isset( $data['code'] ) ) {
                $message = (string) $data['code'];
            }
        }
        if ( '' === $message ) {
            $message = sprintf( 'HTTP %d', $http_code );
        }
        return sprintf(
            /* translators: %s: error message returned by the ZeptoMail API. */
            __( 'ZeptoMail API Error: %s', 'nimblymail-smtp' ),
            $message
        );
    }
}
