<?php

declare(strict_types=1);

namespace Dental\Services;

use RuntimeException;
use Throwable;

final class GmailApiMailer
{
    private array $config;
    private string $transcript = '';

    public function __construct(array $config)
    {
        $this->config = array_merge([
            'client_id' => '',
            'client_secret' => '',
            'refresh_token' => '',
            'sender_email' => '',
            'timeout' => 30,
        ], $config);
    }

    public function send(string $fromEmail, string $fromName, string $toEmail, string $subject, string $html, ?string $text = null): void
    {
        $configuredSender = trim((string) ($this->config['sender_email'] ?? ''));
        if ($configuredSender !== '') {
            $fromEmail = $configuredSender;
        }
        $this->validateEmail($fromEmail, 'sender');
        $this->validateEmail($toEmail, 'recipient');
        $this->assertHeaderSafe($fromName);
        $this->assertHeaderSafe($subject);

        $accessToken = $this->accessToken();
        $raw = $this->base64UrlEncode($this->buildMessage($fromEmail, $fromName, $toEmail, $subject, $html, $text));
        $response = $this->httpJson(
            'https://gmail.googleapis.com/gmail/v1/users/me/messages/send',
            ['raw' => $raw],
            ['Authorization: Bearer ' . $accessToken]
        );

        if (empty($response['id'])) {
            throw new RuntimeException('Gmail API accepted the request but did not return a message id. Transcript: ' . $this->transcript);
        }
        $this->transcript .= 'I: Gmail API message accepted with id ' . (string) $response['id'] . "\n";
    }

    /**
     * Validate OAuth token exchange and Gmail profile access without sending mail.
     *
     * @return array{ok:bool,host:string,port:int,encryption:string,capabilities:array,transcript:string,message:string}
     */
    public function diagnose(): array
    {
        try {
            $this->accessToken();
            $email = trim((string) ($this->config['sender_email'] ?? ''));
            $message = $email !== ''
                ? 'Gmail API OAuth connection succeeded for ' . $email . '. SMTP is not used.'
                : 'Gmail API OAuth connection succeeded. SMTP is not used.';
            return [
                'ok' => true,
                'host' => 'gmail.googleapis.com',
                'port' => 443,
                'encryption' => 'https',
                'capabilities' => ['GMAIL_API' => 'messages.send profile'],
                'transcript' => $this->transcript,
                'message' => $message,
            ];
        } catch (Throwable $exception) {
            return [
                'ok' => false,
                'host' => 'gmail.googleapis.com',
                'port' => 443,
                'encryption' => 'https',
                'capabilities' => [],
                'transcript' => $this->transcript,
                'message' => $exception->getMessage(),
            ];
        }
    }

    public function getTranscript(): string
    {
        return $this->transcript;
    }

    private function accessToken(): string
    {
        foreach (['client_id' => 'Google OAuth Client ID', 'client_secret' => 'Google OAuth Client Secret', 'refresh_token' => 'Google OAuth Refresh Token'] as $key => $label) {
            if (trim((string) ($this->config[$key] ?? '')) === '') {
                throw new RuntimeException($label . ' is required for the Gmail API mail driver.');
            }
        }
        if (!extension_loaded('curl')) {
            throw new RuntimeException('The PHP cURL extension is required for the Gmail API mail driver.');
        }

        $response = $this->httpForm('https://oauth2.googleapis.com/token', [
            'client_id' => (string) $this->config['client_id'],
            'client_secret' => (string) $this->config['client_secret'],
            'refresh_token' => (string) $this->config['refresh_token'],
            'grant_type' => 'refresh_token',
        ]);
        $token = (string) ($response['access_token'] ?? '');
        if ($token === '') {
            throw new RuntimeException('Google OAuth did not return an access token. Reconnect the Gmail account and confirm the refresh token has the gmail.send scope.');
        }
        $this->transcript .= 'I: OAuth token exchange succeeded; access token omitted.\n';
        return $token;
    }

    private function httpForm(string $url, array $fields): array
    {
        return $this->http($url, http_build_query($fields, '', '&'), ['Content-Type: application/x-www-form-urlencoded']);
    }

    private function httpJson(string $url, ?array $payload, array $headers = []): array
    {
        $body = $payload === null ? null : json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        if ($body === false) {
            throw new RuntimeException('Unable to encode Gmail API request payload.');
        }
        $headers[] = 'Content-Type: application/json';
        return $this->http($url, $body, $headers);
    }

    private function http(string $url, ?string $body, array $headers): array
    {
        if (!extension_loaded('curl')) {
            throw new RuntimeException('The PHP cURL extension is required for Gmail API delivery.');
        }
        $timeout = max(10, min(120, (int) ($this->config['timeout'] ?? 30)));
        $ch = curl_init($url);
        if ($ch === false) {
            throw new RuntimeException('Unable to initialize HTTPS request for Gmail API.');
        }

        $options = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => false,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_TIMEOUT => $timeout,
            CURLOPT_CONNECTTIMEOUT => min(30, $timeout),
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_HTTPHEADER => $headers,
        ];
        if ($body !== null) {
            $options[CURLOPT_POST] = true;
            $options[CURLOPT_POSTFIELDS] = $body;
        }
        curl_setopt_array($ch, $options);
        $raw = curl_exec($ch);
        $errno = curl_errno($ch);
        $error = curl_error($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        curl_close($ch);

        $this->transcript .= sprintf("I: HTTPS %s returned HTTP %d\n", preg_replace('/\?.*/', '', $url), $status);
        if ($raw === false || $errno !== 0) {
            throw new RuntimeException('Gmail API HTTPS request failed: ' . ($error ?: 'cURL error ' . $errno));
        }
        $decoded = json_decode((string) $raw, true);
        if (!is_array($decoded)) {
            throw new RuntimeException('Gmail API returned a non-JSON response with HTTP ' . $status . '.');
        }
        if ($status < 200 || $status >= 300) {
            $message = (string) ($decoded['error_description'] ?? $decoded['error']['message'] ?? 'Gmail API request failed.');
            throw new RuntimeException('Gmail API error HTTP ' . $status . ': ' . $message);
        }
        return $decoded;
    }

    private function buildMessage(string $fromEmail, string $fromName, string $toEmail, string $subject, string $html, ?string $text): string
    {
        $boundary = '=_Dental_Gmail_' . bin2hex(random_bytes(12));
        $text = $text ?? trim(html_entity_decode(strip_tags(str_replace(['<br>', '<br/>', '<br />'], "\n", $html)), ENT_QUOTES | ENT_HTML5, 'UTF-8'));

        $headers = [
            'Date: ' . date(DATE_RFC2822),
            'From: ' . $this->formatAddress($fromEmail, $fromName),
            'To: <' . $toEmail . '>',
            'Subject: ' . $this->encodeHeader($subject),
            'MIME-Version: 1.0',
            'Content-Type: multipart/alternative; boundary="' . $boundary . '"',
        ];

        $body = implode("\r\n", $headers) . "\r\n\r\n";
        $body .= '--' . $boundary . "\r\n";
        $body .= "Content-Type: text/plain; charset=UTF-8\r\n";
        $body .= "Content-Transfer-Encoding: base64\r\n\r\n";
        $body .= chunk_split(base64_encode($text), 76, "\r\n") . "\r\n";
        $body .= '--' . $boundary . "\r\n";
        $body .= "Content-Type: text/html; charset=UTF-8\r\n";
        $body .= "Content-Transfer-Encoding: base64\r\n\r\n";
        $body .= chunk_split(base64_encode($html), 76, "\r\n") . "\r\n";
        $body .= '--' . $boundary . "--\r\n";

        return preg_replace('/(?m)^\./', '..', $body) ?? $body;
    }

    private function formatAddress(string $email, string $name): string
    {
        $name = trim($name);
        if ($name === '') {
            return '<' . $email . '>';
        }
        return $this->encodeHeader($name) . ' <' . $email . '>';
    }

    private function encodeHeader(string $value): string
    {
        if (preg_match('/^[\x20-\x7E]*$/', $value)) {
            return $value;
        }
        return '=?UTF-8?B?' . base64_encode($value) . '?=';
    }

    private function base64UrlEncode(string $value): string
    {
        return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
    }

    private function validateEmail(string $email, string $label): void
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new RuntimeException('Invalid ' . $label . ' email address.');
        }
    }

    private function assertHeaderSafe(string $value): void
    {
        if (preg_match('/[\r\n]/', $value)) {
            throw new RuntimeException('Email headers cannot contain line breaks.');
        }
    }
}
