<?php

namespace SoftminalB2B\Http\Controllers;

if (!defined('ABSPATH')) {
    exit;
}

use SoftminalB2B\Messaging\Emails;
use SoftminalB2B\Messaging\Presenter;
use SoftminalB2B\Models\Conversation;
use SoftminalB2B\Models\Message;

/**
 * Merchant side of Communication & Negotiations ([[project-messaging-feature]]).
 *
 * The whole group is `can:manage_woocommerce` — these serve the shop, reading
 * and answering every buyer thread. Basic buyer<->merchant messaging is a Free
 * feature (an acquisition lever; B2BKing paywalls buyer messaging).
 */
class MessagesController extends Controller
{
    private const PER_PAGE  = 15;
    private const MAX_BODY  = 5000;

    /**
     * The inbox: conversations newest-activity first, with the shop's unread
     * count per row. Optional `status` (open|resolved) and `search` (subject).
     */
    public function index(\WP_REST_Request $request, $params = [])
    {
        $page   = max(1, absint($request->get_param('page')));
        $status = sanitize_key((string) $request->get_param('status'));
        $search = trim(sanitize_text_field((string) $request->get_param('search')));

        // Order by last_message_id (unique + monotonic with time) rather than
        // last_message_at (non-unique): equal timestamps make OFFSET pagination
        // non-deterministic, which duplicates/skips rows as the infinite-scroll
        // inbox pages through it.
        $query = Conversation::query()->orderBy('last_message_id', 'DESC');

        if (in_array($status, [Conversation::STATUS_OPEN, Conversation::STATUS_RESOLVED], true)) {
            $query->andWhere('status', '=', $status);
        }

        if ($search !== '') {
            $query->andWhere('subject', 'LIKE', $search);
        }

        $result = $query->paginate(self::PER_PAGE, $page);

        $conversations = iterator_to_array($result['data']);
        $ids           = array_map(static fn (Conversation $c) => (int) $c->id, $conversations);
        $unread        = Conversation::shopUnreadCounts($ids);

        // Conversation rows come from a custom table, so (unlike WP_User_Query
        // screens) nothing has primed the user cache. Prime the page's buyers in
        // one query so Presenter::summary()'s per-row get_userdata() is a cache hit.
        $buyerIds = array_values(array_unique(array_filter(
            array_map(static fn (Conversation $c) => (int) $c->buyer_id, $conversations)
        )));
        if (!empty($buyerIds)) {
            cache_users($buyerIds);
        }

        $rows = array_map(
            static fn (Conversation $c) => Presenter::summary($c, Message::AUTHOR_SHOP, $unread[(int) $c->id] ?? 0),
            $conversations
        );

        return $this->success([
            'conversations' => $rows,
            'meta'          => $result['meta'],
        ]);
    }

    /**
     * One thread. Opening it advances the shop's read cursor to the newest
     * message, so the bell and per-row counts clear.
     */
    public function show(\WP_REST_Request $request, $params = [])
    {
        $conversation = $this->findOrNull($request);

        if (!$conversation) {
            return $this->notFound();
        }

        $conversation->markRead(Message::AUTHOR_SHOP);

        return $this->success(['conversation' => Presenter::thread($conversation, Message::AUTHOR_SHOP)]);
    }

    /**
     * Post a reply as the shop and email the buyer.
     */
    public function reply(\WP_REST_Request $request, $params = [])
    {
        $conversation = $this->findOrNull($request);

        if (!$conversation) {
            return $this->notFound();
        }

        $body = $this->cleanBody($request->get_param('body'));

        if ($body === '') {
            return $this->error(__('Write a message before sending.', 'softminal-b2b-wholesale-for-woocommerce'), 422);
        }

        // A reply reopens a resolved thread — the buyer is being written to.
        // Saved now (not just in-memory) so a handler that reloads the thread on
        // its own instance still sees it open.
        if ($conversation->status === Conversation::STATUS_RESOLVED) {
            $conversation->status = Conversation::STATUS_OPEN;
            $conversation->save();
        }

        $userId = get_current_user_id();

        // Quote-anchored threads are owned by the quote flow (Pro). Let it write
        // the reply and send its own single, debounced notification, so a reply
        // sent from the Inbox never diverges from one sent in the quote drawer
        // and the buyer is not emailed twice. A handler returns true once taken.
        if (apply_filters('smb2b_conversation_reply_handled', false, $conversation, $userId, Message::AUTHOR_SHOP, $body)) {
            $conversation = Conversation::find((int) $conversation->id);

            return $this->success(
                ['conversation' => Presenter::thread($conversation, Message::AUTHOR_SHOP)],
                __('Reply sent.', 'softminal-b2b-wholesale-for-woocommerce')
            );
        }

        $message = $conversation->addMessage($userId, Message::AUTHOR_SHOP, $body);

        Emails::notifyBuyer($conversation, $message);

        /**
         * Fires after the shop replies to a buyer.
         *
         * @param Conversation $conversation
         * @param Message      $message
         */
        do_action('smb2b_message_sent', $conversation, $message);

        return $this->success(
            ['conversation' => Presenter::thread($conversation, Message::AUTHOR_SHOP)],
            __('Reply sent.', 'softminal-b2b-wholesale-for-woocommerce')
        );
    }

    /**
     * Resolve or reopen a thread. The change is recorded as a system line in the
     * thread itself, so the buyer sees it and the read cursors cover the event.
     */
    public function setStatus(\WP_REST_Request $request, $params = [])
    {
        $conversation = $this->findOrNull($request);

        if (!$conversation) {
            return $this->notFound();
        }

        $resolved = filter_var($request->get_param('resolved'), FILTER_VALIDATE_BOOLEAN);
        $target   = $resolved ? Conversation::STATUS_RESOLVED : Conversation::STATUS_OPEN;

        if ($conversation->status !== $target) {
            $conversation->addMessage(
                get_current_user_id(),
                Message::AUTHOR_SYSTEM,
                $resolved
                    ? __('The store marked this conversation resolved.', 'softminal-b2b-wholesale-for-woocommerce')
                    : __('The store reopened this conversation.', 'softminal-b2b-wholesale-for-woocommerce')
            );

            // addMessage already saved the system line + summary; set status and
            // re-read the shop cursor past that line so resolving does not leave
            // the thread looking unread to the shop.
            $conversation->status = $target;
            $conversation->markRead(Message::AUTHOR_SHOP);
            $conversation->save();
        }

        return $this->success(
            ['conversation' => Presenter::thread($conversation, Message::AUTHOR_SHOP)],
            $resolved
                ? __('Marked resolved.', 'softminal-b2b-wholesale-for-woocommerce')
                : __('Reopened.', 'softminal-b2b-wholesale-for-woocommerce')
        );
    }

    /**
     * Delete a conversation and all its messages. Guarded: a quote-anchored
     * thread is that quote's negotiation history, so it is NOT deletable here —
     * the merchant deletes the quote instead (which unanchors this thread).
     * General / product / order inquiries can be removed (e.g. spam).
     */
    public function destroy(\WP_REST_Request $request, $params = [])
    {
        $conversation = $this->findOrNull($request);

        if (!$conversation) {
            return $this->notFound();
        }

        if ((string) $conversation->anchor_type === 'quote') {
            return $this->error(__('This conversation belongs to a quote. Delete the quote instead.', 'softminal-b2b-wholesale-for-woocommerce'), 422);
        }

        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->delete(Message::table(), ['conversation_id' => (int) $conversation->id]);
        $conversation->delete();

        return $this->success([], __('Conversation deleted.', 'softminal-b2b-wholesale-for-woocommerce'));
    }

    /**
     * Unread-thread count for the SPA bell (60s poll). Cheap column comparison.
     */
    public function unreadCount(\WP_REST_Request $request, $params = [])
    {
        return $this->success(['count' => Conversation::unreadThreadCountForShop()]);
    }

    private function findOrNull(\WP_REST_Request $request): ?Conversation
    {
        $id = absint($request->get_param('id'));

        return $id ? Conversation::find($id) : null;
    }

    private function cleanBody($raw): string
    {
        $body = trim(sanitize_textarea_field((string) $raw));

        return function_exists('mb_substr') ? mb_substr($body, 0, self::MAX_BODY) : substr($body, 0, self::MAX_BODY);
    }
}
