<?php

namespace BitApps\SocialPro\HTTP\Services\Notification;

use BitApps\Social\Config as FreeConfig;
use BitApps\Social\Model\Log;
use BitApps\Social\Model\Schedule;
use BitApps\SocialPro\Config;

/**
 * Sends email notifications to Pro users when social posts fail,
 * and daily/weekly digest reports of successes and failures.
 */
class EmailNotificationService
{
    /** Maximum rows shown per list in the digest; the rest link out to the logs. */
    private const MAX_LIST_ROWS = 10;

    /**
     * Reads the notification settings from the pro settings option.
     *
     * @return array
     */
    public function getSettings()
    {
        $proSettings = Config::getOption('settings', []);
        $notification = isset($proSettings['notification']) && \is_array($proSettings['notification'])
            ? $proSettings['notification']
            : [];

        return wp_parse_args($notification, [
            'isEnabled'      => false,
            'recipients'     => '',
            'onFailure'      => false,
            'digestEnabled'  => false,
            'digestInterval' => 1,
            'digestUnit'     => 'week',
        ]);
    }

    /**
     * Parses the comma separated recipients into a list of valid emails.
     *
     * @return array
     */
    public function recipients()
    {
        $settings = $this->getSettings();
        $emails = array_filter(array_map('trim', explode(',', (string) $settings['recipients'])));

        $valid = [];
        foreach ($emails as $email) {
            if (is_email($email)) {
                $valid[] = $email;
            }
        }

        return $valid;
    }

    /**
     * Sends an immediate email when a single post fails.
     *
     * @param int|null     $scheduleId
     * @param string       $platform
     * @param array|object $details
     */
    public function sendFailureEmail($scheduleId, $platform, $details)
    {
        $recipients = $this->recipients();
        if (empty($recipients)) {
            return;
        }

        $details = (object) $details;
        $schedule = !empty($scheduleId) ? Schedule::findOne(['id' => $scheduleId]) : null;
        $postId = isset($details->post_id) ? $details->post_id : null;
        $postTitle = $this->postTitle($details, $schedule);
        $postUrl = $postId ? get_permalink($postId) : '';
        $accountName = isset($details->account_name) ? $details->account_name : '';
        $platformLabel = ucfirst((string) $platform);

        // translators: 1: platform name, 2: post title
        $subject = sprintf(__('[Bit Social] Post failed on %1$s — "%2$s"', 'bit-social'), $platformLabel, $postTitle);

        $rows = [
            __('Post', 'bit-social')     => $postUrl ? $this->link($postUrl, $postTitle) : esc_html($postTitle),
            __('Platform', 'bit-social') => esc_html($platformLabel),
            __('Account', 'bit-social')  => esc_html($accountName),
            __('Schedule', 'bit-social') => esc_html($this->scheduleName($scheduleId)),
        ];

        $body = '<h1 style="margin:0 0 14px;font-size:20px;font-weight:700;color:#18181b;">'
            . esc_html__('A social post failed to publish', 'bit-social') . '</h1>';
        $body .= $this->alertBanner('⚠️ ' . __('Your scheduled post could not be published. Details are below.', 'bit-social'));
        $body .= $this->detailsTable($rows);
        $body .= $this->sectionHeading(__('Error', 'bit-social'));
        $body .= '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#fef2f2;border:1px solid #fee2e2;border-radius:10px;">'
            . '<tr><td style="padding:12px 14px;color:#b91c1c;font-size:14px;line-height:1.5;">' . $this->errorCell($details) . '</td></tr></table>';

        if ($this->send($recipients, $subject, $this->wrap($subject, $body))) {
            // Record when the last failure alert went out (UTC).
            Config::updateOption('failure_last_sent', time());
        }
    }

    /**
     * Sends the digest report covering the configured interval window.
     *
     * @return array{sent: bool, message: string} outcome of the attempt
     */
    public function sendDigest()
    {
        $settings = $this->getSettings();
        if (empty($settings['isEnabled']) || empty($settings['digestEnabled'])) {
            return ['sent' => false, 'message' => __('Digest report is turned off.', 'bit-social')];
        }

        $recipients = $this->recipients();
        if (empty($recipients)) {
            return ['sent' => false, 'message' => __('No valid recipient email address is configured.', 'bit-social')];
        }

        $intervalSeconds = NotificationHandler::digestIntervalSeconds($settings);
        $now = current_time('timestamp');
        $lastSent = (int) Config::getOption('digest_last_sent', 0);

        // Count from the last report onwards so posts are never reported twice.
        // On the first run (no last-sent yet) fall back to the configured window.
        $startTimestamp = $lastSent > 0 ? $lastSent : $now - $intervalSeconds;

        $end = gmdate('Y-m-d', $now);
        $start = gmdate('Y-m-d', $startTimestamp);

        $failLogs = Log::with('schedule')
            ->where('status', Log::status['FAIL'])
            ->whereBetween('DATE(`created_at`)', $start, $end)
            ->desc()
            ->get();

        $successLogs = Log::with('schedule')
            ->where('status', Log::status['SUCCESS'])
            ->whereBetween('DATE(`created_at`)', $start, $end)
            ->desc()
            ->get();

        $failLogs = \is_array($failLogs) ? $failLogs : [];
        $successLogs = \is_array($successLogs) ? $successLogs : [];

        $failCount = \count($failLogs);
        $successCount = \count($successLogs);
        $total = $successCount + $failCount;
        $successRate = $total > 0 ? (int) round(($successCount / $total) * 100) : 0;

        $periodLabel = $this->digestPeriodLabel($settings);
        // Actual window dates, e.g. "Jun 2, 2026 – Jun 9, 2026".
        $rangeLabel = $this->formatDate($startTimestamp) . ' – ' . $this->formatDate($now);

        $subject = sprintf(
            // translators: 1: published count, 2: failed count
            __('[Bit Social] Report — %1$d published, %2$d failed', 'bit-social'),
            $successCount,
            $failCount
        );

        $body = '<h1 style="margin:0 0 6px;font-size:20px;font-weight:700;color:#18181b;">'
            . esc_html__('Your social posting report', 'bit-social') . '</h1>';
        $body .= '<p style="margin:0 0 2px;color:#6b7280;font-size:13px;">'
            // translators: 1: a date range, 2: period label e.g. "last 30 days"
            . sprintf(esc_html__('%1$s · %2$s', 'bit-social'), esc_html($rangeLabel), esc_html($periodLabel)) . '</p>';
        $body .= '<p style="margin:0 0 18px;color:#a1a1aa;font-size:12px;">'
            . sprintf(
                // translators: %s: a formatted date/time
                esc_html__('Last report: %s', 'bit-social'),
                $lastSent ? esc_html($this->formatDateTime($lastSent)) : esc_html__('never (first report)', 'bit-social')
            ) . '</p>';

        $body .= $this->statCards($successCount, $failCount, $successRate);
        $body .= $this->operationSummary($successLogs, $failLogs);
        $body .= $this->platformSummary($successLogs, $failLogs);
        $body .= $this->failuresTable($failLogs);
        $body .= $this->successList($successLogs);

        if ($this->send($recipients, $subject, $this->wrap($subject, $body))) {
            // Record when this report went out (UTC) for the next digest.
            Config::updateOption('digest_last_sent', time());

            return [
                'sent'    => true,
                'message' => sprintf(
                    // translators: 1: published count, 2: failed count
                    __('Digest report sent — %1$d published, %2$d failed.', 'bit-social'),
                    $successCount,
                    $failCount
                ),
            ];
        }

        return ['sent' => false, 'message' => __('The email could not be sent. Please check your SMTP setup.', 'bit-social')];
    }

    /**
     * Formats a UTC timestamp using the site's date/time format and timezone.
     *
     * @param int $timestamp UTC timestamp
     *
     * @return string
     */
    private function formatDateTime($timestamp)
    {
        $format = trim(get_option('date_format') . ' ' . get_option('time_format'));
        if ($format === '') {
            $format = 'Y-m-d H:i';
        }

        // wp_date() converts the UTC timestamp to the configured WordPress timezone.
        return wp_date($format, $timestamp);
    }

    /**
     * Formats a site-local timestamp as a date using the site's date format.
     *
     * @param int $timestamp site-local timestamp (e.g. from current_time())
     *
     * @return string
     */
    private function formatDate($timestamp)
    {
        $format = get_option('date_format');
        if (!$format) {
            $format = 'Y-m-d';
        }

        // date_i18n() treats the timestamp as already in the site's timezone.
        return date_i18n($format, $timestamp);
    }

    /**
     * Formats a log row's created_at (MySQL datetime) for display.
     *
     * @param object $log
     *
     * @return string
     */
    private function formatLogTime($log)
    {
        if (empty($log->created_at)) {
            return '';
        }

        $format = trim(get_option('date_format') . ' ' . get_option('time_format'));
        if ($format === '') {
            $format = 'Y-m-d H:i';
        }

        return mysql2date($format, $log->created_at);
    }

    /**
     * Builds a per-platform summary table (published/failed counts per platform).
     *
     * @param array $successLogs
     * @param array $failLogs
     *
     * @return string
     */
    private function platformSummary($successLogs, $failLogs)
    {
        $summary = [];
        foreach ($successLogs as $log) {
            $platform = ucfirst((string) $log->platform);
            $summary[$platform]['success'] = (isset($summary[$platform]['success']) ? $summary[$platform]['success'] : 0) + 1;
        }
        foreach ($failLogs as $log) {
            $platform = ucfirst((string) $log->platform);
            $summary[$platform]['fail'] = (isset($summary[$platform]['fail']) ? $summary[$platform]['fail'] : 0) + 1;
        }

        if (empty($summary)) {
            return '';
        }

        ksort($summary);

        $html = $this->sectionHeading(__('By platform', 'bit-social'));
        $html .= $this->tableOpen();
        $html .= '<tr>'
            . $this->th(__('Platform', 'bit-social'))
            . $this->th(__('Published', 'bit-social'), 'center')
            . $this->th(__('Failed', 'bit-social'), 'center')
            . '</tr>';

        foreach ($summary as $platform => $counts) {
            $success = isset($counts['success']) ? (int) $counts['success'] : 0;
            $fail = isset($counts['fail']) ? (int) $counts['fail'] : 0;

            $html .= '<tr>'
                . $this->td('<strong style="color:#18181b;">' . esc_html($platform) . '</strong>')
                . $this->td($this->countBadge($success, 'success'), 'center')
                . $this->td($this->countBadge($fail, $fail > 0 ? 'danger' : 'muted'), 'center')
                . '</tr>';
        }

        $html .= '</table>';

        return $html;
    }

    /**
     * Builds a human-readable period label like "last 30 days" / "last week".
     *
     * @param array $settings
     *
     * @return string
     */
    private function digestPeriodLabel($settings)
    {
        $count = isset($settings['digestInterval']) ? max(1, (int) $settings['digestInterval']) : 1;
        $unit = isset($settings['digestUnit']) ? $settings['digestUnit'] : 'week';

        if ($unit === 'day') {
            // translators: %d: number of days
            $period = sprintf(_n('%d day', '%d days', $count, 'bit-social'), $count);
        } elseif ($unit === 'month') {
            // translators: %d: number of months
            $period = sprintf(_n('%d month', '%d months', $count, 'bit-social'), $count);
        } else {
            // translators: %d: number of weeks
            $period = sprintf(_n('%d week', '%d weeks', $count, 'bit-social'), $count);
        }

        // translators: %s: a period like "7 days"
        return sprintf(__('last %s', 'bit-social'), $period);
    }

    /**
     * Per-platform error bags that may sit alongside `response` on a failed log.
     * Each holds one or more strings / error objects describing what went wrong
     * (auth expiry, per-image upload failures, invalid links, comment failures…).
     *
     * @var array
     */
    private const ERROR_BAGS = [
        'authError',
        'linkCardError',
        'imageUploadErrors',
        'mediaUploadErrors',
        'mediaErrors',
        'linkErrors',
        'commentErrors',
        'commentError',
    ];

    /**
     * Extracts a single human readable error message from a log details payload.
     *
     * @param array|object $details
     *
     * @return string
     */
    public function extractError($details)
    {
        $errors = $this->extractErrorList($details);

        return $errors[0];
    }

    /**
     * Collects every error message from a log details payload, so a post that
     * failed for multiple reasons surfaces each error individually.
     *
     * Platforms don't share a single error shape: LinkedIn/TikTok use a
     * `['status' => 0, 'message' => …]` array, Facebook/Instagram/Threads nest it
     * under `response->error->message`, Telegram uses `response->description`,
     * Bluesky/Discord/Line use `response->message`, and the "account disabled"
     * path stores a plain string. We therefore dig the message out of whatever
     * shape `response` happens to be, then append any per-platform error bags.
     *
     * @param array|object $details
     *
     * @return array list of error message strings (never empty)
     */
    public function extractErrorList($details)
    {
        $details = (object) $details;
        $errors = [];

        if (isset($details->response) && $details->response !== '') {
            $message = $this->messageFromValue($details->response);
            if ($message !== '') {
                $errors[] = $message;
            }
        }

        // Per-image upload failures keep their descriptive prefix.
        if (!empty($details->imagePostError)) {
            $this->collectBag($errors, $details->imagePostError, __('Image upload failed: ', 'bit-social'));
        }

        foreach (self::ERROR_BAGS as $bag) {
            if (!empty($details->{$bag})) {
                $this->collectBag($errors, $details->{$bag});
            }
        }

        // Drop blanks/duplicates while preserving order.
        $errors = array_values(array_unique(array_filter(array_map('trim', $errors), 'strlen')));

        if (empty($errors)) {
            $errors[] = __('Post publication failed', 'bit-social');
        }

        return $errors;
    }

    /**
     * Digs a human readable message out of an arbitrary error value — a string,
     * or an array/object using any of the common error field shapes returned by
     * the social APIs. Recurses into a nested `error`/`errors` payload.
     *
     * @param mixed $value
     *
     * @return string '' when nothing readable is found
     */
    private function messageFromValue($value)
    {
        if (\is_string($value)) {
            return trim($value);
        }

        if (\is_scalar($value)) {
            return trim((string) $value);
        }

        if (!\is_array($value) && !\is_object($value)) {
            return '';
        }

        $bag = (array) $value;

        // Nested error object/array (Facebook/Instagram/Threads: response->error->message).
        if (isset($bag['error']) && (\is_object($bag['error']) || \is_array($bag['error']))) {
            $nested = $this->messageFromValue($bag['error']);
            if ($nested !== '') {
                return $nested;
            }
        }

        // Twitter v2 style: { errors: [ { message } ] }.
        if (!empty($bag['errors'])) {
            foreach ((array) $bag['errors'] as $entry) {
                $nested = $this->messageFromValue($entry);
                if ($nested !== '') {
                    return $nested;
                }
            }
        }

        // First populated human-readable field, in order of usefulness. Prefer a
        // real message over a bare error code (e.g. Bluesky sends both).
        foreach (['message', 'error_user_msg', 'error_msg', 'error_description', 'description', 'detail', 'title'] as $key) {
            if (isset($bag[$key]) && \is_string($bag[$key]) && trim($bag[$key]) !== '') {
                return trim($bag[$key]);
            }
        }

        // Last resort: a scalar error code/string under `error`.
        if (isset($bag['error']) && \is_string($bag['error']) && trim($bag['error']) !== '') {
            return trim($bag['error']);
        }

        return '';
    }

    /**
     * Pulls a readable message out of each entry in a platform error bag and
     * appends it (optionally prefixed) to the running error list.
     *
     * @param array        $errors reference to the accumulating list
     * @param array|object $bag    a platform error bag (strings or error objects)
     * @param string       $prefix optional label prepended to each message
     */
    private function collectBag(&$errors, $bag, $prefix = '')
    {
        foreach ((array) $bag as $entry) {
            $message = $this->messageFromValue($entry);
            if ($message !== '') {
                $errors[] = $prefix . $message;
            }
        }
    }

    /**
     * Renders the error list as email-safe HTML: a single line for one error,
     * or a tight bulleted list when a post failed for several reasons.
     *
     * @param array|object $details
     *
     * @return string
     */
    private function errorCell($details)
    {
        $errors = $this->extractErrorList($details);

        if (\count($errors) === 1) {
            return '<span style="color:#b91c1c;">' . esc_html($errors[0]) . '</span>';
        }

        $html = '<ul style="margin:0;padding-left:16px;color:#b91c1c;">';
        foreach ($errors as $error) {
            $html .= '<li style="margin:0 0 2px;">' . esc_html($error) . '</li>';
        }
        $html .= '</ul>';

        return $html;
    }

    /**
     * Resolves a display title for a log row's post. Scheduled/Auto posts use the
     * WordPress post title; Share Now entries have no WP post, so fall back to the
     * Share Now title (config → settings → name) stored on the schedule.
     *
     * @param object       $details  the log details payload
     * @param object|null  $schedule the originating schedule (already loaded)
     *
     * @return string
     */
    private function postTitle($details, $schedule)
    {
        $details = (object) $details;
        $postId = isset($details->post_id) ? $details->post_id : null;
        if ($postId) {
            return get_the_title($postId);
        }

        $name = $this->shareNowName($schedule);

        return $name !== '' ? $name : __('(unknown post)', 'bit-social');
    }

    /**
     * Reads a Share Now title from a schedule: config → settings → name,
     * falling back to the schedule's name column.
     *
     * @param object|null $schedule
     *
     * @return string
     */
    private function shareNowName($schedule)
    {
        if (!\is_object($schedule)) {
            return '';
        }

        if (!empty($schedule->config['settings']['name'])) {
            return (string) $schedule->config['settings']['name'];
        }

        return !empty($schedule->name) ? (string) $schedule->name : '';
    }

    /**
     * Resolves a readable schedule name from a schedule id.
     *
     * @param int|null $scheduleId
     *
     * @return string
     */
    private function scheduleName($scheduleId)
    {
        if (empty($scheduleId)) {
            return __('Direct share', 'bit-social');
        }

        $schedule = Schedule::findOne(['id' => $scheduleId]);
        if ($schedule && !empty($schedule->name)) {
            return $schedule->name;
        }

        return '#' . $scheduleId;
    }

    /**
     * Classifies a log into the operation that produced it:
     * "Share Now", "Auto Post" or "WP Schedule".
     *
     * @param object $log a log with its `schedule` relation eager-loaded
     *
     * @return string
     */
    private function operationType($log)
    {
        // Immediate Share Now logs carry no schedule.
        if (empty($log->schedule_id)) {
            return __('Share Now', 'bit-social');
        }

        $schedule = isset($log->schedule) ? $log->schedule : null;
        $type = is_object($schedule) && isset($schedule->schedule_type) ? (int) $schedule->schedule_type : null;
        $name = is_object($schedule) && isset($schedule->name) ? (string) $schedule->name : '';

        if ($type === Schedule::scheduleType['DIRECT_SHARE']) {
            return __('Share Now', 'bit-social');
        }

        // Auto Post creates schedules named "Auto Post - Post ID: {id}".
        if (strpos($name, 'Auto Post - Post ID:') === 0) {
            return __('Auto Post', 'bit-social');
        }

        return __('WP Schedule', 'bit-social');
    }

    /**
     * A colored pill for an operation type.
     *
     * @param string $label
     *
     * @return string
     */
    private function operationPill($label)
    {
        $map = [
            __('Share Now', 'bit-social')   => ['#9a3412', '#fff7ed'],
            __('Auto Post', 'bit-social')   => ['#6b21a8', '#faf5ff'],
            __('WP Schedule', 'bit-social') => ['#115e59', '#f0fdfa'],
        ];
        $colors = isset($map[$label]) ? $map[$label] : ['#6b7280', '#f3f4f6'];

        return '<span style="display:inline-block;padding:2px 9px;border-radius:6px;font-size:12px;font-weight:600;color:' . $colors[0] . ';background:' . $colors[1] . ';white-space:nowrap;">'
            . esc_html($label) . '</span>';
    }

    /**
     * Builds a per-operation summary table (published/failed counts per type).
     *
     * @param array $successLogs
     * @param array $failLogs
     *
     * @return string
     */
    private function operationSummary($successLogs, $failLogs)
    {
        // Keep a stable, meaningful order rather than alphabetical.
        $order = [__('Share Now', 'bit-social'), __('Auto Post', 'bit-social'), __('WP Schedule', 'bit-social')];
        $summary = [];
        foreach ($successLogs as $log) {
            $op = $this->operationType($log);
            $summary[$op]['success'] = (isset($summary[$op]['success']) ? $summary[$op]['success'] : 0) + 1;
        }
        foreach ($failLogs as $log) {
            $op = $this->operationType($log);
            $summary[$op]['fail'] = (isset($summary[$op]['fail']) ? $summary[$op]['fail'] : 0) + 1;
        }

        if (empty($summary)) {
            return '';
        }

        $html = $this->sectionHeading(__('By operation', 'bit-social'));
        $html .= $this->tableOpen();
        $html .= '<tr>'
            . $this->th(__('Operation', 'bit-social'))
            . $this->th(__('Published', 'bit-social'), 'center')
            . $this->th(__('Failed', 'bit-social'), 'center')
            . '</tr>';

        foreach ($order as $op) {
            if (!isset($summary[$op])) {
                continue;
            }
            $success = isset($summary[$op]['success']) ? (int) $summary[$op]['success'] : 0;
            $fail = isset($summary[$op]['fail']) ? (int) $summary[$op]['fail'] : 0;

            $html .= '<tr>'
                . $this->td($this->operationPill($op))
                . $this->td($this->countBadge($success, 'success'), 'center')
                . $this->td($this->countBadge($fail, $fail > 0 ? 'danger' : 'muted'), 'center')
                . '</tr>';
        }

        $html .= '</table>';

        return $html;
    }

    /**
     * Builds the failures table for the digest email.
     *
     * @param array $failLogs
     *
     * @return string
     */
    private function failuresTable($failLogs)
    {
        if (empty($failLogs)) {
            return '';
        }

        $remaining = \count($failLogs) - self::MAX_LIST_ROWS;
        $rows = \array_slice($failLogs, 0, self::MAX_LIST_ROWS);

        $html = $this->sectionHeading(__('Failed posts', 'bit-social'), '#b91c1c');
        $html .= $this->tableOpen();
        $html .= '<tr>'
            . $this->th(__('Post', 'bit-social'))
            . $this->th(__('Type', 'bit-social'))
            . $this->th(__('Platform', 'bit-social'))
            . $this->th(__('Account', 'bit-social'))
            . $this->th(__('When', 'bit-social'))
            . $this->th(__('Error', 'bit-social'))
            . '</tr>';

        foreach ($rows as $log) {
            $details = (object) (isset($log->details) ? $log->details : []);
            $postId = isset($details->post_id) ? $details->post_id : null;
            $postTitle = $this->postTitle($details, isset($log->schedule) ? $log->schedule : null);
            $postUrl = $postId ? get_permalink($postId) : '';
            $accountName = isset($details->account_name) ? $details->account_name : '';

            $html .= '<tr>'
                . $this->td($postUrl ? $this->link($postUrl, $postTitle) : '<span style="color:#18181b;">' . esc_html($postTitle) . '</span>')
                . $this->td($this->operationPill($this->operationType($log)))
                . $this->td($this->platformPill(ucfirst((string) $log->platform)))
                . $this->td('<span style="color:#6b7280;">' . esc_html($accountName) . '</span>')
                . $this->td('<span style="color:#9ca3af;white-space:nowrap;">' . esc_html($this->formatLogTime($log)) . '</span>')
                . $this->td($this->errorCell($details))
                . '</tr>';
        }

        $html .= '</table>';
        $html .= $this->seeMoreLine($remaining);

        return $html;
    }

    /**
     * Builds the success list for the digest email.
     *
     * @param array $successLogs
     *
     * @return string
     */
    private function successList($successLogs)
    {
        if (empty($successLogs)) {
            return '';
        }

        $remaining = \count($successLogs) - self::MAX_LIST_ROWS;
        $rows = \array_slice($successLogs, 0, self::MAX_LIST_ROWS);

        $html = $this->sectionHeading(__('Published posts', 'bit-social'), '#16a34a');
        $html .= '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #eceef2;border-radius:10px;border-collapse:separate;overflow:hidden;">';

        foreach ($rows as $log) {
            $details = (object) (isset($log->details) ? $log->details : []);
            $postTitle = $this->postTitle($details, isset($log->schedule) ? $log->schedule : null);
            $accountName = isset($details->account_name) ? $details->account_name : '';

            $when = $this->formatLogTime($log);
            $meta = $this->operationPill($this->operationType($log))
                . ' ' . $this->platformPill(ucfirst((string) $log->platform))
                . ($accountName ? ' <span style="color:#9ca3af;font-size:13px;">' . esc_html($accountName) . '</span>' : '');

            $html .= '<tr><td style="padding:11px 14px;border-bottom:1px solid #f1f2f4;font-size:14px;line-height:1.5;">'
                . '<span style="color:#16a34a;font-size:15px;">●</span> '
                . '<span style="color:#18181b;font-weight:500;">' . esc_html($postTitle) . '</span>'
                . '<div style="margin:5px 0 0;">' . $meta
                . ($when ? ' <span style="color:#a1a1aa;font-size:12px;">· ' . esc_html($when) . '</span>' : '') . '</div>'
                . '</td></tr>';
        }

        $html .= '</table>';
        $html .= $this->seeMoreLine($remaining);

        return $html;
    }

    /**
     * Renders a simple key/value details table.
     *
     * @param array $rows
     *
     * @return string
     */
    private function detailsTable($rows)
    {
        $html = '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #eceef2;border-radius:10px;border-collapse:separate;overflow:hidden;font-size:14px;">';
        foreach ($rows as $label => $value) {
            $html .= '<tr>'
                . '<td style="padding:10px 14px;color:#6b7280;width:110px;background:#fafafa;border-bottom:1px solid #f1f2f4;vertical-align:top;">' . esc_html($label) . '</td>'
                . '<td style="padding:10px 14px;color:#18181b;border-bottom:1px solid #f1f2f4;">' . $value . '</td>'
                . '</tr>';
        }
        $html .= '</table>';

        return $html;
    }

    /**
     * Wraps the body in a modern, branded, email-safe HTML shell.
     *
     * @param string $title
     * @param string $bodyHtml
     *
     * @return string
     */
    private function wrap($title, $bodyHtml)
    {
        $logsUrl = $this->logsUrl();
        $font = '-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif';

        $html = '<!DOCTYPE html><html lang="en"><head>'
            . '<meta charset="utf-8">'
            . '<meta name="viewport" content="width=device-width,initial-scale=1">'
            . '<meta name="x-apple-disable-message-reformatting">'
            . '<title>' . esc_html($title) . '</title></head>';
        $html .= '<body style="margin:0;padding:0;background:#f4f5f7;-webkit-font-smoothing:antialiased;">';
        // Hidden preheader keeps the inbox preview clean.
        $html .= '<div style="display:none;max-height:0;overflow:hidden;opacity:0;">' . esc_html($title) . '</div>';
        $html .= '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f4f5f7;padding:24px 12px;font-family:' . $font . ';">'
            . '<tr><td align="center">';
        $html .= '<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 1px 3px rgba(16,24,40,0.08),0 4px 16px rgba(16,24,40,0.05);">';
        // Header
        $html .= '<tr><td style="background:#4059ff;background:linear-gradient(135deg,#4059ff 0%,#6f7cff 100%);padding:22px 28px;">'
            . '<span style="font-size:19px;font-weight:700;color:#ffffff;letter-spacing:.2px;">Bit Social</span></td></tr>';
        // Body
        $html .= '<tr><td style="padding:28px;color:#18181b;font-size:14px;line-height:1.55;">';
        $html .= $bodyHtml;
        // CTA button
        $html .= '<table role="presentation" cellpadding="0" cellspacing="0" style="margin:28px 0 0;"><tr>'
            . '<td style="border-radius:8px;background:#4059ff;">'
            . '<a href="' . esc_url($logsUrl) . '" style="display:inline-block;padding:11px 22px;font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;border-radius:8px;">'
            . esc_html__('Review logs in dashboard', 'bit-social') . ' →</a>'
            . '</td></tr></table>';
        $html .= '</td></tr>';
        // Footer
        $html .= '<tr><td style="padding:18px 28px 24px;border-top:1px solid #eceef2;">'
            . '<p style="font-size:12px;color:#a1a1aa;margin:0;line-height:1.5;">'
            . esc_html__('This is an automated message from Bit Social.', 'bit-social') . '</p></td></tr>';
        $html .= '</table></td></tr></table></body></html>';

        return $html;
    }

    /**
     * A row of summary stat cards (Published / Failed / Success rate).
     *
     * @param int $successCount
     * @param int $failCount
     * @param int $successRate
     *
     * @return string
     */
    private function statCards($successCount, $failCount, $successRate)
    {
        $card = function ($value, $label, $color, $bg) {
            return '<td width="33%" valign="top" style="padding:5px;">'
                . '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:' . $bg . ';border-radius:12px;">'
                . '<tr><td style="padding:16px 10px;text-align:center;">'
                . '<div style="font-size:26px;font-weight:700;color:' . $color . ';line-height:1;">' . esc_html($value) . '</div>'
                . '<div style="font-size:11px;color:#6b7280;margin-top:6px;text-transform:uppercase;letter-spacing:.5px;">' . esc_html($label) . '</div>'
                . '</td></tr></table></td>';
        };

        $html = '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:0 -5px;"><tr>';
        $html .= $card($successCount, __('Published', 'bit-social'), '#16a34a', '#ecfdf5');
        $html .= $card($failCount, __('Failed', 'bit-social'), '#dc2626', '#fef2f2');
        $html .= $card($successRate . '%', __('Success rate', 'bit-social'), '#4059ff', '#eef1ff');
        $html .= '</tr></table>';

        return $html;
    }

    /**
     * A muted, uppercase section heading.
     *
     * @param string $text
     * @param string $color accent color for a small leading bar
     *
     * @return string
     */
    private function sectionHeading($text, $color = '#4059ff')
    {
        return '<h2 style="margin:28px 0 10px;font-size:12px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.6px;">'
            . '<span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:' . $color . ';margin-right:7px;"></span>'
            . esc_html($text) . '</h2>';
    }

    /**
     * Red alert banner used by the failure email.
     *
     * @param string $text
     *
     * @return string
     */
    private function alertBanner($text)
    {
        return '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#fef2f2;border:1px solid #fee2e2;border-radius:10px;margin:0 0 18px;">'
            . '<tr><td style="padding:14px 16px;color:#b91c1c;font-size:14px;font-weight:600;line-height:1.5;">' . esc_html($text) . '</td></tr></table>';
    }

    /**
     * Opens a rounded, modern data table.
     *
     * @return string
     */
    private function tableOpen()
    {
        return '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #eceef2;border-radius:10px;border-collapse:separate;overflow:hidden;font-size:14px;">';
    }

    /**
     * A table header cell.
     *
     * @param string $text
     * @param string $align
     *
     * @return string
     */
    private function th($text, $align = 'left')
    {
        return '<th align="' . esc_attr($align) . '" style="padding:10px 14px;font-size:11px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;background:#fafafa;border-bottom:1px solid #eceef2;">'
            . esc_html($text) . '</th>';
    }

    /**
     * A table body cell. Value is treated as trusted HTML (callers escape).
     *
     * @param string $html
     * @param string $align
     *
     * @return string
     */
    private function td($html, $align = 'left')
    {
        return '<td align="' . esc_attr($align) . '" style="padding:11px 14px;border-bottom:1px solid #f1f2f4;vertical-align:top;">' . $html . '</td>';
    }

    /**
     * A small rounded count badge.
     *
     * @param int    $n
     * @param string $type success|danger|muted
     *
     * @return string
     */
    private function countBadge($n, $type)
    {
        $map = [
            'success' => ['#15803d', '#ecfdf5'],
            'danger'  => ['#b91c1c', '#fef2f2'],
            'muted'   => ['#6b7280', '#f3f4f6'],
        ];
        $colors = isset($map[$type]) ? $map[$type] : $map['muted'];

        return '<span style="display:inline-block;min-width:20px;text-align:center;padding:2px 9px;border-radius:999px;font-size:12px;font-weight:700;color:' . $colors[0] . ';background:' . $colors[1] . ';">'
            . esc_html($n) . '</span>';
    }

    /**
     * A neutral pill used for platform names.
     *
     * @param string $text
     *
     * @return string
     */
    private function platformPill($text)
    {
        return '<span style="display:inline-block;padding:2px 9px;border-radius:6px;font-size:12px;font-weight:600;color:#3730a3;background:#eef1ff;">'
            . esc_html($text) . '</span>';
    }

    /**
     * Admin URL of the Bit Social logs screen.
     *
     * @return string
     */
    private function logsUrl()
    {
        return admin_url('admin.php?page=' . FreeConfig::SLUG . '#/logs');
    }

    /**
     * A "see N more in logs" line shown under a truncated list.
     *
     * @param int $remaining number of rows not shown
     *
     * @return string
     */
    private function seeMoreLine($remaining)
    {
        if ($remaining < 1) {
            return '';
        }

        return '<p style="margin:10px 2px 0;font-size:13px;color:#6b7280;">'
            . '<a href="' . esc_url($this->logsUrl()) . '" style="color:#4059ff;text-decoration:none;font-weight:600;">'
            // translators: %d: number of additional rows not shown in the email
            . sprintf(esc_html(_n('See %d more in logs', 'See %d more in logs', $remaining, 'bit-social')), $remaining)
            . ' →</a></p>';
    }

    /**
     * @param string $url
     * @param string $text
     *
     * @return string
     */
    private function link($url, $text)
    {
        return '<a href="' . esc_url($url) . '" style="color:#4059ff;text-decoration:none;font-weight:500;">' . esc_html($text) . '</a>';
    }

    /**
     * Sends an HTML email.
     *
     * @param array  $recipients
     * @param string $subject
     * @param string $htmlBody
     *
     * @return bool whether wp_mail() accepted the message
     */
    private function send($recipients, $subject, $htmlBody)
    {
        $contentType = static function () {
            return 'text/html';
        };

        add_filter('wp_mail_content_type', $contentType);
        $sent = wp_mail($recipients, $subject, $htmlBody);
        remove_filter('wp_mail_content_type', $contentType);

        return (bool) $sent;
    }
}
