<?php

declare(strict_types=1);

namespace Dental\Services;

use PDO;
use RuntimeException;
use Throwable;

final class EmailService
{
    private const MAX_ATTEMPTS = 5;
    private const STALE_PROCESSING_MINUTES = 15;

    public function __construct(private array $config, private ?PDO $pdo = null)
    {
    }

    public function send(string $to, string $subject, string $html, ?string $text = null, array $meta = []): void
    {
        $mail = $this->config['mail'] ?? [];
        $driver = $mail['driver'] ?? 'smtp';
        $fromEmail = (string) ($mail['from_email'] ?? '');
        $fromName = (string) ($mail['from_name'] ?? 'Dental Clinic');

        try {
            if ($driver === 'mail') {
                $this->sendUsingPhpMail($fromEmail, $fromName, $to, $subject, $html);
            } elseif ($driver === 'log') {
                $this->writeLog($to, $subject, $html);
                $this->recordLog($to, $subject, 'failed', 'Log-only mode recorded the message but did not deliver it.', $meta);
                return;
            } elseif ($driver === 'gmail_api') {
                $gmail = new GmailApiMailer($mail['gmail_api'] ?? []);
                $gmail->send($fromEmail, $fromName, $to, $subject, $html, $text);
            } else {
                $smtp = new SmtpMailer($mail['smtp'] ?? []);
                $smtp->send($fromEmail, $fromName, $to, $subject, $html, $text);
            }
            $this->recordLog($to, $subject, 'sent', null, $meta);
        } catch (Throwable $e) {
            $this->recordLog($to, $subject, 'failed', $e->getMessage(), $meta);
            throw $e;
        }
    }

    public function queue(string $to, string $subject, string $html, ?string $text = null, array $meta = []): int
    {
        if (!$this->pdo) {
            throw new RuntimeException('A database connection is required to queue email.');
        }
        if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
            throw new RuntimeException('A valid recipient email address is required.');
        }
        if (trim($subject) === '' || trim($html) === '') {
            throw new RuntimeException('Email subject and message content are required.');
        }

        $prefix = (string) (($this->config['database']['prefix'] ?? 'dent_'));
        $stmt = $this->pdo->prepare(
            "INSERT INTO `{$prefix}email_outbox` (recipient_email, subject, html_body, text_body, metadata_json, status, attempts, available_at, created_at)\n"
            . "VALUES (:recipient, :subject, :html, :text, :metadata, 'pending', 0, NOW(), NOW())"
        );
        $stmt->execute([
            ':recipient' => strtolower(trim($to)),
            ':subject' => $subject,
            ':html' => $html,
            ':text' => $text,
            ':metadata' => json_encode($meta, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
        ]);

        return (int) $this->pdo->lastInsertId();
    }

    /**
     * Process due messages under one database advisory lock.
     *
     * @return array{selected:int,sent:int,retry:int,failed:int,skipped:int,recovered:int,busy:bool}
     */
    public function processQueue(int $limit = 10, int $staleProcessingMinutes = self::STALE_PROCESSING_MINUTES): array
    {
        if (!$this->pdo) {
            throw new RuntimeException('A database connection is required to process email.');
        }

        $prefix = (string) (($this->config['database']['prefix'] ?? 'dent_'));
        $limit = max(1, min(100, $limit));
        $staleProcessingMinutes = max(5, min(1440, $staleProcessingMinutes));
        $result = ['selected' => 0, 'sent' => 0, 'retry' => 0, 'failed' => 0, 'skipped' => 0, 'recovered' => 0, 'busy' => false];
        $lockName = substr('dentalcare_email_queue_' . preg_replace('/[^a-z0-9_]/i', '_', $prefix), 0, 60);

        $lockStmt = $this->pdo->prepare('SELECT GET_LOCK(?,0)');
        $lockStmt->execute([$lockName]);
        if ((int) ($lockStmt->fetchColumn() ?: 0) !== 1) {
            $result['busy'] = true;
            return $result;
        }

        try {
            $result['recovered'] = $this->recoverStaleProcessing($staleProcessingMinutes);
            $rows = $this->pdo->query(
                "SELECT id FROM `{$prefix}email_outbox`\n"
                . "WHERE status IN ('pending','retry') AND available_at <= NOW() AND attempts < " . self::MAX_ATTEMPTS . "\n"
                . "ORDER BY available_at ASC,id ASC LIMIT {$limit}"
            )->fetchAll(PDO::FETCH_COLUMN);
            $result['selected'] = count($rows);

            foreach ($rows as $id) {
                try {
                    $delivery = $this->processMessage((int) $id);
                    $status = (string) ($delivery['status'] ?? 'skipped');
                    if (isset($result[$status])) {
                        $result[$status]++;
                    } else {
                        $result['skipped']++;
                    }
                } catch (Throwable $messageError) {
                    error_log('Queued email #' . (int)$id . ' could not be processed: ' . $messageError->getMessage());
                    $result['failed']++;
                }
            }
        } finally {
            try {
                $release = $this->pdo->prepare('SELECT RELEASE_LOCK(?)');
                $release->execute([$lockName]);
            } catch (Throwable) {
                // The database connection releases advisory locks automatically.
            }
        }

        return $result;
    }

    public function recoverStaleProcessing(int $minutes = self::STALE_PROCESSING_MINUTES): int
    {
        if (!$this->pdo) {
            throw new RuntimeException('A database connection is required to recover email.');
        }
        $minutes = max(5, min(1440, $minutes));
        $prefix = (string) (($this->config['database']['prefix'] ?? 'dent_'));
        $message = 'Recovered after the previous queue worker stopped before recording a final result.';
        $failed = $this->pdo->prepare(
            "UPDATE `{$prefix}email_outbox` SET status='failed',last_error=?,updated_at=NOW()\n"
            . "WHERE status='processing' AND COALESCE(last_attempt_at,created_at)<DATE_SUB(NOW(),INTERVAL {$minutes} MINUTE) AND attempts>=" . self::MAX_ATTEMPTS
        );
        $failed->execute([$message . ' The maximum attempt count had already been reached.']);
        $stmt = $this->pdo->prepare(
            "UPDATE `{$prefix}email_outbox` SET status='retry',available_at=NOW(),last_error=?,updated_at=NOW()\n"
            . "WHERE status='processing' AND COALESCE(last_attempt_at,created_at)<DATE_SUB(NOW(),INTERVAL {$minutes} MINUTE) AND attempts<" . self::MAX_ATTEMPTS
        );
        $stmt->execute([$message]);
        return $stmt->rowCount() + $failed->rowCount();
    }

    /**
     * Process one queued message and return its authoritative state.
     * Row locking prevents two requests from claiming the same message.
     *
     * @return array{status:string,id:int,error:?string,attempts:int}
     */
    public function processMessage(int $id): array
    {
        if (!$this->pdo) {
            throw new RuntimeException('A database connection is required to process email.');
        }
        if ($id < 1) {
            throw new RuntimeException('A valid queued email is required.');
        }

        $prefix = (string) (($this->config['database']['prefix'] ?? 'dent_'));
        $this->pdo->beginTransaction();
        try {
            $stmt = $this->pdo->prepare("SELECT * FROM `{$prefix}email_outbox` WHERE id=:id FOR UPDATE");
            $stmt->execute([':id' => $id]);
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            if (!$row) {
                throw new RuntimeException('Queued email not found.');
            }

            $status = (string) $row['status'];
            $attempts = (int) $row['attempts'];
            $availableAt = strtotime((string) ($row['available_at'] ?? '')) ?: 0;
            if (!in_array($status, ['pending','retry'], true) || $attempts >= self::MAX_ATTEMPTS || ($status === 'retry' && $availableAt > time())) {
                $this->pdo->commit();
                return ['status' => 'skipped', 'id' => $id, 'error' => $row['last_error'] ?? null, 'attempts' => $attempts];
            }

            $claim = $this->pdo->prepare(
                "UPDATE `{$prefix}email_outbox` SET status='processing',attempts=attempts+1,last_attempt_at=NOW(),updated_at=NOW()\n"
                . "WHERE id=:id AND status IN ('pending','retry')"
            );
            $claim->execute([':id' => $id]);
            if ($claim->rowCount() !== 1) {
                $this->pdo->commit();
                return ['status' => 'skipped', 'id' => $id, 'error' => null, 'attempts' => $attempts];
            }
            $this->pdo->commit();
        } catch (Throwable $e) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            throw $e;
        }

        $nextAttempts = ((int) $row['attempts']) + 1;
        try {
            $metadata = json_decode((string) ($row['metadata_json'] ?? '{}'), true);
            if (!is_array($metadata)) $metadata = [];
            $metadata['outbox_id'] = $id;
            $driver = (string) (($this->config['mail']['driver'] ?? 'smtp'));
            if ($driver === 'log') {
                $this->send(
                    (string) $row['recipient_email'],
                    (string) $row['subject'],
                    (string) $row['html_body'],
                    $row['text_body'] !== null ? (string) $row['text_body'] : null,
                    $metadata
                );
                $error = 'Log-only mode recorded the message but did not deliver it. Configure SMTP or PHP mail for delivery.';
                $this->pdo->prepare("UPDATE `{$prefix}email_outbox` SET status='failed',last_error=:error,updated_at=NOW() WHERE id=:id AND status='processing'")
                    ->execute([':id' => $id, ':error' => $error]);
                $this->syncReminderState($id, 'failed', $error);
                $this->syncLinkedEntityState($metadata, 'failed', $error);
                return ['status' => 'failed', 'id' => $id, 'error' => $error, 'attempts' => $nextAttempts];
            }
            $this->send(
                (string) $row['recipient_email'],
                (string) $row['subject'],
                (string) $row['html_body'],
                $row['text_body'] !== null ? (string) $row['text_body'] : null,
                $metadata
            );
            $this->pdo->prepare("UPDATE `{$prefix}email_outbox` SET status='sent',sent_at=NOW(),last_error=NULL,updated_at=NOW() WHERE id=:id AND status='processing'")
                ->execute([':id' => $id]);
            $this->syncReminderState($id, 'sent', null);
            $this->syncLinkedEntityState($metadata, 'sent', null);
            return ['status' => 'sent', 'id' => $id, 'error' => null, 'attempts' => $nextAttempts];
        } catch (Throwable $e) {
            $error = function_exists('mb_substr') ? mb_substr($e->getMessage(), 0, 1000) : substr($e->getMessage(), 0, 1000);
            if ($nextAttempts >= self::MAX_ATTEMPTS) {
                $status = 'failed';
                $this->pdo->prepare("UPDATE `{$prefix}email_outbox` SET status='failed',last_error=:error,updated_at=NOW() WHERE id=:id AND status='processing'")
                    ->execute([':id' => $id, ':error' => $error]);
            } else {
                $status = 'retry';
                $delay = min(1440, 5 * (2 ** max(0, $nextAttempts - 1)));
                $this->pdo->prepare("UPDATE `{$prefix}email_outbox` SET status='retry',available_at=DATE_ADD(NOW(),INTERVAL {$delay} MINUTE),last_error=:error,updated_at=NOW() WHERE id=:id AND status='processing'")
                    ->execute([':id' => $id, ':error' => $error]);
            }
            $this->syncReminderState($id, $status === 'failed' ? 'failed' : 'queued', $error);
            $this->syncLinkedEntityState($metadata, $status, $error);
            return ['status' => $status, 'id' => $id, 'error' => $error, 'attempts' => $nextAttempts];
        }
    }

    private function syncReminderState(int $outboxId, string $status, ?string $error): void
    {
        if (!$this->pdo) return;
        $prefix = (string) (($this->config['database']['prefix'] ?? 'dent_'));
        try {
            $stmt = $this->pdo->prepare(
                "UPDATE `{$prefix}appointment_reminders` SET status=?,last_error=?,updated_at=NOW() WHERE outbox_id=?"
            );
            $stmt->execute([$status, $error, $outboxId]);
        } catch (Throwable) {
            // Older installations may not have the reminder table yet.
        }
    }

    private function syncLinkedEntityState(array $metadata, string $status, ?string $error): void
    {
        if (!$this->pdo) return;
        $prefix = (string) (($this->config['database']['prefix'] ?? 'dent_'));
        try {
            if (!empty($metadata['communication_id'])) {
                $stmt = $this->pdo->prepare("UPDATE `{$prefix}communications` SET delivery_status=? WHERE id=?");
                $stmt->execute([$status, (int) $metadata['communication_id']]);
            }
        } catch (Throwable) {
            // Older installations may not yet have entity delivery columns.
        }
    }

    private function sendUsingPhpMail(string $fromEmail, string $fromName, string $to, string $subject, string $html): void
    {
        if (!filter_var($to, FILTER_VALIDATE_EMAIL) || !filter_var($fromEmail, FILTER_VALIDATE_EMAIL)) {
            throw new RuntimeException('Invalid sender or recipient address.');
        }
        $encodedName = '=?UTF-8?B?' . base64_encode($fromName) . '?=';
        $encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
        $headers = [
            'MIME-Version: 1.0',
            'Content-Type: text/html; charset=UTF-8',
            'From: ' . $encodedName . ' <' . $fromEmail . '>',
        ];
        if (!mail($to, $encodedSubject, $html, implode("\r\n", $headers))) {
            throw new RuntimeException('PHP mail() did not accept the message.');
        }
    }

    private function writeLog(string $to, string $subject, string $html): void
    {
        $dir = dirname(__DIR__, 2) . '/storage/logs';
        if (!is_dir($dir) && !mkdir($dir, 0770, true) && !is_dir($dir)) {
            throw new RuntimeException('Unable to create mail log directory.');
        }
        file_put_contents($dir . '/mail-' . date('Y-m-d') . '.log', "\n---\nTO: {$to}\nSUBJECT: {$subject}\n{$html}\n", FILE_APPEND | LOCK_EX);
    }

    private function recordLog(string $to, string $subject, string $status, ?string $error, array $meta): void
    {
        if (!$this->pdo) {
            return;
        }
        $prefix = (string) (($this->config['database']['prefix'] ?? 'dent_'));
        try {
            $stmt = $this->pdo->prepare(
                "INSERT INTO `{$prefix}email_log` (recipient_email, subject, status, error_message, metadata_json, created_at)\n"
                . "VALUES (:recipient, :subject, :status, :error, :metadata, NOW())"
            );
            $stmt->execute([
                ':recipient' => $to,
                ':subject' => $subject,
                ':status' => $status,
                ':error' => $error,
                ':metadata' => json_encode($meta, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            ]);
        } catch (Throwable) {
            // Email delivery must not be masked by a secondary logging failure.
        }
    }
}
